diff --git a/packages/kilo-vscode/tests/unit/permission-queue.test.ts b/packages/kilo-vscode/tests/unit/permission-queue.test.ts new file mode 100644 index 000000000..e1fc36058 --- /dev/null +++ b/packages/kilo-vscode/tests/unit/permission-queue.test.ts @@ -0,0 +1,53 @@ +import { describe, expect, it } from "bun:test" +import { removeSessionPermissions, upsertPermission } from "../../webview-ui/src/context/permission-queue" +import type { PermissionRequest } from "../../webview-ui/src/types/messages" + +const permission = (input: Partial = {}): PermissionRequest => ({ + id: input.id ?? "perm-1", + sessionID: input.sessionID ?? "session-1", + toolName: input.toolName ?? "read", + patterns: input.patterns ?? ["/tmp/*"], + args: input.args ?? {}, + message: input.message, + tool: input.tool, +}) + +describe("permission queue", () => { + it("appends a new permission id", () => { + const result = upsertPermission([], permission({ id: "perm-1" })) + expect(result).toHaveLength(1) + expect(result[0].id).toBe("perm-1") + }) + + it("updates an existing permission id instead of duplicating", () => { + const existing = permission({ id: "perm-1", toolName: "read", patterns: ["a"] }) + const incoming = permission({ id: "perm-1", toolName: "write", patterns: ["b"] }) + + const result = upsertPermission([existing], incoming) + + expect(result).toHaveLength(1) + expect(result[0]).toEqual(incoming) + }) + + it("keeps other permission entries when updating one id", () => { + const first = permission({ id: "perm-1", sessionID: "session-1" }) + const second = permission({ id: "perm-2", sessionID: "session-2" }) + const incoming = permission({ id: "perm-1", toolName: "edit" }) + + const result = upsertPermission([first, second], incoming) + + expect(result).toHaveLength(2) + expect(result.find((item) => item.id === "perm-1")).toEqual(incoming) + expect(result.find((item) => item.id === "perm-2")).toEqual(second) + }) + + it("removes only permissions from the deleted session", () => { + const first = permission({ id: "perm-1", sessionID: "session-1" }) + const second = permission({ id: "perm-2", sessionID: "session-2" }) + const third = permission({ id: "perm-3", sessionID: "session-1" }) + + const result = removeSessionPermissions([first, second, third], "session-1") + + expect(result).toEqual([second]) + }) +}) diff --git a/packages/kilo-vscode/webview-ui/src/App.tsx b/packages/kilo-vscode/webview-ui/src/App.tsx index f940c36e4..3578ebb36 100644 --- a/packages/kilo-vscode/webview-ui/src/App.tsx +++ b/packages/kilo-vscode/webview-ui/src/App.tsx @@ -50,15 +50,21 @@ export const DataBridge: Component<{ children: any }> = (props) => { const data = createMemo(() => { const id = session.currentSessionID() - const msgs = session.allMessages() - const parts = session.allParts() + const perms = id ? session.permissions().filter((p) => p.sessionID === id) : [] return { session: session.sessions().map((s) => ({ ...s, id: s.id, role: "user" as const })) as unknown as any[], session_status: {} as Record, session_diff: {} as Record, - message: msgs as unknown as Record, - part: parts as unknown as Record, - permission: id ? { [id]: session.permissions() as unknown as any[] } : {}, + message: id ? { [id]: session.messages() as unknown as SDKMessage[] } : {}, + part: id + ? Object.fromEntries( + session + .messages() + .map((msg) => [msg.id, session.getParts(msg.id) as unknown as SDKPart[]]) + .filter(([, parts]) => (parts as SDKPart[]).length > 0), + ) + : {}, + permission: id ? { [id]: perms as unknown as any[] } : {}, } }) diff --git a/packages/kilo-vscode/webview-ui/src/components/chat/ChatView.tsx b/packages/kilo-vscode/webview-ui/src/components/chat/ChatView.tsx index 56db2a1eb..f8e12ebae 100644 --- a/packages/kilo-vscode/webview-ui/src/components/chat/ChatView.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/chat/ChatView.tsx @@ -27,7 +27,7 @@ export const ChatView: Component = (props) => { const sessionPermissions = () => session.permissions().filter((p) => p.sessionID === id()) const questionRequest = () => sessionQuestions()[0] - const permissionRequest = () => sessionPermissions()[0] + const permissionRequest = () => sessionPermissions().find((p) => !p.tool) const blocked = () => sessionPermissions().length > 0 || sessionQuestions().length > 0 const [responding, setResponding] = createSignal(false) diff --git a/packages/kilo-vscode/webview-ui/src/context/permission-queue.ts b/packages/kilo-vscode/webview-ui/src/context/permission-queue.ts new file mode 100644 index 000000000..da977468a --- /dev/null +++ b/packages/kilo-vscode/webview-ui/src/context/permission-queue.ts @@ -0,0 +1,13 @@ +import type { PermissionRequest } from "../types/messages" + +export function upsertPermission(list: PermissionRequest[], permission: PermissionRequest) { + const idx = list.findIndex((item) => item.id === permission.id) + if (idx === -1) return [...list, permission] + const next = list.slice() + next[idx] = permission + return next +} + +export function removeSessionPermissions(list: PermissionRequest[], sessionID: string) { + return list.filter((item) => item.sessionID !== sessionID) +} diff --git a/packages/kilo-vscode/webview-ui/src/context/session.tsx b/packages/kilo-vscode/webview-ui/src/context/session.tsx index 3718b6606..00a454de2 100644 --- a/packages/kilo-vscode/webview-ui/src/context/session.tsx +++ b/packages/kilo-vscode/webview-ui/src/context/session.tsx @@ -37,6 +37,7 @@ import type { ExtensionMessage, FileAttachment, } from "../types/messages" +import { removeSessionPermissions, upsertPermission } from "./permission-queue" // Derive human-readable status from the last streaming part function computeStatus( @@ -508,7 +509,7 @@ export const SessionProvider: ParentComponent = (props) => { } function handlePermissionRequest(permission: PermissionRequest) { - setPermissions((prev) => [...prev, permission]) + setPermissions((prev) => upsertPermission(prev, permission)) } function handleQuestionRequest(question: QuestionRequest) { @@ -603,6 +604,7 @@ export const SessionProvider: ParentComponent = (props) => { return next }) } + setPermissions((prev) => removeSessionPermissions(prev, sessionID)) setStatusMap( produce((map) => { delete map[sessionID]