refactor(agent-manager): namespace terminal keys

This commit is contained in:
marius-kilocode
2026-07-29 10:46:07 +02:00
parent 0d853df3ec
commit 9f528a2315
2 changed files with 55 additions and 28 deletions
@@ -34,6 +34,8 @@ export interface Disposable {
*/
export class SessionTerminalManager {
private static readonly LOCAL_KEY = "__local__"
private static readonly SESSION_PREFIX = "session:"
private static readonly WORKTREE_PREFIX = "worktree:"
private terminals = new Map<string, { terminal: TerminalHandle; cwd: string }>()
private disposables: Disposable[] = []
@@ -47,10 +49,10 @@ export class SessionTerminalManager {
) {
this.disposables.push(
host.onTerminalClosed((terminal) => {
for (const [sessionId, entry] of this.terminals) {
for (const [key, entry] of this.terminals) {
if (entry.terminal !== terminal) continue
this.terminals.delete(sessionId)
this.log(`Removed terminal mapping for session ${sessionId} (terminal closed)`)
this.terminals.delete(key)
this.log(`Removed terminal mapping for ${key} (terminal closed)`)
break
}
this.updateContextKey()
@@ -86,7 +88,8 @@ export class SessionTerminalManager {
*/
showTerminal(sessionId: string, state: WorktreeStateManager | undefined): void {
// If terminal already exists, just focus it
if (this.showExisting(sessionId, false)) return
const key = SessionTerminalManager.sessionKey(sessionId)
if (this.showExistingKey(key, false)) return
const repoPath = this.host.repoPath()
const worktreePath = state?.directoryFor(sessionId)
@@ -102,7 +105,7 @@ export class SessionTerminalManager {
const worktree = session?.worktreeId ? state?.getWorktree(session.worktreeId) : undefined
const name = worktree ? `Agent: ${worktree.branch}` : "Agent: local"
this.showOrCreate(sessionId, cwd, name)
this.showOrCreate(key, cwd, name)
}
/**
@@ -110,7 +113,7 @@ export class SessionTerminalManager {
* Used when the user triggers a terminal in local mode without an active session.
*/
showLocalTerminal(): void {
if (this.showExisting(SessionTerminalManager.LOCAL_KEY, false)) return
if (this.showExistingKey(SessionTerminalManager.LOCAL_KEY, false)) return
const cwd = this.host.repoPath()
if (!cwd) {
@@ -129,8 +132,8 @@ export class SessionTerminalManager {
* worktree.
*/
showWorktreeTerminal(worktreeId: string, state: WorktreeStateManager | undefined): void {
const key = `worktree:${worktreeId}`
if (this.showExisting(key, false)) return
const key = SessionTerminalManager.worktreeKey(worktreeId)
if (this.showExistingKey(key, false)) return
const worktree = state?.getWorktree(worktreeId)
const cwd = worktree?.path ?? this.host.repoPath()
@@ -147,7 +150,7 @@ export class SessionTerminalManager {
* Show the existing local terminal if one was previously created (used on context switch).
*/
showExistingLocal(): boolean {
return this.showExisting(SessionTerminalManager.LOCAL_KEY)
return this.showExistingKey(SessionTerminalManager.LOCAL_KEY)
}
/**
@@ -180,34 +183,38 @@ export class SessionTerminalManager {
* Pass preserveFocus=true to keep focus on the current editor (default for session switching).
*/
showExisting(sessionId: string, preserveFocus = true): boolean {
const entry = this.terminals.get(sessionId)
return this.showExistingKey(SessionTerminalManager.sessionKey(sessionId), preserveFocus)
}
private showExistingKey(key: string, preserveFocus = true): boolean {
const entry = this.terminals.get(key)
if (!entry) return false
if (entry.terminal.exitStatus !== undefined) {
this.terminals.delete(sessionId)
this.log(`showExisting: terminal exited for session ${sessionId}, clearing`)
this.terminals.delete(key)
this.log(`showExisting: terminal exited for ${key}, clearing`)
return false
}
entry.terminal.show(preserveFocus)
this.panelOpen = true
this.log(`showExisting: revealed terminal for session ${sessionId}`)
this.log(`showExisting: revealed terminal for ${key}`)
return true
}
activeSession(): string | undefined {
private activeKey(): string | undefined {
const active = this.host.activeTerminal()
if (!active) return undefined
for (const [id, entry] of this.terminals) {
if (entry.terminal === active && entry.terminal.exitStatus === undefined) return id
for (const [key, entry] of this.terminals) {
if (entry.terminal === active && entry.terminal.exitStatus === undefined) return key
}
return undefined
}
prepareContext(sessionId: string): boolean {
if (this.showExisting(sessionId)) return true
const active = this.activeSession()
return !active || active === sessionId
const active = this.activeKey()
return !active || active === SessionTerminalManager.sessionKey(sessionId)
}
dispose(): void {
@@ -275,32 +282,40 @@ export class SessionTerminalManager {
void this.host.setContext("kilo-code.agentTerminalFocus", managed)
}
private showOrCreate(sessionId: string, cwd: string, name: string): void {
let entry = this.terminals.get(sessionId)
private showOrCreate(key: string, cwd: string, name: string): void {
let entry = this.terminals.get(key)
// Clean up exited terminals
if (entry && entry.terminal.exitStatus !== undefined) {
this.terminals.delete(sessionId)
this.terminals.delete(key)
entry = undefined
}
// Recreate if CWD changed
if (entry && entry.cwd !== cwd) {
entry.terminal.dispose()
this.terminals.delete(sessionId)
this.terminals.delete(key)
entry = undefined
this.log(`showTerminal: cwd changed for session ${sessionId}, recreating`)
this.log(`showTerminal: cwd changed for ${key}, recreating`)
}
if (!entry) {
const terminal = this.host.createTerminal({ cwd, name })
entry = { terminal, cwd }
this.terminals.set(sessionId, entry)
this.log(`showTerminal: created terminal for session ${sessionId} (cwd=${cwd})`)
this.terminals.set(key, entry)
this.log(`showTerminal: created terminal for ${key} (cwd=${cwd})`)
}
entry.terminal.show(false)
this.panelOpen = true
this.updateContextKey()
}
private static sessionKey(id: string): string {
return `${SessionTerminalManager.SESSION_PREFIX}${id}`
}
private static worktreeKey(id: string): string {
return `${SessionTerminalManager.WORKTREE_PREFIX}${id}`
}
}
@@ -131,8 +131,8 @@ describe("SessionTerminalManager structure", () => {
expect(text).toContain("panel command registration skipped")
})
it("resolves the session that owns the active managed terminal", () => {
const text = body("activeSession")
it("resolves the key that owns the active managed terminal", () => {
const text = body("activeKey")
expect(text).toContain("this.host.activeTerminal()")
expect(text).toContain("entry.terminal === active")
})
@@ -140,7 +140,8 @@ describe("SessionTerminalManager structure", () => {
it("rejects context capture from another managed session", () => {
const text = body("prepareContext")
expect(text).toContain("this.showExisting(sessionId)")
expect(text).toContain("this.activeSession()")
expect(text).toContain("this.activeKey()")
expect(text).toContain("SessionTerminalManager.sessionKey(sessionId)")
})
})
@@ -210,6 +211,17 @@ describe("SessionTerminalManager worktree terminals", () => {
expect(s.shown()).toBe(2)
})
it("keeps session and worktree terminal keys in separate namespaces", () => {
const s = scene({ worktreePath: "/repo/.kilo/worktrees/wt-1", repoPath: "/repo" })
s.manager.showTerminal("worktree:wt-1", undefined)
s.manager.showWorktreeTerminal("wt-1", s.state)
expect(s.created).toEqual([
{ cwd: "/repo", name: "Agent: local" },
{ cwd: "/repo/.kilo/worktrees/wt-1", name: "Agent: feature/x" },
])
expect(s.shown()).toBe(2)
})
it("falls back to the repo root when the worktree is unknown", () => {
const s = scene({ repoPath: "/repo" })
s.manager.showWorktreeTerminal("gone", s.state)