From cba4b1065e6e1ec02710d7f5eaff8326cf6f6759 Mon Sep 17 00:00:00 2001 From: Thomas Brugman Date: Wed, 18 Feb 2026 13:00:00 +0100 Subject: [PATCH 1/3] fix(vscode): stop duplicate permission prompts (#408) --- .../tests/unit/permission-queue.test.ts | 53 +++++++++++++++++++ packages/kilo-vscode/webview-ui/src/App.tsx | 3 +- .../src/components/chat/ChatView.tsx | 53 +------------------ .../src/context/permission-queue.ts | 13 +++++ .../webview-ui/src/context/session.tsx | 4 +- 5 files changed, 72 insertions(+), 54 deletions(-) create mode 100644 packages/kilo-vscode/tests/unit/permission-queue.test.ts create mode 100644 packages/kilo-vscode/webview-ui/src/context/permission-queue.ts 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 0000000000..e1fc360586 --- /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 bdbc1ec859..79f7b92edc 100644 --- a/packages/kilo-vscode/webview-ui/src/App.tsx +++ b/packages/kilo-vscode/webview-ui/src/App.tsx @@ -50,6 +50,7 @@ export const DataBridge: Component<{ children: any }> = (props) => { const data = createMemo(() => { const id = session.currentSessionID() + const perms = id ? session.permissions().filter((p) => p.sessionID === id) : [] return { session: session.sessions().map((s) => ({ ...s, id: s.id, role: "user" as const })), session_status: {} as Record, @@ -63,7 +64,7 @@ export const DataBridge: Component<{ children: any }> = (props) => { .filter(([, parts]) => (parts as SDKPart[]).length > 0), ) : {}, - permission: id ? { [id]: session.permissions() as unknown as any[] } : {}, + 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 d5d3ef118f..318c885c7a 100644 --- a/packages/kilo-vscode/webview-ui/src/components/chat/ChatView.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/chat/ChatView.tsx @@ -3,15 +3,12 @@ * Main chat container that combines all chat components */ -import { Component, For, Show, createSignal } from "solid-js" -import { Button } from "@kilocode/kilo-ui/button" -import { BasicTool } from "@kilocode/kilo-ui/basic-tool" +import { Component, Show } from "solid-js" import { TaskHeader } from "./TaskHeader" import { MessageList } from "./MessageList" import { PromptInput } from "./PromptInput" import { QuestionDock } from "./QuestionDock" import { useSession } from "../../context/session" -import { useLanguage } from "../../context/language" interface ChatViewProps { onSelectSession?: (id: string) => void @@ -19,26 +16,14 @@ interface ChatViewProps { export const ChatView: Component = (props) => { const session = useSession() - const language = useLanguage() const id = () => session.currentSessionID() const sessionQuestions = () => session.questions().filter((q) => q.sessionID === id()) const sessionPermissions = () => session.permissions().filter((p) => p.sessionID === id()) const questionRequest = () => sessionQuestions()[0] - const permissionRequest = () => sessionPermissions()[0] const blocked = () => sessionPermissions().length > 0 || sessionQuestions().length > 0 - const [responding, setResponding] = createSignal(false) - - const decide = (response: "once" | "always" | "reject") => { - const perm = permissionRequest() - if (!perm || responding()) return - setResponding(true) - session.respondToPermission(perm.id, response) - setResponding(false) - } - return (
@@ -50,42 +35,6 @@ export const ChatView: Component = (props) => { {(req) => } - - {(perm) => ( -
- - 0}> -
- - {(pattern) => {pattern}} - -
-
-
-
-
- - - -
-
-
- )} -
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 0000000000..da977468a6 --- /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 08a930563c..b28eaea8a6 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( @@ -489,7 +490,7 @@ export const SessionProvider: ParentComponent = (props) => { } function handlePermissionRequest(permission: PermissionRequest) { - setPermissions((prev) => [...prev, permission]) + setPermissions((prev) => upsertPermission(prev, permission)) } function handleQuestionRequest(question: QuestionRequest) { @@ -584,6 +585,7 @@ export const SessionProvider: ParentComponent = (props) => { return next }) } + setPermissions((prev) => removeSessionPermissions(prev, sessionID)) if (currentSessionID() === sessionID) { setCurrentSessionID(undefined) setStatusInfo({ type: "idle" }) From ff769cee01b0b3190e09d727b3dee630d17257a3 Mon Sep 17 00:00:00 2001 From: Thomas Brugman Date: Wed, 18 Feb 2026 21:08:30 +0100 Subject: [PATCH 2/3] Fix permission dock fallback for non-tool requests --- .../src/pages/session/session-prompt-dock.tsx | 110 +++++++++--------- 1 file changed, 56 insertions(+), 54 deletions(-) diff --git a/packages/app/src/pages/session/session-prompt-dock.tsx b/packages/app/src/pages/session/session-prompt-dock.tsx index f6029c10c0..ecb692e52f 100644 --- a/packages/app/src/pages/session/session-prompt-dock.tsx +++ b/packages/app/src/pages/session/session-prompt-dock.tsx @@ -1,5 +1,5 @@ import { For, Show } from "solid-js" -import type { QuestionRequest } from "@kilocode/sdk/v2" +import type { PermissionRequest, QuestionRequest } from "@kilocode/sdk/v2" import { Button } from "@opencode-ai/ui/button" import { BasicTool } from "@opencode-ai/ui/basic-tool" import { PromptInput } from "@/components/prompt-input" @@ -9,7 +9,7 @@ import { questionSubtitle } from "@/pages/session/session-prompt-helpers" export function SessionPromptDock(props: { centered: boolean questionRequest: () => QuestionRequest | undefined - permissionRequest: () => { patterns: string[]; permission: string } | undefined + permissionRequest: () => PermissionRequest | undefined blocked: boolean promptReady: boolean handoffPrompt?: string @@ -57,61 +57,63 @@ export function SessionPromptDock(props: { {(perm) => ( -
- - 0}> -
- - {(pattern) => {pattern}} - -
-
- -
- {props.t("settings.permissions.tool.doom_loop.description")} + +
+ + 0}> +
+ + {(pattern) => {pattern}} + +
+
+ +
+ {props.t("settings.permissions.tool.doom_loop.description")} +
+
+
+
+
+ + +
- - -
-
- - -
-
+ )} From 370e28f8a2470682eee3a64393899821eb7127e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Fri, 20 Feb 2026 11:50:25 +0100 Subject: [PATCH 3/3] fix(vscode): gate permission dock by tool presence, revert session-prompt-dock --- .../src/pages/session/session-prompt-dock.tsx | 110 +++++++++--------- .../src/components/chat/ChatView.tsx | 53 ++++++++- 2 files changed, 106 insertions(+), 57 deletions(-) diff --git a/packages/app/src/pages/session/session-prompt-dock.tsx b/packages/app/src/pages/session/session-prompt-dock.tsx index ecb692e52f..f6029c10c0 100644 --- a/packages/app/src/pages/session/session-prompt-dock.tsx +++ b/packages/app/src/pages/session/session-prompt-dock.tsx @@ -1,5 +1,5 @@ import { For, Show } from "solid-js" -import type { PermissionRequest, QuestionRequest } from "@kilocode/sdk/v2" +import type { QuestionRequest } from "@kilocode/sdk/v2" import { Button } from "@opencode-ai/ui/button" import { BasicTool } from "@opencode-ai/ui/basic-tool" import { PromptInput } from "@/components/prompt-input" @@ -9,7 +9,7 @@ import { questionSubtitle } from "@/pages/session/session-prompt-helpers" export function SessionPromptDock(props: { centered: boolean questionRequest: () => QuestionRequest | undefined - permissionRequest: () => PermissionRequest | undefined + permissionRequest: () => { patterns: string[]; permission: string } | undefined blocked: boolean promptReady: boolean handoffPrompt?: string @@ -57,63 +57,61 @@ export function SessionPromptDock(props: { {(perm) => ( - -
- - 0}> -
- - {(pattern) => {pattern}} - -
-
- -
- {props.t("settings.permissions.tool.doom_loop.description")} -
-
-
-
-
- - - +
+ + 0}> +
+ + {(pattern) => {pattern}} + +
+
+ +
+ {props.t("settings.permissions.tool.doom_loop.description")}
+
+
+
+
+ + +
- +
)} 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 318c885c7a..9ecccac5cf 100644 --- a/packages/kilo-vscode/webview-ui/src/components/chat/ChatView.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/chat/ChatView.tsx @@ -3,12 +3,15 @@ * Main chat container that combines all chat components */ -import { Component, Show } from "solid-js" +import { Component, For, Show, createSignal } from "solid-js" +import { Button } from "@kilocode/kilo-ui/button" +import { BasicTool } from "@kilocode/kilo-ui/basic-tool" import { TaskHeader } from "./TaskHeader" import { MessageList } from "./MessageList" import { PromptInput } from "./PromptInput" import { QuestionDock } from "./QuestionDock" import { useSession } from "../../context/session" +import { useLanguage } from "../../context/language" interface ChatViewProps { onSelectSession?: (id: string) => void @@ -16,14 +19,26 @@ interface ChatViewProps { export const ChatView: Component = (props) => { const session = useSession() + const language = useLanguage() const id = () => session.currentSessionID() const sessionQuestions = () => session.questions().filter((q) => q.sessionID === id()) const sessionPermissions = () => session.permissions().filter((p) => p.sessionID === id()) const questionRequest = () => sessionQuestions()[0] + const permissionRequest = () => sessionPermissions().find((p) => !p.tool) const blocked = () => sessionPermissions().length > 0 || sessionQuestions().length > 0 + const [responding, setResponding] = createSignal(false) + + const decide = (response: "once" | "always" | "reject") => { + const perm = permissionRequest() + if (!perm || responding()) return + setResponding(true) + session.respondToPermission(perm.id, response) + setResponding(false) + } + return (
@@ -35,6 +50,42 @@ export const ChatView: Component = (props) => { {(req) => } + + {(perm) => ( +
+ + 0}> +
+ + {(pattern) => {pattern}} + +
+
+
+
+
+ + + +
+
+
+ )} +