Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/quiet-terminals-switch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"kilo-code": patch
---

Open an embedded terminal automatically when switching to a worktree without one.
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ function scene(
) {
const calls = {
requestSide: 0,
ensureSide: 0,
closed: [] as string[],
hide: 0,
refocus: 0,
Expand All @@ -32,6 +33,7 @@ function scene(
calls.requestSide++
visible = true
},
ensureSide: () => calls.ensureSide++,
closeSide: (terminalId) => {
calls.closed.push(terminalId)
focusedId = undefined
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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")),
Expand All @@ -1878,6 +1880,7 @@ const AgentManagerContent: Component = () => {
) as never,
),
})
createEffect(on(terms.sideKey, (key, previous) => sideCtl.syncContext(key, previous), { defer: true }))
Comment thread
marius-kilocode marked this conversation as resolved.

const handleReviewTabMouseDown = (e: MouseEvent) => {
if (e.button !== 1) return
Expand Down
11 changes: 10 additions & 1 deletion packages/kilo-vscode/webview-ui/agent-manager/terminal/side.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ export function resolveVscodeTerminalRequest(

interface Handlers {
requestSide(): void
ensureSide(): void
closeSide(terminalId: string): boolean
}

Expand Down Expand Up @@ -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. */
Expand Down Expand Up @@ -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 }
}
29 changes: 20 additions & 9 deletions packages/kilo-vscode/webview-ui/agent-manager/terminal/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -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
Expand All @@ -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) => {
Expand Down Expand Up @@ -746,6 +756,7 @@ export function createTerminalHandlers(deps: TerminalHandlerDeps) {
deactivate,
requestNew,
requestSide,
ensureSide,
addSide,
closeActive,
}
Expand Down
Loading