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/agent-manager-terminal-focus.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"kilo-code": minor
---

Improve Agent Manager terminal focus and keyboard navigation. `Cmd+/` now focuses a visible embedded terminal before hiding it, `Cmd+Shift+T` creates a side terminal only while that terminal area has focus, and `Cmd+Shift+[` / `]` switch terminal tabs. `Cmd+Shift+M` focuses the Agent Manager prompt instead of opening VS Code Problems. `Cmd+W` hides the last side terminal instead of stopping its shell.
23 changes: 16 additions & 7 deletions packages/kilo-docs/pages/automate/agent-manager.md
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,7 @@ See [Agent Manager Workflows](/docs/automate/agent-manager-workflows#merging-wor

## Terminals

Each session has a dedicated terminal rooted in the session's worktree directory. Press `Cmd+/` (macOS) / `Ctrl+/` (Windows/Linux) to focus the terminal for the active session.
Each session has a dedicated terminal rooted in the session's worktree directory. Press `Cmd+/` (macOS) / `Ctrl+/` (Windows/Linux) to focus the terminal for the active session. If the embedded terminal is already visible but the prompt has focus, the same shortcut focuses the terminal without hiding it. Press it again while the terminal has focus to hide the panel.

### Choosing the Terminal Destination

Expand All @@ -243,11 +243,18 @@ The toolbar's terminal button is a split button: click it to open a terminal, or

The dropdown choice is remembered per panel and becomes the default for new panels. You can also set the default directly with the `kilo-code.new.agentManager.terminalButtonDestination` setting (`vscode` or `agentManager`). The `Cmd+/` (macOS) / `Ctrl+/` (Windows/Linux) shortcut follows the same destination.

With the **Agent Manager panel** destination, the terminal works like the diff panel: press `Cmd+/` to reveal it and press again to hide it. Hiding never stops the terminal — scrollback and running processes continue in the background, and focus returns to the chat input. A terminal stops only when you close its tab in the panel.
With the **Agent Manager panel** destination, the terminal works like the diff panel: press `Cmd+/` to reveal and focus it, press it while the panel is visible but another control has focus to move focus into the terminal, and press it again from the terminal to hide it. Hiding never stops the terminal — scrollback and running processes continue in the background, and focus returns to the chat input. A terminal stops only when you click its close button or type `exit` in the shell.

### Multiple Terminals

The side panel hosts multiple terminals per context (the local workspace or a worktree). The panel header is a tab strip: click a tab to switch, click **+** to open another terminal, and click **X** (or middle-click) to close a single terminal. Drag tabs to reorder them. Closing a terminal no longer hides the panel — closing the last one lands on the empty state. Pressing `Cmd+W` (macOS) / `Ctrl+W` (Windows/Linux) with a focused side terminal closes exactly that terminal.
Agent Manager has two separate terminal tab strips:

- **Main terminal tabs** appear alongside the agent session tabs. With the prompt or a main terminal focused, press `Cmd+Shift+T` / `Ctrl+Shift+T` to create another main terminal tab.
- **Side terminal tabs** appear in the terminal panel. Focus a side terminal, then press `Cmd+Shift+T` / `Ctrl+Shift+T` to create another side terminal. You can also click **+** in the side-terminal strip.

The shortcut follows terminal focus, not panel visibility. A visible side panel with the prompt focused still creates a main terminal tab. Press `Cmd+Shift+[` / `Ctrl+Shift+[` for the previous terminal or `Cmd+Shift+]` / `Ctrl+Shift+]` for the next terminal in the focused terminal strip. Drag tabs to reorder them. Pressing `Cmd+W` / `Ctrl+W` with a focused side terminal closes that terminal when other terminals remain. On the last side terminal, it hides the panel and keeps the shell alive; use its close button or type `exit` to stop it.

`Cmd+T` / `Ctrl+T` always creates a new agent session tab. It never creates a terminal.

New terminals are named "Terminal N" using the lowest free number, and tabs pick up the live title from the shell or running program, so a dev server or editor names its own tab.

Expand All @@ -256,7 +263,7 @@ New terminals are named "Terminal N" using the lowest free number, and tabs pick
A common workflow is letting the agent work, then switching to the terminal to run tests or inspect the worktree, then switching back to control the agent:

1. **Agent Manager → Terminal:** Press `Cmd+/` (macOS) / `Ctrl+/` (Windows/Linux) to open and focus the terminal for the current session. The terminal runs inside the session's worktree, so commands like `npm test` or `git status` operate on the agent's isolated branch.
2. **Terminal → Agent Manager:** Press `Cmd+Shift+M` (macOS) / `Ctrl+Shift+M` (Windows/Linux) to bring focus back to the Agent Manager panel and its prompt input. This works from anywhere in VS Code — the terminal, another editor tab, or the sidebar.
2. **Terminal → Agent Manager:** Press `Cmd+Shift+M` (macOS) / `Ctrl+Shift+M` (Windows/Linux) to bring focus back to the Agent Manager panel and its prompt input. This explicit shortcut always targets the prompt and works from anywhere in VS Code — the terminal, another editor tab, or the sidebar. Returning to the panel by clicking its editor tab or switching windows restores the last focused control instead.

## Setup Scripts

Expand Down Expand Up @@ -392,11 +399,13 @@ Closing a managed worktree removes it from Agent Manager, deletes its `.kilo/wor
| `Cmd+Shift+N` | `Ctrl+Shift+N` | Create a new worktree immediately |
| `Cmd+Shift+O` | `Ctrl+Shift+O` | Import/open worktree |
| `Cmd+Shift+W` | `Ctrl+Shift+W` | Close current worktree |
| `Cmd+T` | `Ctrl+T` | New tab (session) in worktree |
| `Cmd+W` | `Ctrl+W` | Close current tab |
| `Cmd+T` | `Ctrl+T` | New agent session tab in worktree |
| `Cmd+W` | `Ctrl+W` | Close the focused tab or terminal; the last side terminal hides instead of stopping |
| `Cmd+Alt+Up` / `Down` | `Ctrl+Alt+Up` / `Down` | Previous / next worktree |
| `Cmd+Alt+Left` / `Right` | `Ctrl+Alt+Left` / `Right` | Previous / next tab in worktree |
| `Cmd+/` | `Ctrl+/` | Focus terminal for current session |
| `Cmd+/` | `Ctrl+/` | Focus terminal, or hide it when it already has focus |
| `Cmd+Shift+T` | `Ctrl+Shift+T` | New side terminal when a side terminal is focused; otherwise new main terminal tab |
| `Cmd+Shift+[` / `]` | `Ctrl+Shift+[` / `]` | Previous / next terminal |
| `Cmd+D` | `Ctrl+D` | Toggle diff panel |
| `Cmd+E` | `Ctrl+E` | Run / stop run script |
| `Cmd+Shift+/` | `Ctrl+Shift+/` | Show keyboard shortcuts |
Expand Down
27 changes: 27 additions & 0 deletions packages/kilo-vscode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,16 @@
"title": "Agent Manager: Next Tab",
"category": "Kilo Code"
},
{
"command": "kilo-code.new.agentManager.previousTerminal",
"title": "Agent Manager: Previous Terminal",
"category": "Kilo Code"
},
{
"command": "kilo-code.new.agentManager.nextTerminal",
"title": "Agent Manager: Next Terminal",
"category": "Kilo Code"
},
{
"command": "kilo-code.new.agentManager.search",
"title": "Agent Manager: Search Worktrees and Sessions",
Expand Down Expand Up @@ -633,6 +643,11 @@
"key": "ctrl+shift+g",
"mac": "cmd+shift+g"
},
{
"command": "-workbench.actions.view.problems",
"key": "ctrl+shift+m",
"mac": "cmd+shift+m"
},
{
"command": "kilo-code.new.agentManagerOpen",
"key": "ctrl+shift+m",
Expand Down Expand Up @@ -668,6 +683,18 @@
"mac": "cmd+alt+right",
"when": "activeWebviewPanelId == 'kilo-code.new.AgentManagerPanel'"
},
{
"command": "kilo-code.new.agentManager.previousTerminal",
"key": "ctrl+shift+[",
"mac": "cmd+shift+[",
"when": "activeWebviewPanelId == 'kilo-code.new.AgentManagerPanel'"
},
{
"command": "kilo-code.new.agentManager.nextTerminal",
"key": "ctrl+shift+]",
"mac": "cmd+shift+]",
"when": "activeWebviewPanelId == 'kilo-code.new.AgentManagerPanel'"
},
{
"command": "kilo-code.new.agentManager.search",
"key": "ctrl+f",
Expand Down
29 changes: 15 additions & 14 deletions packages/kilo-vscode/src/agent-manager/AgentManagerProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ import { createProjectWiring } from "./project/wiring"
import { ProjectScope } from "./project/scope"
import type { AgentManagerOutMessage, AgentManagerInMessage } from "./types"
import type { Host, PanelContext, OutputHandle, Disposable } from "./host"

import { focusPanelPrompt } from "./focus-panel"
export class AgentManagerProvider implements Disposable {
public static readonly viewType = "kilo-code.new.AgentManagerPanel"
private panel: PanelContext | undefined
Expand Down Expand Up @@ -356,22 +356,22 @@ export class AgentManagerProvider implements Disposable {
if (this.panel) {
this.log("Panel already open, revealing")
this.panel.reveal(preserveFocus)
if (!preserveFocus) this.postToWebview({ type: "action", action: "focusInput" })
if (!preserveFocus)
focusPanelPrompt(this.panel, this.waitForPanelReady(this.panel), this.waitForPanelActive(this.panel))
return
}
this.log("Opening Agent Manager panel")
this.host.capture("Agent Manager Opened", { source: PLATFORM })

this.attachPanel(
this.host.openPanel({
onBeforeMessage: (msg) => this.onMessage(msg),
worktreeDirectories: () => this.getWorktreeDirectories(),
workspaceRoot: () => this.getRoot(),
projectId: () => this.contexts.active()?.id,
}),
)
const panel = this.host.openPanel({
onBeforeMessage: (msg) => this.onMessage(msg),
worktreeDirectories: () => this.getWorktreeDirectories(),
workspaceRoot: () => this.getRoot(),
projectId: () => this.contexts.active()?.id,
})
this.attachPanel(panel)
if (!preserveFocus) focusPanelPrompt(panel, this.waitForPanelReady(panel), this.waitForPanelActive(panel))
}

public onPanelVisibilityChange(cb: (visible: boolean) => void): void {
this.onVisibilityChange = cb
}
Expand Down Expand Up @@ -1692,9 +1692,10 @@ export class AgentManagerProvider implements Disposable {
* Used for the keyboard shortcut to switch back from terminal.
*/
public focusPanel(): void {
if (!this.panel) return
this.panel.reveal(false)
this.postToWebview({ type: "action", action: "focusInput" })
const panel = this.panel
if (!panel) return
panel.reveal(false)
focusPanelPrompt(panel, this.waitForPanelReady(panel), this.waitForPanelActive(panel))
}

public isActive(): boolean {
Expand Down
8 changes: 8 additions & 0 deletions packages/kilo-vscode/src/agent-manager/focus-panel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import type { PanelContext } from "./host"

export function focusPanelPrompt(panel: PanelContext, ready: Promise<boolean>, active: Promise<boolean>): void {
void Promise.all([ready, active]).then(([ready, active]) => {
if (!ready || !active) return
panel.postMessage({ type: "action", action: "focusInput" })
})
}
3 changes: 3 additions & 0 deletions packages/kilo-vscode/src/agent-manager/format-keybinding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,9 @@ export function buildKeybindingMap(
if (!bindings.runScript) bindings.runScript = formatKeybinding(mac ? "cmd+e" : "ctrl+e", mac)
if (!bindings.toggleDiff) bindings.toggleDiff = formatKeybinding(mac ? "cmd+d" : "ctrl+d", mac)
if (!bindings.showShortcuts) bindings.showShortcuts = formatKeybinding(mac ? "cmd+shift+/" : "ctrl+shift+/", mac)
if (!bindings.previousTerminal)
bindings.previousTerminal = formatKeybinding(mac ? "cmd+shift+[" : "ctrl+shift+[", mac)
if (!bindings.nextTerminal) bindings.nextTerminal = formatKeybinding(mac ? "cmd+shift+]" : "ctrl+shift+]", mac)

return bindings
}
13 changes: 12 additions & 1 deletion packages/kilo-vscode/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,12 @@ export function activate(context: vscode.ExtensionContext) {
// The terminal intercepts all keystrokes unless the command is listed in
// terminal.integrated.commandsToSkipShell, which only contains built-in
// commands by default.
const skip = ["kilo-code.new.agentManagerOpen", "kilo-code.new.agentManager.showTerminal"]
const skip = [
"kilo-code.new.agentManagerOpen",
"kilo-code.new.agentManager.showTerminal",
"kilo-code.new.agentManager.previousTerminal",
"kilo-code.new.agentManager.nextTerminal",
]
if (process.platform === "darwin") skip.push("kilo-code.new.agentManager.runScript")
ensureCommandsSkipShell(skip)

Expand Down Expand Up @@ -476,6 +481,12 @@ export function activate(context: vscode.ExtensionContext) {
vscode.commands.registerCommand("kilo-code.new.agentManager.nextTab", () => {
agentManagerProvider.postMessage({ type: "action", action: "tabNext" })
}),
vscode.commands.registerCommand("kilo-code.new.agentManager.previousTerminal", () => {
agentManagerProvider.postMessage({ type: "action", action: "terminalPrevious" })
}),
vscode.commands.registerCommand("kilo-code.new.agentManager.nextTerminal", () => {
agentManagerProvider.postMessage({ type: "action", action: "terminalNext" })
}),
vscode.commands.registerCommand("kilo-code.new.agentManager.search", () => {
agentManagerProvider.postMessage({ type: "action", action: "search" })
}),
Expand Down
22 changes: 22 additions & 0 deletions packages/kilo-vscode/tests/unit/agent-manager-arch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,28 @@ describe("Agent Manager Worktree Actions", () => {
expect(source).toContain('quickWorktree: isMac ? "⌘⇧N" : "Ctrl+Shift+N"')
})

it("reserves Cmd+Shift+M for the Agent Manager instead of Problems", () => {
const manifest = JSON.parse(fs.readFileSync(path.join(ROOT, "package.json"), "utf-8")) as {
contributes: { keybindings: { command: string; key?: string; mac?: string }[] }
}
const removed = manifest.contributes.keybindings.find((item) => item.command === "-workbench.actions.view.problems")
const manager = manifest.contributes.keybindings.find((item) => item.command === "kilo-code.new.agentManagerOpen")

expect(removed).toMatchObject({ key: "ctrl+shift+m", mac: "cmd+shift+m" })
expect(manager).toMatchObject({ key: "ctrl+shift+m", mac: "cmd+shift+m" })
})

it("creates side terminals only while a side terminal owns focus", () => {
const source = fs.readFileSync(TSX_FILE, "utf-8")
const start = source.indexOf('else if (msg.action === "newTerminal")')
const end = source.indexOf('else if (msg.action === "cycleAgentMode"', start)
const action = source.slice(start, end)

expect(action).toContain("if (terms.sideFocusedId()) termHandlers.addSide()")
expect(action).not.toContain("terminalVisible()")
expect(action).toContain("else termHandlers.requestNew()")
})

it("forwards the quick-worktree command to immediate creation", () => {
const source = fs.readFileSync(path.join(ROOT, "src/extension.ts"), "utf-8")
const start = source.indexOf('vscode.commands.registerCommand("kilo-code.new.agentManager.quickWorktree"')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ function scene(
saved?: "vscode" | "agentManager"
visible?: boolean
focusedId?: string
count?: number
script?: boolean
mac?: boolean
} = {},
) {
Expand All @@ -33,6 +35,7 @@ function scene(
requestSide: () => {
calls.requestSide++
visible = true
focusedId ??= "terminal:side"
},
ensureSide: () => calls.ensureSide++,
closeSide: (terminalId) => {
Expand All @@ -43,6 +46,8 @@ function scene(
},
visible: () => visible,
focusedId: () => focusedId,
count: () => opts.count ?? 2,
isScript: () => opts.script ?? false,
hide: () => {
calls.hide++
visible = false
Expand All @@ -60,15 +65,16 @@ function scene(
}

describe("Agent Manager side terminal controller", () => {
it("toggles the panel and hands focus to the chat only when the terminal had it", () => {
it("toggles the panel, focusing a visible terminal before hiding it", () => {
const focused = scene({ destination: "agentManager", visible: true, focusedId: "terminal:side" })
focused.ctl.toggle()
expect(focused.calls.hide).toBe(1)
expect(focused.calls.refocus).toBe(1)

const elsewhere = scene({ destination: "agentManager", visible: true })
elsewhere.ctl.toggle()
expect(elsewhere.calls.hide).toBe(1)
expect(elsewhere.calls.requestSide).toBe(1)
expect(elsewhere.calls.hide).toBe(0)
expect(elsewhere.calls.refocus).toBe(0)

const hidden = scene({ destination: "agentManager", visible: false })
Expand All @@ -93,18 +99,32 @@ describe("Agent Manager side terminal controller", () => {
hidden.ctl.syncContext("wt-2", "wt-1")
expect(hidden.calls.ensureSide).toBe(0)

const closed = scene({ visible: true })
const closed = scene({ visible: true, focusedId: "terminal:side" })
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", () => {
it("closes the focused terminal without stealing focus from its survivor", () => {
const focused = scene({ focusedId: "terminal:two" })
expect(focused.ctl.close()).toBe(true)
expect(focused.calls.closed).toEqual(["terminal:two"])
expect(focused.calls.refocus).toBe(1)
expect(focused.calls.refocus).toBe(0)
})

it("hides instead of killing the last or provider-owned terminal", () => {
const last = scene({ focusedId: "terminal:last", count: 1 })
expect(last.ctl.close()).toBe(true)
expect(last.calls.closed).toEqual([])
expect(last.calls.hide).toBe(1)
expect(last.calls.refocus).toBe(1)

const script = scene({ focusedId: "script:run", script: true, count: 2 })
expect(script.ctl.close()).toBe(true)
expect(script.calls.closed).toEqual([])
expect(script.calls.hide).toBe(1)
expect(script.calls.refocus).toBe(1)
})

it("does nothing on close without a focused terminal", () => {
Expand Down
Loading
Loading