mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-08-28 11:05:31 +08:00
fix(vscode): restore Agent Manager terminals (#12720)
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Fix Agent Manager embedded terminals not opening in the side panel or as a tab.
|
||||
@@ -134,6 +134,7 @@ export class AgentManagerProvider implements Disposable {
|
||||
getServerConfig: () => this.connectionService.getServerConfig() ?? undefined,
|
||||
getRoot: () => this.getRoot(),
|
||||
getWorktreePath: (id) => this.getStateManager()?.getWorktree(id)?.path,
|
||||
getProjectId: () => this.projectScope.current()?.id ?? this.contexts.active()?.id,
|
||||
log: (...args) => this.log("[XTerm]", ...args),
|
||||
post: (msg) => this.postToWebview(msg),
|
||||
getTerminalFont: () => readTerminalFont(),
|
||||
|
||||
@@ -34,6 +34,10 @@ export interface TerminalRoutingDeps {
|
||||
getRoot(): string | undefined
|
||||
/** Resolve a worktree id to its on-disk path, or undefined if unknown. */
|
||||
getWorktreePath(worktreeId: string): string | undefined
|
||||
/** Project the current message is dispatched for; stamped onto
|
||||
* `terminal.created` so the webview can namespace its per-project
|
||||
* terminal state (worktree ids collide across projects). */
|
||||
getProjectId(): string | undefined
|
||||
/** Output channel log — prefixed by the caller. */
|
||||
log(...args: unknown[]): void
|
||||
/** Send a message back to the webview. */
|
||||
@@ -115,6 +119,9 @@ export class TerminalRouter {
|
||||
const generation = this.generation
|
||||
const manager = this.manager
|
||||
const cwd = this.resolveCwd(worktreeId)
|
||||
// Captured synchronously: the project scope is only current while the
|
||||
// dispatch runs, not when the async create settles.
|
||||
const pid = this.deps.getProjectId()
|
||||
if (!cwd) {
|
||||
this.deps.post({
|
||||
type: "agentManager.terminal.error",
|
||||
@@ -141,6 +148,7 @@ export class TerminalRouter {
|
||||
createId,
|
||||
placement,
|
||||
worktreeId: created.worktreeId,
|
||||
...(pid ? { projectId: pid } : {}),
|
||||
terminalId: created.terminalId,
|
||||
title: created.title,
|
||||
wsUrl: created.wsUrl,
|
||||
|
||||
@@ -190,6 +190,9 @@ interface TerminalCreatedMessage {
|
||||
placement: TerminalPlacement
|
||||
/** null for LOCAL, worktree id otherwise */
|
||||
worktreeId: string | null
|
||||
/** Project that owns the create; the webview namespaces its per-project
|
||||
* terminal state with it (mirrors `ScriptTerminalView.projectId`). */
|
||||
projectId?: string
|
||||
terminalId: string
|
||||
title: string
|
||||
wsUrl: string
|
||||
|
||||
@@ -57,6 +57,7 @@ describe("Agent Manager terminal font", () => {
|
||||
getServerConfig: () => ({ baseUrl: "http://127.0.0.1:4096", password: "secret" }),
|
||||
getRoot: () => "/workspace",
|
||||
getWorktreePath: () => undefined,
|
||||
getProjectId: () => "prj-1",
|
||||
log: () => undefined,
|
||||
post: resolve,
|
||||
getTerminalFont: () => font,
|
||||
|
||||
@@ -25,6 +25,7 @@ describe("Agent Manager terminal routing", () => {
|
||||
getServerConfig: () => ({ baseUrl: "http://127.0.0.1:4096", password: "secret" }),
|
||||
getRoot: () => "/workspace",
|
||||
getWorktreePath: (id) => (id === "wt-1" ? "/workspace/wt-1" : undefined),
|
||||
getProjectId: () => "prj-1",
|
||||
log: () => undefined,
|
||||
post: (message) => messages.push(message),
|
||||
getTerminalFont: () => font,
|
||||
@@ -42,6 +43,7 @@ describe("Agent Manager terminal routing", () => {
|
||||
createId: "side-1",
|
||||
placement: "side",
|
||||
worktreeId: "wt-1",
|
||||
projectId: "prj-1",
|
||||
})
|
||||
|
||||
router.handle({
|
||||
@@ -83,6 +85,7 @@ describe("Agent Manager terminal routing", () => {
|
||||
getServerConfig: () => ({ baseUrl: "http://127.0.0.1:4096", password: "secret" }),
|
||||
getRoot: () => "/workspace",
|
||||
getWorktreePath: () => undefined,
|
||||
getProjectId: () => "prj-1",
|
||||
log: () => undefined,
|
||||
post: (message) => messages.push(message),
|
||||
getTerminalFont: () => font,
|
||||
@@ -132,6 +135,7 @@ describe("Agent Manager terminal routing", () => {
|
||||
getServerConfig: () => ({ baseUrl: "http://127.0.0.1:4096", password: "secret" }),
|
||||
getRoot: () => "/workspace",
|
||||
getWorktreePath: () => undefined,
|
||||
getProjectId: () => "prj-1",
|
||||
log: () => undefined,
|
||||
post: (message) => messages.push(message),
|
||||
getTerminalFont: () => font,
|
||||
@@ -178,6 +182,7 @@ describe("Agent Manager terminal routing", () => {
|
||||
getServerConfig: () => ({ baseUrl: "http://127.0.0.1:4096", password: "secret" }),
|
||||
getRoot: () => "/workspace",
|
||||
getWorktreePath: () => undefined,
|
||||
getProjectId: () => "prj-1",
|
||||
log: () => undefined,
|
||||
post: (message) => messages.push(message),
|
||||
getTerminalFont: () => font,
|
||||
@@ -217,6 +222,7 @@ describe("Agent Manager terminal routing", () => {
|
||||
getServerConfig: () => ({ baseUrl: "http://127.0.0.1:4096", password: "secret" }),
|
||||
getRoot: () => "/workspace",
|
||||
getWorktreePath: () => undefined,
|
||||
getProjectId: () => "prj-1",
|
||||
log: () => undefined,
|
||||
post: () => undefined,
|
||||
getTerminalFont: () => font,
|
||||
@@ -262,6 +268,7 @@ describe("Agent Manager terminal routing", () => {
|
||||
getServerConfig: () => ({ baseUrl: "http://127.0.0.1:4096", password: "secret" }),
|
||||
getRoot: () => "/workspace",
|
||||
getWorktreePath: () => undefined,
|
||||
getProjectId: () => "prj-1",
|
||||
log: () => undefined,
|
||||
post: (message) => messages.push(message),
|
||||
getTerminalFont: () => font,
|
||||
|
||||
@@ -561,4 +561,102 @@ describe("Agent Manager terminal state", () => {
|
||||
dispose()
|
||||
})
|
||||
})
|
||||
|
||||
// Multi-project regression: AgentManagerApp feeds createTerminalState a
|
||||
// project-namespaced selection accessor (`${projectId}:${sel}`) so ids
|
||||
// from different projects never collide. The wire protocol still speaks
|
||||
// plain worktree ids, and `terminal.created` carries the owning
|
||||
// `projectId` so the answer lands back in the namespaced context.
|
||||
function nsScene(initial: string | null = LOCAL, pid = "prj-1") {
|
||||
const [selection, setSelection] = createSignal<string | null>(initial)
|
||||
const ns = (sel: string) => `${pid}:${sel}`
|
||||
const state = createTerminalState(() => {
|
||||
const sel = selection()
|
||||
return sel === null ? null : ns(sel)
|
||||
})
|
||||
const posted: Array<Record<string, unknown>> = []
|
||||
const events = { activated: [] as string[], selected: [] as string[], created: [] as string[] }
|
||||
const handlers = createTerminalHandlers({
|
||||
state,
|
||||
tabIds: () => state.current().map((term) => term.id),
|
||||
selectReview: () => undefined,
|
||||
selectSessionTab: () => undefined,
|
||||
clearSession: () => undefined,
|
||||
resetOthers: () => undefined,
|
||||
isPendingId: () => false,
|
||||
findTab: () => undefined,
|
||||
postMessage: (message) => posted.push(message as Record<string, unknown>),
|
||||
onShowSide: () => undefined,
|
||||
getSelection: selection,
|
||||
LOCAL,
|
||||
REVIEW_TAB_ID: "review",
|
||||
})
|
||||
const dispatch = createTerminalMessageHandler({
|
||||
state,
|
||||
activate: (id) => events.activated.push(id),
|
||||
saveTabMemory: () => undefined,
|
||||
setSelection: (value) => events.selected.push(value),
|
||||
showError: () => undefined,
|
||||
postMessage: (message) => posted.push(message as Record<string, unknown>),
|
||||
onCreated: (contextKey) => events.created.push(contextKey),
|
||||
})
|
||||
return { state, posted, events, handlers, dispatch, ns }
|
||||
}
|
||||
|
||||
it("keeps the namespaced state key out of side creates and buckets project-stamped answers", () => {
|
||||
createRoot((dispose) => {
|
||||
const item = nsScene(LOCAL)
|
||||
item.handlers.requestSide()
|
||||
|
||||
expect(item.posted).toHaveLength(1)
|
||||
const request = item.posted[0]!
|
||||
expect(request).toMatchObject({ type: "agentManager.terminal.create", placement: "side", worktreeId: null })
|
||||
expect(item.dispatch({ ...createdSide(String(request.createId), "terminal:side"), projectId: "prj-1" })).toBe(
|
||||
true,
|
||||
)
|
||||
expect(item.state.sideKey()).toBe("prj-1:local")
|
||||
expect(item.state.sides().map((term) => term.id)).toEqual(["terminal:side"])
|
||||
expect(item.state.sideActiveFor("prj-1:local")).toBe("terminal:side")
|
||||
|
||||
// A worktree context sends its plain worktree id, not "prj-1:wt-1".
|
||||
const wt = nsScene("wt-1")
|
||||
wt.handlers.addSide()
|
||||
expect(wt.posted[0]).toMatchObject({
|
||||
type: "agentManager.terminal.create",
|
||||
placement: "side",
|
||||
worktreeId: "wt-1",
|
||||
})
|
||||
dispose()
|
||||
})
|
||||
})
|
||||
|
||||
it("buckets project-stamped tab terminals under the namespaced context", () => {
|
||||
createRoot((dispose) => {
|
||||
const item = nsScene(LOCAL)
|
||||
item.handlers.requestNew()
|
||||
expect(item.posted[0]).toMatchObject({
|
||||
type: "agentManager.terminal.create",
|
||||
placement: "tab",
|
||||
worktreeId: null,
|
||||
})
|
||||
const created: ExtensionMessage = {
|
||||
type: "agentManager.terminal.created",
|
||||
createId: String(item.posted[0]!.createId),
|
||||
placement: "tab",
|
||||
worktreeId: null,
|
||||
projectId: "prj-1",
|
||||
terminalId: "terminal:tab",
|
||||
title: "Terminal 1",
|
||||
wsUrl: "ws://tab",
|
||||
font,
|
||||
}
|
||||
expect(item.dispatch(created)).toBe(true)
|
||||
expect(item.state.current().map((term) => term.id)).toEqual(["terminal:tab"])
|
||||
// Selection and tab order stay on the plain protocol id.
|
||||
expect(item.events.selected).toEqual([LOCAL])
|
||||
expect(item.events.created).toEqual([LOCAL])
|
||||
expect(item.events.activated).toEqual(["terminal:tab"])
|
||||
dispose()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -890,6 +890,7 @@ const AgentManagerContent: Component = () => {
|
||||
post: (msg: unknown) => vscode.postMessage(msg as never),
|
||||
tabMemory,
|
||||
terms,
|
||||
nsKey,
|
||||
activateTerminal: (id: string) => termHandlers.activate(id),
|
||||
setActivePendingId,
|
||||
selectSession: session.selectSession,
|
||||
@@ -2066,9 +2067,11 @@ const AgentManagerContent: Component = () => {
|
||||
setLocalSessionIDs(sessionSubset)
|
||||
}
|
||||
// Mirror the order into the terminal state so `terms.current()`
|
||||
// (the source for renderTerminalLayer's slot order) matches.
|
||||
// (the source for renderTerminalLayer's slot order) matches. The
|
||||
// terminal state is keyed by namespaced context, not the plain
|
||||
// tab-order key.
|
||||
const terminalSubset = reordered.filter(isTerminalTabId)
|
||||
if (terminalSubset.length > 0) terms.reorder(key, terminalSubset)
|
||||
if (terminalSubset.length > 0) terms.reorder(nsKey(key), terminalSubset)
|
||||
}
|
||||
|
||||
const handleDragEnd = () => {
|
||||
|
||||
@@ -24,6 +24,9 @@ export interface SelectionActionDeps<T extends SessionLike> {
|
||||
post: (msg: unknown) => void
|
||||
tabMemory: () => Record<string, string>
|
||||
terms: TermState
|
||||
/** Terminal state is keyed by project-namespaced context; map a plain
|
||||
* selection ("local" or a worktree id) to its terminal-state key. */
|
||||
nsKey: (sel: string) => string
|
||||
activateTerminal: (id: string) => void
|
||||
setActivePendingId: (id: string | undefined) => void
|
||||
selectSession: (id: string) => void
|
||||
@@ -40,7 +43,7 @@ export function selectLocalAction<T extends SessionLike>(deps: SelectionActionDe
|
||||
deps.setSelection(LOCAL)
|
||||
deps.post({ type: "agentManager.requestRepoInfo" })
|
||||
const remembered = deps.tabMemory()[LOCAL]
|
||||
if (deps.terms.hasRemembered(LOCAL, remembered)) return deps.activateTerminal(remembered!)
|
||||
if (deps.terms.hasRemembered(deps.nsKey(LOCAL), remembered)) return deps.activateTerminal(remembered!)
|
||||
deps.terms.setActiveId(undefined)
|
||||
const target = remembered ? locals.find((s) => s.id === remembered) : undefined
|
||||
const fallback = target ?? locals[0]
|
||||
@@ -64,7 +67,7 @@ export function selectWorktreeAction<T extends SessionLike>(
|
||||
deps.saveTabMemory()
|
||||
deps.setSelection(worktreeId)
|
||||
const remembered = deps.tabMemory()[worktreeId]
|
||||
if (deps.terms.hasRemembered(worktreeId, remembered)) return deps.activateTerminal(remembered!)
|
||||
if (deps.terms.hasRemembered(deps.nsKey(worktreeId), remembered)) return deps.activateTerminal(remembered!)
|
||||
deps.terms.setActiveId(undefined)
|
||||
const target = remembered ? sessions.find((s) => s.id === remembered) : undefined
|
||||
const fallback = target ?? sessions[0]
|
||||
|
||||
@@ -639,13 +639,16 @@ export function createTerminalHandlers(deps: TerminalHandlerDeps) {
|
||||
|
||||
const createSide = () => {
|
||||
const key = deps.state.sideKey()
|
||||
// The wire protocol speaks plain worktree ids; `sideKey` is the
|
||||
// project-namespaced state key and must not leak into the message.
|
||||
const sel = deps.getSelection()
|
||||
const id = newId()
|
||||
deps.state.beginSide(key, id)
|
||||
deps.postMessage({
|
||||
type: "agentManager.terminal.create",
|
||||
createId: id,
|
||||
placement: "side",
|
||||
worktreeId: key === deps.LOCAL ? null : key,
|
||||
worktreeId: sel === null || sel === deps.LOCAL ? null : sel,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -838,7 +841,10 @@ type CreatedMessage = Extract<ExtensionMessage, { type: "agentManager.terminal.c
|
||||
type ScriptTerminalsMessage = Extract<ExtensionMessage, { type: "agentManager.scriptTerminals" }>
|
||||
|
||||
function handleCreated(deps: TerminalMessageHandlerDeps, msg: CreatedMessage) {
|
||||
const contextKey = msg.worktreeId === null ? LOCAL : msg.worktreeId
|
||||
// `target` is the plain protocol id (selection/tab-order keys); `key` is
|
||||
// the project-namespaced terminal-state key (same shape as syncScripts).
|
||||
const target = msg.worktreeId === null ? LOCAL : msg.worktreeId
|
||||
const key = msg.projectId ? `${msg.projectId}:${target}` : target
|
||||
const term = {
|
||||
id: msg.terminalId,
|
||||
title: msg.title,
|
||||
@@ -852,20 +858,20 @@ function handleCreated(deps: TerminalMessageHandlerDeps, msg: CreatedMessage) {
|
||||
// reloaded (or the context is gone) — close the PTY again instead
|
||||
// of leaking it.
|
||||
const request = deps.state.completeSide(msg.createId)
|
||||
if (!request || request.contextKey !== contextKey) {
|
||||
if (!request || request.contextKey !== key) {
|
||||
deps.postMessage({ type: "agentManager.terminal.close", terminalId: msg.terminalId })
|
||||
return
|
||||
}
|
||||
deps.state.add(msg.worktreeId, term)
|
||||
deps.state.add(key === LOCAL ? null : key, term)
|
||||
// The newest terminal becomes the visible one in its panel.
|
||||
deps.state.setSideActive(contextKey, msg.terminalId)
|
||||
deps.onSideCreated?.(contextKey, msg.terminalId)
|
||||
deps.state.setSideActive(key, msg.terminalId)
|
||||
deps.onSideCreated?.(key, msg.terminalId)
|
||||
return
|
||||
}
|
||||
deps.state.add(msg.worktreeId, term)
|
||||
deps.onCreated?.(contextKey, msg.terminalId)
|
||||
deps.state.add(key === LOCAL ? null : key, term)
|
||||
deps.onCreated?.(target, msg.terminalId)
|
||||
deps.saveTabMemory()
|
||||
deps.setSelection(contextKey)
|
||||
deps.setSelection(target)
|
||||
deps.activate(msg.terminalId)
|
||||
}
|
||||
|
||||
|
||||
@@ -785,6 +785,9 @@ export interface AgentManagerTerminalCreatedMessage {
|
||||
placement: TerminalPlacement
|
||||
/** null for LOCAL, worktree id otherwise */
|
||||
worktreeId: string | null
|
||||
/** Project that owns the create; the webview namespaces its per-project
|
||||
* terminal state with it (mirrors `ScriptTerminalView.projectId`). */
|
||||
projectId?: string
|
||||
terminalId: string
|
||||
title: string
|
||||
wsUrl: string
|
||||
|
||||
Reference in New Issue
Block a user