From 4de82931ed3d5315e6717827a9b53b11c1162e7f Mon Sep 17 00:00:00 2001 From: Mohammad Javad Naderi Date: Fri, 5 Jun 2026 11:31:32 +0330 Subject: [PATCH 1/2] fix(vscode): tolerate duplicate panel command registration --- .changeset/quiet-terminal-panel.md | 5 +++++ .../src/agent-manager/SessionTerminalManager.ts | 17 +++++++++++++++-- .../tests/unit/session-terminal-manager.test.ts | 7 +++++++ 3 files changed, 27 insertions(+), 2 deletions(-) create mode 100644 .changeset/quiet-terminal-panel.md diff --git a/.changeset/quiet-terminal-panel.md b/.changeset/quiet-terminal-panel.md new file mode 100644 index 0000000000..794b02b799 --- /dev/null +++ b/.changeset/quiet-terminal-panel.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Avoid failing Agent Manager startup when another extension already registered VS Code panel commands. diff --git a/packages/kilo-vscode/src/agent-manager/SessionTerminalManager.ts b/packages/kilo-vscode/src/agent-manager/SessionTerminalManager.ts index 189045d5a7..0af740f994 100644 --- a/packages/kilo-vscode/src/agent-manager/SessionTerminalManager.ts +++ b/packages/kilo-vscode/src/agent-manager/SessionTerminalManager.ts @@ -203,8 +203,10 @@ export class SessionTerminalManager { return result } + const disposable = this.tryRegisterCommand(id, handler) + if (!disposable) return this.commandHandlers.set(id, handler) - this.commandDisposables.set(id, this.host.registerCommand(id, handler)) + this.commandDisposables.set(id, disposable) } private async runOriginalCommand(id: string, args: unknown[]): Promise { @@ -219,11 +221,22 @@ export class SessionTerminalManager { } finally { const handler = this.commandHandlers.get(id) if (!handler) return - const replacement = this.host.registerCommand(id, handler) + const replacement = this.tryRegisterCommand(id, handler) + if (!replacement) return this.commandDisposables.set(id, replacement) } } + private tryRegisterCommand(id: string, handler: (...args: unknown[]) => Promise): Disposable | undefined { + try { + return this.host.registerCommand(id, handler) + } catch (err) { + const msg = err instanceof Error ? err.message : String(err) + this.log(`panel command registration skipped for ${id}: ${msg}`) + return undefined + } + } + private isManaged(terminal: TerminalHandle): boolean { for (const entry of this.terminals.values()) { if (entry.terminal === terminal) return true diff --git a/packages/kilo-vscode/tests/unit/session-terminal-manager.test.ts b/packages/kilo-vscode/tests/unit/session-terminal-manager.test.ts index bb08e44b82..c7a7f29197 100644 --- a/packages/kilo-vscode/tests/unit/session-terminal-manager.test.ts +++ b/packages/kilo-vscode/tests/unit/session-terminal-manager.test.ts @@ -88,6 +88,13 @@ describe("SessionTerminalManager structure", () => { expect(text).toContain("this.showExistingLocal()") }) + it("panel command registration is best effort", () => { + const text = body("tryRegisterCommand") + expect(text).toContain("this.host.registerCommand") + expect(text).toContain("catch (err)") + expect(text).toContain("panel command registration skipped") + }) + it("exposes active terminal state for terminal context routing", () => { const text = body("hasActiveTerminal") expect(text).toContain("this.host.activeTerminal()") From 6bd1e79c588bd7614f5bf2463f5172fd14135451 Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Fri, 19 Jun 2026 11:23:35 +0200 Subject: [PATCH 2/2] fix(vscode): preserve panel command outcomes --- .../agent-manager/SessionTerminalManager.ts | 8 +-- .../unit/session-terminal-manager.test.ts | 63 +++++++++++++++++-- 2 files changed, 63 insertions(+), 8 deletions(-) diff --git a/packages/kilo-vscode/src/agent-manager/SessionTerminalManager.ts b/packages/kilo-vscode/src/agent-manager/SessionTerminalManager.ts index 0af740f994..dbe7aa4867 100644 --- a/packages/kilo-vscode/src/agent-manager/SessionTerminalManager.ts +++ b/packages/kilo-vscode/src/agent-manager/SessionTerminalManager.ts @@ -220,10 +220,10 @@ export class SessionTerminalManager { return await this.host.executeCommand(id, ...args) } finally { const handler = this.commandHandlers.get(id) - if (!handler) return - const replacement = this.tryRegisterCommand(id, handler) - if (!replacement) return - this.commandDisposables.set(id, replacement) + if (handler) { + const replacement = this.tryRegisterCommand(id, handler) + if (replacement) this.commandDisposables.set(id, replacement) + } } } diff --git a/packages/kilo-vscode/tests/unit/session-terminal-manager.test.ts b/packages/kilo-vscode/tests/unit/session-terminal-manager.test.ts index c7a7f29197..3025d3b0cb 100644 --- a/packages/kilo-vscode/tests/unit/session-terminal-manager.test.ts +++ b/packages/kilo-vscode/tests/unit/session-terminal-manager.test.ts @@ -1,18 +1,53 @@ /** * SessionTerminalManager tests. * - * The class is tightly coupled to VS Code terminal APIs. We use ts-morph - * static analysis to verify structural invariants that protect against - * real regressions — focusing on ordering constraints and cleanup logic - * that are easy to break during refactoring. + * Structural tests use ts-morph to protect ordering and cleanup invariants. + * Command behavior tests exercise the narrow TerminalHost interface directly. */ import { describe, it, expect } from "bun:test" import path from "node:path" import { Project, SyntaxKind } from "ts-morph" +import { SessionTerminalManager, type TerminalHost } from "../../src/agent-manager/SessionTerminalManager" const ROOT = path.resolve(import.meta.dir, "../..") const FILE = path.join(ROOT, "src/agent-manager/SessionTerminalManager.ts") +const COMMAND = "workbench.action.togglePanel" + +type Handler = (...args: unknown[]) => Promise + +function runtime(run: () => Promise) { + let blocked = false + const handlers = new Map() + const host: TerminalHost = { + createTerminal() { + throw new Error("not used") + }, + activeTerminal: () => undefined, + repoPath: () => undefined, + showWarning() {}, + setContext() {}, + onTerminalClosed: () => ({ dispose() {} }), + onActiveTerminalChanged: () => ({ dispose() {} }), + registerCommand(id, handler) { + if (blocked && id === COMMAND) throw new Error(`command '${id}' already exists`) + handlers.set(id, handler) + return { + dispose() { + if (handlers.get(id) === handler) handlers.delete(id) + }, + } + }, + executeCommand() { + blocked = true + return run() + }, + } + const manager = new SessionTerminalManager(() => {}, host) + const handler = handlers.get(COMMAND) + if (!handler) throw new Error(`command '${COMMAND}' was not registered`) + return { manager, handler } +} function getClass() { const project = new Project({ compilerOptions: { allowJs: true } }) @@ -100,3 +135,23 @@ describe("SessionTerminalManager structure", () => { expect(text).toContain("this.host.activeTerminal()") }) }) + +describe("SessionTerminalManager command restoration", () => { + it("preserves the original command result when re-registration fails", async () => { + const expected = { status: "complete" } + const state = runtime(async () => expected) + + expect(await state.handler()).toBe(expected) + state.manager.dispose() + }) + + it("preserves the original command error when re-registration fails", async () => { + const expected = new Error("panel command failed") + const state = runtime(async () => { + throw expected + }) + + await expect(state.handler()).rejects.toBe(expected) + state.manager.dispose() + }) +})