Merge pull request #10940 from mjnaderi/fix/vscode-panel-command-registration

fix(vscode): tolerate duplicate panel command registration
This commit is contained in:
Marius
2026-06-19 11:40:19 +02:00
committed by GitHub
3 changed files with 88 additions and 8 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"kilo-code": patch
---
Avoid failing Agent Manager startup when another extension already registered VS Code panel commands.
@@ -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<unknown> {
@@ -218,9 +220,20 @@ export class SessionTerminalManager {
return await this.host.executeCommand(id, ...args)
} finally {
const handler = this.commandHandlers.get(id)
if (!handler) return
const replacement = this.host.registerCommand(id, handler)
this.commandDisposables.set(id, replacement)
if (handler) {
const replacement = this.tryRegisterCommand(id, handler)
if (replacement) this.commandDisposables.set(id, replacement)
}
}
}
private tryRegisterCommand(id: string, handler: (...args: unknown[]) => Promise<unknown>): 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
}
}
@@ -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<unknown>
function runtime(run: () => Promise<unknown>) {
let blocked = false
const handlers = new Map<string, Handler>()
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 } })
@@ -88,8 +123,35 @@ 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()")
})
})
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()
})
})