diff --git a/.changeset/quiet-terminals-switch.md b/.changeset/quiet-terminals-switch.md new file mode 100644 index 00000000000..0306eb34755 --- /dev/null +++ b/.changeset/quiet-terminals-switch.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Open an embedded terminal automatically when switching to a worktree without one. diff --git a/packages/kilo-vscode/tests/unit/agent-manager-terminal-side.test.ts b/packages/kilo-vscode/tests/unit/agent-manager-terminal-side.test.ts index 1d60d6ecd2b..544cae9eb2f 100644 --- a/packages/kilo-vscode/tests/unit/agent-manager-terminal-side.test.ts +++ b/packages/kilo-vscode/tests/unit/agent-manager-terminal-side.test.ts @@ -16,6 +16,7 @@ function scene( ) { const calls = { requestSide: 0, + ensureSide: 0, closed: [] as string[], hide: 0, refocus: 0, @@ -32,6 +33,7 @@ function scene( calls.requestSide++ visible = true }, + ensureSide: () => calls.ensureSide++, closeSide: (terminalId) => { calls.closed.push(terminalId) focusedId = undefined @@ -73,6 +75,29 @@ describe("Agent Manager side terminal controller", () => { expect(hidden.calls.hide).toBe(0) }) + it("ensures an open terminal panel has a terminal after switching contexts", async () => { + const visible = scene({ visible: true }) + visible.ctl.syncContext("wt-2", "wt-1") + await Promise.resolve() + expect(visible.calls.ensureSide).toBe(1) + + visible.ctl.syncContext("wt-2", "wt-2") + visible.ctl.syncContext("wt-2", undefined) + await Promise.resolve() + expect(visible.calls.ensureSide).toBe(2) + expect(visible.calls.requestSide).toBe(0) + + const hidden = scene() + hidden.ctl.syncContext("wt-2", "wt-1") + expect(hidden.calls.ensureSide).toBe(0) + + const closed = scene({ visible: true }) + closed.ctl.syncContext("wt-2", "wt-1") + closed.ctl.toggle() + await Promise.resolve() + expect(closed.calls.ensureSide).toBe(0) + }) + it("kills the focused terminal and refocuses the chat", () => { const focused = scene({ focusedId: "terminal:two" }) expect(focused.ctl.close()).toBe(true) diff --git a/packages/kilo-vscode/tests/unit/agent-manager-terminal-state.test.ts b/packages/kilo-vscode/tests/unit/agent-manager-terminal-state.test.ts index d533f33895d..639349ae4d8 100644 --- a/packages/kilo-vscode/tests/unit/agent-manager-terminal-state.test.ts +++ b/packages/kilo-vscode/tests/unit/agent-manager-terminal-state.test.ts @@ -186,6 +186,23 @@ describe("Agent Manager terminal state", () => { }) }) + it("ensures a side terminal without revealing the panel", () => { + createRoot((dispose) => { + const item = scene("wt-1") + item.handlers.ensureSide() + item.handlers.ensureSide() + + expect(item.events.shown).toEqual([]) + expect(item.posted).toHaveLength(1) + expect(item.posted[0]).toMatchObject({ + type: "agentManager.terminal.create", + placement: "side", + worktreeId: "wt-1", + }) + dispose() + }) + }) + it("supports several side terminals per context with newest active", () => { createRoot((dispose) => { const item = scene() diff --git a/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx b/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx index 36a4653565b..8b7b719a125 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx @@ -1099,7 +1099,9 @@ const AgentManagerContent: Component = () => { onSideCreated: (contextKey, terminalId) => { // Focus only when the user is still looking at this panel — // a slow create landing after a mode switch must not steal it. - if (sidePanel() === "terminal" && terms.sideKey() === contextKey) terms.requestFocus(terminalId) + if (sidePanel() === "terminal" && !history() && !reviewActive() && terms.sideKey() === contextKey) { + terms.requestFocus(terminalId) + } }, onScriptRunning: (contextKey, terminalId) => { if (terms.sideKey() !== contextKey) return @@ -1860,7 +1862,7 @@ const AgentManagerContent: Component = () => { const sideCtl = createSideTerminal({ handlers: termHandlers, - visible: () => sidePanel() === "terminal", + visible: () => sidePanel() === "terminal" && !history() && !reviewActive(), focusedId: () => terms.sideFocusedId(), hide: () => setSidePanel(null), refocus: () => window.dispatchEvent(new Event("focusPrompt")), @@ -1878,6 +1880,7 @@ const AgentManagerContent: Component = () => { ) as never, ), }) + createEffect(on(terms.sideKey, (key, previous) => sideCtl.syncContext(key, previous), { defer: true })) const handleReviewTabMouseDown = (e: MouseEvent) => { if (e.button !== 1) return diff --git a/packages/kilo-vscode/webview-ui/agent-manager/terminal/side.ts b/packages/kilo-vscode/webview-ui/agent-manager/terminal/side.ts index d4d9c80cff9..cac99f17578 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/terminal/side.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/terminal/side.ts @@ -61,6 +61,7 @@ export function resolveVscodeTerminalRequest( interface Handlers { requestSide(): void + ensureSide(): void closeSide(terminalId: string): boolean } @@ -109,6 +110,14 @@ export function createSideTerminal(deps: SideTerminalDeps) { deps.handlers.requestSide() } + /** Keep an open terminal panel useful when its worktree context changes. */ + const syncContext = (key: string, previous: string | undefined) => { + if (key === previous || !deps.visible()) return + queueMicrotask(() => { + if (deps.visible()) deps.handlers.ensureSide() + }) + } + /** Kill the focused side terminal (Cmd/Ctrl+W). The panel stays open * on the remaining terminals, or on the empty state when this was * the last one. */ @@ -180,5 +189,5 @@ export function createSideTerminal(deps: SideTerminalDeps) { /** True while an incoming showTerminal action is the echo of `press`. */ const echo = () => Date.now() - lastPress < ECHO_MS - return { destination, syncDefault, toggle, close, openPreferred, choose, press, echo } + return { destination, syncDefault, syncContext, toggle, close, openPreferred, choose, press, echo } } diff --git a/packages/kilo-vscode/webview-ui/agent-manager/terminal/state.ts b/packages/kilo-vscode/webview-ui/agent-manager/terminal/state.ts index d68f9ae2282..60f4e930009 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/terminal/state.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/terminal/state.ts @@ -618,14 +618,8 @@ export function createTerminalHandlers(deps: TerminalHandlerDeps) { }) } - /** - * Always create a fresh side terminal for the current context (the - * panel's `+` action and empty state). Multiple creates may be in - * flight at once; each lands as its own tab in the panel strip. - */ - const addSide = () => { + const createSide = () => { const key = deps.state.sideKey() - deps.onShowSide(key) const id = newId() deps.state.beginSide(key, id) deps.postMessage({ @@ -636,6 +630,23 @@ export function createTerminalHandlers(deps: TerminalHandlerDeps) { }) } + /** + * Always create a fresh side terminal for the current context (the + * panel's `+` action and empty state). Multiple creates may be in + * flight at once; each lands as its own tab in the panel strip. + */ + const addSide = () => { + deps.onShowSide(deps.state.sideKey()) + createSide() + } + + /** Ensure the current context has a terminal without changing panel mode. */ + const ensureSide = () => { + const key = deps.state.sideKey() + if (deps.state.sidesForContext(key).length > 0 || deps.state.pendingSide(key)) return + createSide() + } + /** * Reveal the side panel and focus the context's active side terminal, * creating one when the context has none. Never touches the tab strip @@ -651,8 +662,7 @@ export function createTerminalHandlers(deps: TerminalHandlerDeps) { deps.state.requestFocus(active) return } - if (deps.state.pendingSide(key)) return - addSide() + ensureSide() } const closeTerminal = (terminalId: string) => { @@ -746,6 +756,7 @@ export function createTerminalHandlers(deps: TerminalHandlerDeps) { deactivate, requestNew, requestSide, + ensureSide, addSide, closeActive, }