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

Make Agent Manager shortcuts follow the focused area. In the center, Cmd/Ctrl+T creates a new session tab and Cmd/Ctrl+Shift+T creates a central terminal tab, whether the prompt or a central terminal is focused. In the right sidebar terminal, Cmd/Ctrl+T creates another sidebar terminal tab and Cmd/Ctrl+Shift+T does nothing. Returning from a terminal with Cmd/Ctrl+Shift+M restores the previous session tab before focusing its prompt. Update the shortcut dialog and hints to show the distinct actions.
3 changes: 3 additions & 0 deletions packages/kilo-vscode/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

### Minor Changes


- [#11219](https://github.com/Kilo-Org/kilocode/pull/11219) [`8013e5f`](https://github.com/Kilo-Org/kilocode/commit/8013e5f50451225bb32b1284579322c137f497e3) Thanks [@sylwester-liljegren](https://github.com/sylwester-liljegren)! - Make file references in agent responses clickable by validating inline code spans against the filesystem. Code spans that match real files in the workspace become clickable links that open the file at the referenced line. Non-existent paths stay as plain code. Also adds fallback workspace search and "File not found" warning when clicking dead links.

- [#13071](https://github.com/Kilo-Org/kilocode/pull/13071) [`3e3dd3d`](https://github.com/Kilo-Org/kilocode/commit/3e3dd3dcb2a711192ec2477ef7c0532b11f67886) Thanks [@cosi-conda](https://github.com/cosi-conda)! - Add Agent Manager PR comment actions: resolve/unresolve review threads, jump to comments section, and scroll-to-top for PR diff view.
Expand All @@ -12,6 +13,8 @@

### Patch Changes

- Make Agent Manager shortcuts follow the focused area. In the center, `Cmd+T` / `Ctrl+T` creates a new session tab and `Cmd+Shift+T` / `Ctrl+Shift+T` creates a central terminal tab, whether the prompt or a central terminal is focused. In the right sidebar terminal, `Cmd+T` / `Ctrl+T` creates another sidebar terminal tab and `Cmd+Shift+T` / `Ctrl+Shift+T` does nothing. Returning from a terminal with `Cmd+Shift+M` / `Ctrl+Shift+M` restores the previous session tab before focusing its prompt.

- [#13063](https://github.com/Kilo-Org/kilocode/pull/13063) [`ca9a99f`](https://github.com/Kilo-Org/kilocode/commit/ca9a99ffd8f118b5445e0fc2c890c4c6dd797d66) - Show and select the model's default reasoning variant in chat and Agent Manager.

- [#13100](https://github.com/Kilo-Org/kilocode/pull/13100) [`753d560`](https://github.com/Kilo-Org/kilocode/commit/753d5609859f2b646c404392e71ca048714f61dd) - Support structured AWS access keys and Google Cloud service-account JSON when connecting Bedrock and Vertex AI in VS Code.
Expand Down
21 changes: 16 additions & 5 deletions packages/kilo-vscode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -296,8 +296,13 @@
"category": "Kilo Code"
},
{
"command": "kilo-code.new.agentManager.newTerminal",
"title": "Agent Manager: New Terminal Tab (Experimental)",
"command": "kilo-code.new.agentManager.newTerminalTab",
"title": "Agent Manager: New Terminal Tab",
"category": "Kilo Code"
},
{
"command": "kilo-code.new.agentManager.newSideTerminal",
"title": "Agent Manager: New Sidebar Terminal Tab",
"category": "Kilo Code"
},
{
Expand Down Expand Up @@ -735,13 +740,19 @@
"command": "kilo-code.new.agentManager.newTab",
"key": "ctrl+t",
"mac": "cmd+t",
"when": "activeWebviewPanelId == 'kilo-code.new.AgentManagerPanel'"
"when": "activeWebviewPanelId == 'kilo-code.new.AgentManagerPanel' && !kilo-code.new.agentManagerSideTerminalFocused"
},
{
"command": "kilo-code.new.agentManager.newTerminal",
"command": "kilo-code.new.agentManager.newTerminalTab",
"key": "ctrl+shift+t",
"mac": "cmd+shift+t",
"when": "activeWebviewPanelId == 'kilo-code.new.AgentManagerPanel'"
"when": "activeWebviewPanelId == 'kilo-code.new.AgentManagerPanel' && !kilo-code.new.agentManagerSideTerminalFocused"
},
{
"command": "kilo-code.new.agentManager.newSideTerminal",
"key": "ctrl+t",
"mac": "cmd+t",
"when": "activeWebviewPanelId == 'kilo-code.new.AgentManagerPanel' && kilo-code.new.agentManagerSideTerminalFocused"
},
{
"command": "kilo-code.new.agentManager.closeTab",
Expand Down
29 changes: 26 additions & 3 deletions packages/kilo-vscode/src/KiloProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -987,6 +987,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper

private setupWebviewMessageHandler(webview: vscode.Webview): void {
this.webviewMessageDisposable?.dispose()
this.setFocusTarget("other")
this.autocompleteConfigDisposable?.dispose()
this.autocompleteConfigDisposable = watchAutocompleteConfig((msg) => this.postMessage(msg))
this.indexingConfigDisposable?.dispose()
Expand Down Expand Up @@ -1530,11 +1531,32 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
this.webviewMessageDisposable = watchWorkStyleConfig((msg) => this.postMessage(msg), this.webviewMessageDisposable)
}

private handleWebviewFocusMessage(message: TypedWebviewMessage & { focused?: unknown }): void {
if (message.type !== "webviewFocusChanged") return
if (this.opts.focusContext) {
private handleWebviewFocusMessage(message: TypedWebviewMessage & { focused?: unknown; target?: unknown }): void {
if (message.type === "webviewFocusChanged" && this.opts.focusContext) {
void vscode.commands.executeCommand("setContext", this.opts.focusContext, message.focused === true)
}
if (message.type === "webviewFocusChanged" && message.focused === true) {
if (this.opts.focusTargetContext) this.postMessage({ type: "agentManager.focusContextRequested" })
return
}
if (message.type === "webviewFocusChanged" && message.focused !== true) {
Comment thread
marius-kilocode marked this conversation as resolved.
this.setFocusTarget("other")
return
}
if (message.type !== "agentManagerFocusChanged") return
const target =
message.target === "prompt" || message.target === "mainTerminal" || message.target === "sideTerminal"
? message.target
: "other"
this.setFocusTarget(target)
}

private setFocusTarget(target: "prompt" | "mainTerminal" | "sideTerminal" | "other"): void {
const contexts = this.opts.focusTargetContext
if (!contexts) return
void vscode.commands.executeCommand("setContext", contexts.prompt, target === "prompt")
void vscode.commands.executeCommand("setContext", contexts.mainTerminal, target === "mainTerminal")
void vscode.commands.executeCommand("setContext", contexts.sideTerminal, target === "sideTerminal")
}

private handleChildSyncMessage(
Expand Down Expand Up @@ -5065,6 +5087,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
if (this.opts.focusContext) {
void vscode.commands.executeCommand("setContext", this.opts.focusContext, false)
}
this.setFocusTarget("other")
this.unsubscribeRemote?.()
this.streams.focus(undefined)
this.connectionService.unregisterVisible(this.instanceId)
Expand Down
44 changes: 34 additions & 10 deletions packages/kilo-vscode/src/agent-manager/format-keybinding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,26 +43,47 @@ const GLOBAL_KEYBINDINGS: Record<string, string> = {
"kilo-code.new.cyclePreviousAgentMode": "cyclePreviousAgentMode",
}

function addBinding(bindings: Record<string, string>, name: string, value: string, when?: string): void {
if (name === "newTerminalTab" && when?.includes("!kilo-code.new.agentManagerSideTerminalFocused")) {
bindings.newTerminalCenter = value
return
}
if (name === "newSideTerminal" && when?.includes("agentManagerSideTerminalFocused")) {
bindings.newTerminalTerminal = value
return
}
if (name === "newTerminal" || name === "newTerminalTab" || name === "newSideTerminal") return
bindings[name] = value
}

function addRawBinding(
bindings: Record<string, string>,
kb: { command: string; key?: string; mac?: string; when?: string },
mac: boolean,
): void {
const raw = mac ? (kb.mac ?? kb.key) : kb.key
if (!raw) return
const value = formatKeybinding(raw, mac)
if (kb.command.startsWith(AM_PREFIX)) {
addBinding(bindings, kb.command.slice(AM_PREFIX.length), value, kb.when)
return
}
const name = GLOBAL_KEYBINDINGS[kb.command]
if (name) bindings[name] = value
}

/**
* Build a keybinding map from VS Code's raw `contributes.keybindings` array.
* Returns a record of action name → formatted shortcut string.
*/
export function buildKeybindingMap(
keybindings: Array<{ command: string; key?: string; mac?: string }>,
keybindings: Array<{ command: string; key?: string; mac?: string; when?: string }>,
mac: boolean,
): Record<string, string> {
const bindings: Record<string, string> = {}

for (const kb of keybindings) {
const raw = mac ? (kb.mac ?? kb.key) : kb.key
if (!raw) continue

if (kb.command.startsWith(AM_PREFIX)) {
bindings[kb.command.slice(AM_PREFIX.length)] = formatKeybinding(raw, mac)
continue
}
const name = GLOBAL_KEYBINDINGS[kb.command]
if (name) bindings[name] = formatKeybinding(raw, mac)
addRawBinding(bindings, kb, mac)
}

// Ensure fallback bindings are always present (may be missing from
Expand All @@ -74,6 +95,9 @@ export function buildKeybindingMap(
if (!bindings.previousTerminal)
bindings.previousTerminal = formatKeybinding(mac ? "cmd+shift+[" : "ctrl+shift+[", mac)
if (!bindings.nextTerminal) bindings.nextTerminal = formatKeybinding(mac ? "cmd+shift+]" : "ctrl+shift+]", mac)
if (!bindings.newTerminalCenter)
bindings.newTerminalCenter = formatKeybinding(mac ? "cmd+shift+t" : "ctrl+shift+t", mac)
if (!bindings.newTerminalTerminal) bindings.newTerminalTerminal = formatKeybinding(mac ? "cmd+t" : "ctrl+t", mac)

return bindings
}
2 changes: 1 addition & 1 deletion packages/kilo-vscode/src/agent-manager/host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ export interface Host {
createOutput(name: string): OutputHandle

/** Read extension keybinding metadata. */
extensionKeybindings(): Array<{ command: string; key?: string; mac?: string }>
extensionKeybindings(): Array<{ command: string; key?: string; mac?: string; when?: string }>

/** Copy text to the system clipboard. */
copyToClipboard(text: string): void
Expand Down
7 changes: 6 additions & 1 deletion packages/kilo-vscode/src/agent-manager/vscode-host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,11 @@ export class VscodeHost implements Host {
worktreeDirectories: () => opts.worktreeDirectories?.() ?? [],
rootDirectory: opts.workspaceRoot,
disableViewedRegistration: true,
focusTargetContext: {
prompt: "kilo-code.new.agentManagerPromptFocused",
mainTerminal: "kilo-code.new.agentManagerMainTerminalFocused",
sideTerminal: "kilo-code.new.agentManagerSideTerminalFocused",
},
routeService: this.routes,
projectQualifier: () => {
const projectId = opts.projectId?.()
Expand Down Expand Up @@ -298,7 +303,7 @@ export class VscodeHost implements Host {
}
}

extensionKeybindings(): Array<{ command: string; key?: string; mac?: string }> {
extensionKeybindings(): Array<{ command: string; key?: string; mac?: string; when?: string }> {
const ext = vscode.extensions.getExtension("kilocode.kilo-code")
return ext?.packageJSON?.contributes?.keybindings ?? []
}
Expand Down
7 changes: 5 additions & 2 deletions packages/kilo-vscode/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -515,8 +515,11 @@ export function activate(context: vscode.ExtensionContext) {
vscode.commands.registerCommand("kilo-code.new.agentManager.newTab", () => {
agentManagerProvider.postMessage({ type: "action", action: "newTab" })
}),
vscode.commands.registerCommand("kilo-code.new.agentManager.newTerminal", () => {
agentManagerProvider.postMessage({ type: "action", action: "newTerminal" })
vscode.commands.registerCommand("kilo-code.new.agentManager.newTerminalTab", () => {
agentManagerProvider.postMessage({ type: "action", action: "newTerminalTab" })
}),
vscode.commands.registerCommand("kilo-code.new.agentManager.newSideTerminal", () => {
agentManagerProvider.postMessage({ type: "action", action: "newSideTerminal" })
}),
vscode.commands.registerCommand("kilo-code.new.agentManager.closeTab", () => {
agentManagerProvider.postMessage({ type: "action", action: "closeTab" })
Expand Down
6 changes: 6 additions & 0 deletions packages/kilo-vscode/src/kilo-provider/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@ import type { ProjectRouteService } from "../agent-manager/project/route"
export type KiloProviderOptions = {
/** Context key updated from focus events reported by this provider's webview. */
focusContext?: string
/** Context keys updated by Agent Manager prompt and terminal focus events. */
focusTargetContext?: {
prompt: string
mainTerminal: string
sideTerminal: string
}
projectDirectory?: string | null
platform?: string
snapshotInitialization?: "wait"
Expand Down
12 changes: 7 additions & 5 deletions packages/kilo-vscode/tests/unit/agent-manager-arch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -397,15 +397,17 @@ describe("Agent Manager Worktree Actions", () => {
expect(manager).toMatchObject({ key: "ctrl+shift+m", mac: "cmd+shift+m" })
})

it("creates side terminals only while a side terminal owns focus", () => {
it("routes prompt and side-terminal shortcut actions separately", () => {
const source = fs.readFileSync(TSX_FILE, "utf-8")
const start = source.indexOf('else if (msg.action === "newTerminal")')
const start = source.indexOf('else if (msg.action === "newTerminalTab")')
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()")
expect(action).toContain('msg.action === "newTerminalTab"')
expect(action).toContain("termHandlers.requestNew()")
expect(action).toContain('msg.action === "newSideTerminal"')
expect(action).toContain("termHandlers.addSide()")
expect(action).not.toContain('msg.action === "newMainTerminal"')
})

it("forwards the quick-worktree command to immediate creation", () => {
Expand Down
30 changes: 29 additions & 1 deletion packages/kilo-vscode/tests/unit/agent-manager-focus.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { describe, expect, it } from "bun:test"
import { Window } from "happy-dom"
import { focusQuestionOption, hasQuestionOption, preservesTextFocus } from "../../webview-ui/agent-manager/focus"
import {
agentManagerFocusTarget,
focusQuestionOption,
hasQuestionOption,
preservesTextFocus,
} from "../../webview-ui/agent-manager/focus"
import { isTextControl } from "../../webview-ui/src/utils/focus"

describe("Agent Manager focus", () => {
Expand Down Expand Up @@ -71,4 +76,27 @@ describe("Agent Manager focus", () => {
expect(isTextControl(editor)).toBe(true)
expect(isTextControl(button)).toBe(false)
})

it("resolves prompt and terminal focus from the active DOM owner", () => {
const window = new Window()
const prompt = window.document.createElement("textarea")
const main = window.document.createElement("div")
const side = window.document.createElement("div")
const mainHost = window.document.createElement("div")
const sideHost = window.document.createElement("div")
prompt.className = "prompt-input"
main.className = "am-terminal-layer"
side.className = "am-side-terminal-layer"
mainHost.className = "am-terminal-host"
sideHost.className = "am-terminal-host"
main.append(mainHost)
side.append(sideHost)

expect(agentManagerFocusTarget(prompt)).toBe("prompt")
expect(agentManagerFocusTarget(mainHost)).toBe("mainTerminal")
expect(agentManagerFocusTarget(sideHost)).toBe("sideTerminal")
expect(agentManagerFocusTarget(window.document.body)).toBe("other")
expect(agentManagerFocusTarget(mainHost, true)).toBe("prompt")
expect(agentManagerFocusTarget(prompt, true)).toBe("prompt")
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { describe, expect, it } from "bun:test"
import { restoreSessionAfterTerminal } from "../../webview-ui/agent-manager/selection-actions"

describe("Agent Manager session restoration", () => {
it("restores the remembered session after a central terminal", () => {
const selected: Array<[string, boolean]> = []
const created: string[] = []

expect(
restoreSessionAfterTerminal({
terminal: "terminal:one",
remembered: "session:two",
sessions: [{ id: "session:one" }, { id: "session:two" }],
isPending: () => false,
select: (id, pending) => selected.push([id, pending]),
create: () => {
created.push("created")
return "pending"
},
}),
).toBe("ready")
expect(selected).toEqual([["session:two", false]])
expect(created).toEqual([])
})

it("falls back to the first session when the remembered tab is gone", () => {
const selected: Array<[string, boolean]> = []

expect(
restoreSessionAfterTerminal({
terminal: "terminal:one",
remembered: "session:gone",
sessions: [{ id: "pending:one" }, { id: "session:two" }],
isPending: (id) => id.startsWith("pending:"),
select: (id, pending) => selected.push([id, pending]),
create: () => "pending",
}),
).toBe("ready")

expect(selected).toEqual([["pending:one", true]])
})

it("creates a real session tab only when no session can be restored", () => {
const created: string[] = []

expect(
restoreSessionAfterTerminal({
terminal: "terminal:one",
remembered: undefined,
sessions: [],
isPending: () => false,
select: () => undefined,
create: () => {
created.push("created")
return "pending"
},
}),
).toBe("pending")
expect(created).toEqual(["created"])
})

it("does nothing when no central terminal is selected", () => {
const selected: string[] = []

expect(
restoreSessionAfterTerminal({
terminal: undefined,
remembered: "session:one",
sessions: [{ id: "session:one" }],
isPending: () => false,
select: (id) => selected.push(id),
create: () => selected.push("created"),
}),
).toBe("none")
expect(selected).toEqual([])
})
})
Loading
Loading