mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-08-31 01:37:28 +08:00
Merge pull request #13229 from Kilo-Org/refactor-terminal-shortcut-logic
fix(agent-manager): make terminal shortcuts focus-aware
This commit is contained in:
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
{
|
||||
@@ -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",
|
||||
|
||||
@@ -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()
|
||||
@@ -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) {
|
||||
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(
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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?.()
|
||||
@@ -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 ?? []
|
||||
}
|
||||
|
||||
@@ -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" })
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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", () => {
|
||||
@@ -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")
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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([])
|
||||
})
|
||||
})
|
||||
@@ -117,12 +117,15 @@ describe("Extension — package.json command sync", () => {
|
||||
})
|
||||
})
|
||||
|
||||
it("keeps Agent Manager terminal shortcuts distinct", () => {
|
||||
it("keeps Agent Manager session and terminal shortcuts focus-aware", () => {
|
||||
const terminal = pkg.contributes?.keybindings?.find(
|
||||
(item: { command: string }) => item.command === "kilo-code.new.agentManager.showTerminal",
|
||||
)
|
||||
const create = pkg.contributes?.keybindings?.find(
|
||||
(item: { command: string }) => item.command === "kilo-code.new.agentManager.newTerminal",
|
||||
(item: { command: string }) => item.command === "kilo-code.new.agentManager.newTerminalTab",
|
||||
)
|
||||
const sessionCreate = pkg.contributes?.keybindings?.find(
|
||||
(item: { command: string }) => item.command === "kilo-code.new.agentManager.newTab",
|
||||
)
|
||||
expect(terminal).toMatchObject({
|
||||
key: "ctrl+/",
|
||||
@@ -132,8 +135,29 @@ describe("Extension — package.json command sync", () => {
|
||||
expect(create).toMatchObject({
|
||||
key: "ctrl+shift+t",
|
||||
mac: "cmd+shift+t",
|
||||
when: "activeWebviewPanelId == 'kilo-code.new.AgentManagerPanel'",
|
||||
when: "activeWebviewPanelId == 'kilo-code.new.AgentManagerPanel' && !kilo-code.new.agentManagerSideTerminalFocused",
|
||||
})
|
||||
expect(sessionCreate).toMatchObject({
|
||||
key: "ctrl+t",
|
||||
mac: "cmd+t",
|
||||
when: "activeWebviewPanelId == 'kilo-code.new.AgentManagerPanel' && !kilo-code.new.agentManagerSideTerminalFocused",
|
||||
})
|
||||
const terminalCreate = pkg.contributes?.keybindings?.find(
|
||||
(item: { command: string; key?: string; mac?: string; when?: string }) =>
|
||||
item.command === "kilo-code.new.agentManager.newSideTerminal" && item.key === "ctrl+t",
|
||||
)
|
||||
expect(terminalCreate).toMatchObject({
|
||||
key: "ctrl+t",
|
||||
mac: "cmd+t",
|
||||
when: "activeWebviewPanelId == 'kilo-code.new.AgentManagerPanel' && kilo-code.new.agentManagerSideTerminalFocused",
|
||||
})
|
||||
expect(
|
||||
pkg.contributes?.keybindings?.some(
|
||||
(item: { command: string }) =>
|
||||
item.command === "kilo-code.new.agentManager.newTerminal" ||
|
||||
item.command === "kilo-code.new.agentManager.newMainTerminal",
|
||||
),
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
it("declares the Agent Manager terminal destination setting", () => {
|
||||
|
||||
@@ -89,4 +89,29 @@ describe("buildKeybindingMap", () => {
|
||||
expect(buildKeybindingMap([], false).previousTerminal).toBe("Ctrl+Shift+[")
|
||||
expect(buildKeybindingMap([], false).nextTerminal).toBe("Ctrl+Shift+]")
|
||||
})
|
||||
|
||||
it("keeps prompt and side-terminal shortcuts separate", () => {
|
||||
const bindings = [
|
||||
{
|
||||
command: "kilo-code.new.agentManager.newTerminalTab",
|
||||
key: "ctrl+shift+t",
|
||||
mac: "cmd+shift+t",
|
||||
when: "activeWebviewPanelId == 'kilo-code.new.AgentManagerPanel' && kilo-code.new.agentManagerPromptFocused",
|
||||
},
|
||||
{
|
||||
command: "kilo-code.new.agentManager.newSideTerminal",
|
||||
key: "ctrl+t",
|
||||
mac: "cmd+t",
|
||||
when: "activeWebviewPanelId == 'kilo-code.new.AgentManagerPanel' && kilo-code.new.agentManagerSideTerminalFocused",
|
||||
},
|
||||
]
|
||||
expect(buildKeybindingMap(bindings, true)).toMatchObject({
|
||||
newTerminalCenter: "⌘⇧T",
|
||||
newTerminalTerminal: "⌘T",
|
||||
})
|
||||
expect(buildKeybindingMap(bindings, false)).toMatchObject({
|
||||
newTerminalCenter: "Ctrl+Shift+T",
|
||||
newTerminalTerminal: "Ctrl+T",
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -92,7 +92,7 @@ import { rememberTarget, restoreProjectTarget } from "./project/restore"
|
||||
import { createProjectStateRouter } from "./project/state"
|
||||
import { applyRunStatus } from "./project/run-status"
|
||||
import { clearMultiVersionBusy, markMultiVersionBusy } from "./project/progress"
|
||||
import { selectLocalAction, selectWorktreeAction } from "./selection-actions"
|
||||
import { createSessionRestore, selectLocalAction, selectWorktreeAction } from "./selection-actions"
|
||||
import { DataBridge } from "../src/App"
|
||||
import { LanguageBridge } from "../src/context/language-bridge"
|
||||
import { useLanguage } from "../src/context/language"
|
||||
@@ -141,7 +141,7 @@ import {
|
||||
resolveRunScriptRequest,
|
||||
resolveVscodeTerminalRequest,
|
||||
} from "./terminal"
|
||||
import { readTerminalOutput } from "./terminal/output"
|
||||
import { createEmbeddedTerminalReader } from "./terminal/output"
|
||||
import { focusCurrentTab, renderTab, renderTerminalLayer, renderNewTabButton } from "./tab-rendering"
|
||||
import { useTabScroll } from "./tab-scroll"
|
||||
import { DiffPanel } from "./DiffPanel"
|
||||
@@ -182,7 +182,7 @@ import { SubagentPanel } from "./SubagentPanel"
|
||||
import { createSubagentTabs } from "./subagent-tabs"
|
||||
import { buildShortcutCategories } from "./shortcuts"
|
||||
import { tracker } from "./telemetry"
|
||||
import { createChatFocus, createPromptFocus, hasQuestionOption } from "./focus"
|
||||
import { createChatFocus, createFocusBridge, createPromptFocus, forgetTerminalFocus, hasQuestionOption } from "./focus"
|
||||
import { usePendingCreate } from "./pending-create"
|
||||
import { defaultBase as projectDefaultBase } from "./project/default-base"
|
||||
import "./agent-manager.css"
|
||||
@@ -339,7 +339,6 @@ const AgentManagerContent: Component = () => {
|
||||
const worktreeStats = () => registry.active().worktreeStats()
|
||||
|
||||
const prStatuses = () => registry.active().prStatuses()
|
||||
|
||||
const runStatuses = () => registry.active().runStatuses()
|
||||
const setRunStatuses: Setter<Record<string, RunStatus>> = (v) => registry.active().setRunStatuses(v)
|
||||
const runScriptConfigured = () => registry.active().runScriptConfigured()
|
||||
@@ -347,7 +346,6 @@ const AgentManagerContent: Component = () => {
|
||||
|
||||
// Local repo git stats (branch name, diff additions/deletions, commits)
|
||||
const localStats = () => registry.active().localStats()
|
||||
|
||||
const projectLive = createProjectLive({
|
||||
ensure: (pid) => (pid ? registry.ensure(pid) : registry.active()),
|
||||
active: isActivePayload,
|
||||
@@ -362,37 +360,25 @@ const AgentManagerContent: Component = () => {
|
||||
fontSize: readFontSize(),
|
||||
})
|
||||
|
||||
/** Namespace key so worktree/local ids from different projects never collide. */
|
||||
const nsKey = (sel: string) => `${currentProjectId() ?? "single"}:${sel}`
|
||||
|
||||
// Per-sidebar-context terminal state. `terms.activeId` holds the id of the focused
|
||||
// terminal tab, if any — takes precedence over session/pending/review when deriving
|
||||
// the visible tab. Contexts are project-keyed: every project reuses LOCAL and ids like "0".
|
||||
const terms = createTerminalState(() => {
|
||||
const sel = selection()
|
||||
return sel === null ? null : nsKey(sel)
|
||||
})
|
||||
const resolveEmbeddedTerminal = async (context?: string) => {
|
||||
const key = nsKey(context ?? LOCAL)
|
||||
const side = terms.sidesForContext(key)
|
||||
const tabs = terms.forSelection(key)
|
||||
const focused = terms.focusedId()
|
||||
const focusedTerm = side.find((term) => term.id === focused) ?? tabs.find((term) => term.id === focused)
|
||||
const sideTerm = side.find((term) => term.id === terms.sideActiveFor(key))
|
||||
const tab = tabs.find((term) => term.id === terms.activeId())
|
||||
const id = focusedTerm?.id ?? sideTerm?.id
|
||||
const target = id ?? tab?.id
|
||||
if (!target) return undefined
|
||||
const term = side.find((item) => item.id === target) ?? tabs.find((item) => item.id === target)
|
||||
if (!term) return undefined
|
||||
return readTerminalOutput(term.id)
|
||||
}
|
||||
const resolveTerminal = createEmbeddedTerminalReader({
|
||||
key: (context) => nsKey(context ?? LOCAL),
|
||||
local: LOCAL,
|
||||
side: (key) => terms.sidesForContext(key),
|
||||
tabs: (key) => terms.forSelection(key),
|
||||
focused: terms.focusedId,
|
||||
sideActive: terms.sideActiveFor,
|
||||
active: terms.activeId,
|
||||
})
|
||||
const requestChatFocus = createChatFocus({
|
||||
term: () => terms.activeId(),
|
||||
history,
|
||||
review: reviewActive,
|
||||
})
|
||||
|
||||
createEffect(
|
||||
on(
|
||||
() => {
|
||||
@@ -411,25 +397,20 @@ const AgentManagerContent: Component = () => {
|
||||
type FocusOwner = "prompt" | { terminal: string }
|
||||
const focusMemory = new Map<string, FocusOwner>()
|
||||
const prompt = createPromptFocus(terms, requestChatFocus)
|
||||
const focusKey = () => {
|
||||
const context = terms.sideKey()
|
||||
const sessionID = session.currentSessionID() ?? activePendingId() ?? "new"
|
||||
return `${context}:${sessionID}`
|
||||
}
|
||||
const focusKey = () => `${terms.sideKey()}:${session.currentSessionID() ?? activePendingId() ?? "new"}`
|
||||
const forgetSessionFocus = (sessionID: string) => {
|
||||
for (const key of focusMemory.keys()) if (key.endsWith(`:${sessionID}`)) focusMemory.delete(key)
|
||||
}
|
||||
const forgetContextFocus = (context: string) => {
|
||||
for (const key of focusMemory.keys()) if (key.startsWith(`${context}:`)) focusMemory.delete(key)
|
||||
}
|
||||
const forgetTerminalFocus = (terminalID: string) => {
|
||||
for (const [key, owner] of focusMemory) {
|
||||
if (owner !== "prompt" && owner.terminal === terminalID) focusMemory.delete(key)
|
||||
}
|
||||
}
|
||||
const rememberPromptFocus = (focused: boolean) => {
|
||||
if (focused) focusMemory.set(focusKey(), "prompt")
|
||||
}
|
||||
let restoreSession: () => "none" | "ready" | "pending" = () => "none"
|
||||
const focusCtl = createFocusBridge({
|
||||
prompt,
|
||||
post: (target) => vscode.postMessage({ type: "agentManagerFocusChanged", target }),
|
||||
remember: () => focusMemory.set(focusKey(), "prompt"),
|
||||
restore: () => restoreSession(),
|
||||
})
|
||||
const terminalVisible = () => sidePanel() === SidePanel.Terminal && !history() && !reviewActive()
|
||||
const focusOnDraftChange = () => {
|
||||
const key = focusKey()
|
||||
@@ -1226,13 +1207,12 @@ const AgentManagerContent: Component = () => {
|
||||
else if (msg.action === "advancedWorktree") showNewWorktreeDialog()
|
||||
else if (msg.action === "closeWorktree") closeSelectedWorktree()
|
||||
else if (msg.action === "showShortcuts") handleShowKeyboardShortcuts()
|
||||
else if (msg.action === "focusInput") prompt.focus()
|
||||
else if (msg.action === "focusInput") focusCtl.focus()
|
||||
else if (msg.action === "focusSearch")
|
||||
focusChatSearch({ history: setHistory, review: setReviewActive, terminal: () => terms.setActiveId(undefined) })
|
||||
else if (msg.action === "newTerminal") {
|
||||
if (terms.sideFocusedId()) termHandlers.addSide()
|
||||
else termHandlers.requestNew()
|
||||
} else if (msg.action === "cycleAgentMode" && document.hasFocus()) {
|
||||
else if (msg.action === "newTerminalTab") termHandlers.requestNew()
|
||||
else if (msg.action === "newSideTerminal") termHandlers.addSide()
|
||||
else if (msg.action === "cycleAgentMode" && document.hasFocus()) {
|
||||
if (!mode.dispatch(1)) cycleAgent(1)
|
||||
} else if (msg.action === "cyclePreviousAgentMode" && document.hasFocus()) {
|
||||
if (!mode.dispatch(-1)) cycleAgent(-1)
|
||||
@@ -1266,8 +1246,8 @@ const AgentManagerContent: Component = () => {
|
||||
if (["t", "w", "n", "d", "e", "f"].includes(e.key.toLowerCase()) && !e.shiftKey) {
|
||||
e.preventDefault()
|
||||
}
|
||||
// Prevent browser defaults for shift variants (new terminal, close worktree,
|
||||
// advanced/new/open worktree, open PR, terminal cycling)
|
||||
// Prevent browser defaults for shift variants (new central terminal,
|
||||
// close worktree, advanced/new/open worktree, open PR, terminal cycling)
|
||||
if (["t", "m", "w", "n", "o", "r", "[", "]"].includes(e.key.toLowerCase()) && e.shiftKey) {
|
||||
e.preventDefault()
|
||||
}
|
||||
@@ -1322,6 +1302,7 @@ const AgentManagerContent: Component = () => {
|
||||
const onWindowFocus = () => {
|
||||
document.body.style.pointerEvents = ""
|
||||
document.body.style.overflow = ""
|
||||
focusCtl.report()
|
||||
restoreFocus()
|
||||
}
|
||||
window.addEventListener("focus", onWindowFocus)
|
||||
@@ -1382,13 +1363,14 @@ const AgentManagerContent: Component = () => {
|
||||
state: terms,
|
||||
activate: termHandlers.activate,
|
||||
saveTabMemory,
|
||||
rememberSession: tabs.remember,
|
||||
setSelection,
|
||||
showError: (message) =>
|
||||
showToast({ variant: "error", title: t("agentManager.terminal.errorTitle"), description: message }),
|
||||
postMessage: (message) => vscode.postMessage(message as never),
|
||||
onCreated: (contextKey, terminalId) => appendToTabOrder(contextKey, terminalId),
|
||||
|
||||
onSideClosed: (_contextKey, terminalId) => forgetTerminalFocus(terminalId),
|
||||
onSideClosed: (_contextKey, terminalId) => forgetTerminalFocus(focusMemory, terminalId),
|
||||
onScriptRunning: (contextKey, terminalId) => {
|
||||
if (terms.sideKey() !== contextKey) return
|
||||
// Setup output is informational: reveal without stealing focus, and
|
||||
@@ -1499,6 +1481,8 @@ const AgentManagerContent: Component = () => {
|
||||
setKb(ev.bindings)
|
||||
}
|
||||
|
||||
if (msg.type === "agentManager.focusContextRequested") focusCtl.report()
|
||||
|
||||
if (msg.type === "agentManager.state") applyState(msg)
|
||||
|
||||
// When a multi-version progress update arrives, mark newly created worktrees as loading
|
||||
@@ -2041,6 +2025,28 @@ const AgentManagerContent: Component = () => {
|
||||
}
|
||||
})
|
||||
}
|
||||
const handleNewTabForCurrentSelection = () => {
|
||||
const sel = selection()
|
||||
if (sel === LOCAL) {
|
||||
addPendingTab()
|
||||
return "ready" as const
|
||||
}
|
||||
if (sel) vscode.postMessage({ type: "agentManager.addSessionToWorktree", worktreeId: sel })
|
||||
return "pending" as const
|
||||
}
|
||||
const tabs = createSessionRestore({
|
||||
terminal: terms.activeId,
|
||||
selection,
|
||||
remembered: (sel) => registry.active().sessionRestore.get(sel),
|
||||
sessions: activeTabs,
|
||||
current: session.currentSessionID,
|
||||
pending: activePendingId,
|
||||
isPending,
|
||||
select: selectSessionTab,
|
||||
create: handleNewTabForCurrentSelection,
|
||||
remember: (sel, id) => registry.active().sessionRestore.set(sel, id),
|
||||
})
|
||||
restoreSession = tabs.restore
|
||||
const termHandlers = createTerminalHandlers({
|
||||
state: terms,
|
||||
tabIds: () => tabIds(),
|
||||
@@ -2233,17 +2239,6 @@ const AgentManagerContent: Component = () => {
|
||||
handleCloseTab(target.id)
|
||||
}
|
||||
|
||||
// Cmd+T: add a new tab strictly to the current selection (no side effects)
|
||||
const handleNewTabForCurrentSelection = () => {
|
||||
const sel = selection()
|
||||
if (sel === LOCAL) {
|
||||
addPendingTab()
|
||||
} else if (sel) {
|
||||
// Pass the captured worktree ID directly to avoid race conditions
|
||||
vscode.postMessage({ type: "agentManager.addSessionToWorktree", worktreeId: sel })
|
||||
}
|
||||
}
|
||||
|
||||
// Close the currently selected worktree with a confirmation dialog
|
||||
const closeSelectedWorktree = () => {
|
||||
const sel = selection()
|
||||
@@ -2530,7 +2525,11 @@ const AgentManagerContent: Component = () => {
|
||||
>
|
||||
<div class={`am-main-pane ${terms.activeId() ? "am-main-pane-terminal-active" : ""}`}>
|
||||
{/* Keep terminal tabs mounted so output streams across worktree switches. */}
|
||||
{renderTerminalLayer({ state: terms, onFocusPrompt: prompt.focus })}
|
||||
{renderTerminalLayer({
|
||||
state: terms,
|
||||
onFocusPrompt: focusCtl.focus,
|
||||
onFocusChange: focusCtl.report,
|
||||
})}
|
||||
{/* Session-less context (e.g. a worktree mid-provisioning): the
|
||||
empty state lives in the main pane so the side terminal
|
||||
panel can render next to it. */}
|
||||
@@ -2591,8 +2590,8 @@ const AgentManagerContent: Component = () => {
|
||||
deferFocusToQuestion={hasQuestionOption}
|
||||
pendingSessionID={selection() === LOCAL ? activePendingId() : undefined}
|
||||
focusOnDraftChange={focusOnDraftChange}
|
||||
onFocusChange={rememberPromptFocus}
|
||||
resolveEmbeddedTerminal={resolveEmbeddedTerminal}
|
||||
onFocusChange={focusCtl.prompt}
|
||||
resolveEmbeddedTerminal={resolveTerminal}
|
||||
/>
|
||||
<Show when={readOnly()}>
|
||||
<div class="am-readonly-banner">
|
||||
@@ -2719,7 +2718,8 @@ const AgentManagerContent: Component = () => {
|
||||
visible={() => sidePanel() === SidePanel.Terminal}
|
||||
nextKeybind={kb().nextTerminal ?? ""}
|
||||
closeKeybind={kb().closeTab ?? ""}
|
||||
onFocusPrompt={prompt.focus}
|
||||
onFocusPrompt={focusCtl.focus}
|
||||
onFocusChange={focusCtl.report}
|
||||
onSelect={(id) => termHandlers.selectSide(id)}
|
||||
onClose={(id) => {
|
||||
cancelAmbientSetup()
|
||||
|
||||
@@ -272,7 +272,7 @@ export const TabBar: Component<TabBarProps> = (props) => (
|
||||
{/* Terminal destination split button: the primary action
|
||||
follows the user's setting (VS Code integrated terminal
|
||||
or the embedded side panel), the dropdown picks which.
|
||||
Cmd+Shift+T creates a terminal in the active terminal container. */}
|
||||
Cmd+Shift+T creates a central terminal from center focus. Cmd+T creates a session in the center or a terminal in the right sidebar. */}
|
||||
<TerminalDestinationButton
|
||||
destination={props.terminalDestination}
|
||||
active={props.terminalDestinationActive}
|
||||
|
||||
@@ -2,6 +2,48 @@ import { isTextControl } from "../src/utils/focus"
|
||||
|
||||
const OPTION = '[data-component="question-dock"] button[data-slot="question-option"]'
|
||||
|
||||
export type AgentManagerFocusTarget = "prompt" | "mainTerminal" | "sideTerminal" | "other"
|
||||
|
||||
export function forgetTerminalFocus(memory: Map<string, "prompt" | { terminal: string }>, id: string): void {
|
||||
for (const [key, owner] of memory) if (owner !== "prompt" && owner.terminal === id) memory.delete(key)
|
||||
}
|
||||
|
||||
/** Resolve focus from the current DOM owner, not focus event order. */
|
||||
export function agentManagerFocusTarget(active: Element | null, promptPending = false): AgentManagerFocusTarget {
|
||||
if (promptPending || active?.matches("textarea.prompt-input")) return "prompt"
|
||||
if (active?.closest(".am-side-terminal-layer .am-terminal-host")) return "sideTerminal"
|
||||
if (active?.closest(".am-terminal-layer .am-terminal-host")) return "mainTerminal"
|
||||
return "other"
|
||||
}
|
||||
|
||||
export function createFocusBridge(deps: {
|
||||
prompt: { active: () => boolean; focus: () => void }
|
||||
post: (target: AgentManagerFocusTarget) => void
|
||||
remember: () => void
|
||||
restore: () => "none" | "ready" | "pending"
|
||||
}) {
|
||||
const report = () => {
|
||||
const active = agentManagerFocusTarget(document.activeElement)
|
||||
deps.post(
|
||||
active === "mainTerminal" || active === "sideTerminal"
|
||||
? active
|
||||
: agentManagerFocusTarget(document.activeElement, deps.prompt.active()),
|
||||
)
|
||||
}
|
||||
return {
|
||||
report,
|
||||
prompt: (focused: boolean) => {
|
||||
if (focused) deps.remember()
|
||||
report()
|
||||
},
|
||||
focus: () => {
|
||||
if (deps.restore() === "pending") return
|
||||
deps.post("prompt")
|
||||
deps.prompt.focus()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/** Keep an active editor, such as the worktree rename input, in control. */
|
||||
export const preservesTextFocus = (active: Element | null): boolean =>
|
||||
active !== null && isTextControl(active) && !active.classList.contains("prompt-input")
|
||||
|
||||
@@ -61,6 +61,8 @@ export const dict = {
|
||||
"agentManager.sidebarSearch.contexts": "محلي & WORKTREES",
|
||||
|
||||
"agentManager.terminal.new": "علامة تبويب جديدة للمحطة الطرفية",
|
||||
"agentManager.terminal.addCentral": "علامة تبويب طرفية مركزية جديدة",
|
||||
"agentManager.terminal.addTerminal": "علامة تبويب طرفية جديدة في الشريط الجانبي",
|
||||
"agentManager.terminal.ended": "انتهت المحطة الطرفية — أغلق علامة التبويب للإخفاء",
|
||||
"agentManager.terminal.endedRestartable": "انتهت المحطة الطرفية - اكتب لبدء صدفة جديدة أو أغلق علامة التبويب",
|
||||
"agentManager.terminal.setupFailed": "فشل البرنامج النصي للإعداد",
|
||||
|
||||
@@ -62,6 +62,8 @@ export const dict = {
|
||||
"agentManager.sidebarSearch.contexts": "LOCAL & WORKTREES",
|
||||
|
||||
"agentManager.terminal.new": "Nova aba de terminal",
|
||||
"agentManager.terminal.addCentral": "Nova aba de terminal central",
|
||||
"agentManager.terminal.addTerminal": "Nova aba de terminal na barra lateral",
|
||||
"agentManager.terminal.ended": "terminal encerrado — feche a aba para dispensar",
|
||||
"agentManager.terminal.endedRestartable": "terminal encerrado - digite para iniciar um novo shell ou feche a aba",
|
||||
"agentManager.terminal.setupFailed": "falha no script de configuração",
|
||||
|
||||
@@ -61,6 +61,8 @@ export const dict = {
|
||||
"agentManager.sidebarSearch.contexts": "LOKALNO & WORKTREES",
|
||||
|
||||
"agentManager.terminal.new": "Nova kartica terminala",
|
||||
"agentManager.terminal.addCentral": "Nova centralna kartica terminala",
|
||||
"agentManager.terminal.addTerminal": "Nova kartica terminala u bočnoj traci",
|
||||
"agentManager.terminal.ended": "terminal je završen — zatvorite karticu da biste odbacili",
|
||||
"agentManager.terminal.endedRestartable": "terminal je završen - kucajte za novu ljusku ili zatvorite karticu",
|
||||
"agentManager.terminal.setupFailed": "skripta za postavljanje nije uspjela",
|
||||
|
||||
@@ -63,6 +63,8 @@ export const dict = {
|
||||
"agentManager.sidebarSearch.contexts": "LOKAL & WORKTREES",
|
||||
|
||||
"agentManager.terminal.new": "Ny terminalfane",
|
||||
"agentManager.terminal.addCentral": "Ny central terminalfane",
|
||||
"agentManager.terminal.addTerminal": "Ny terminalfane i sidepanelet",
|
||||
"agentManager.terminal.ended": "terminal afsluttet — luk fanen for at fjerne",
|
||||
"agentManager.terminal.endedRestartable": "terminal afsluttet - skriv for at starte en ny shell, eller luk fanen",
|
||||
"agentManager.terminal.setupFailed": "opsætningsscript mislykkedes",
|
||||
|
||||
@@ -62,6 +62,8 @@ export const dict = {
|
||||
"agentManager.sidebarSearch.contexts": "LOKAL & WORKTREES",
|
||||
|
||||
"agentManager.terminal.new": "Neuer Terminal-Tab",
|
||||
"agentManager.terminal.addCentral": "Neuer zentraler Terminal-Tab",
|
||||
"agentManager.terminal.addTerminal": "Neuer Terminal-Tab in der Seitenleiste",
|
||||
"agentManager.terminal.ended": "Terminal beendet — Tab schließen zum Verwerfen",
|
||||
"agentManager.terminal.endedRestartable":
|
||||
"Terminal beendet - tippen, um eine neue Shell zu starten, oder Tab schließen",
|
||||
|
||||
@@ -66,6 +66,8 @@ export const dict = {
|
||||
|
||||
"agentManager.terminal.new": "New Terminal Tab",
|
||||
"agentManager.terminal.add": "New terminal",
|
||||
"agentManager.terminal.addCentral": "New central terminal tab",
|
||||
"agentManager.terminal.addTerminal": "New sidebar terminal tab",
|
||||
"agentManager.terminal.ended": "terminal ended — close tab to dismiss",
|
||||
"agentManager.terminal.endedRestartable": "terminal ended - type to start a new shell or close tab to dismiss",
|
||||
"agentManager.terminal.setupFailed": "setup script failed",
|
||||
|
||||
@@ -62,6 +62,8 @@ export const dict = {
|
||||
"agentManager.sidebarSearch.contexts": "LOCAL & WORKTREES",
|
||||
|
||||
"agentManager.terminal.new": "Nueva pestaña de terminal",
|
||||
"agentManager.terminal.addCentral": "Nueva pestaña de terminal central",
|
||||
"agentManager.terminal.addTerminal": "Nueva pestaña de terminal en la barra lateral",
|
||||
"agentManager.terminal.ended": "terminal finalizado — cierra la pestaña para descartar",
|
||||
"agentManager.terminal.endedRestartable":
|
||||
"terminal finalizado - escribe para iniciar un shell nuevo o cierra la pestaña",
|
||||
|
||||
@@ -65,6 +65,8 @@ export const dict = {
|
||||
"agentManager.sidebarSearch.contexts": "محلی و WORKTREEها",
|
||||
|
||||
"agentManager.terminal.new": "تب ترمینال جدید",
|
||||
"agentManager.terminal.addCentral": "زبانه ترمینال مرکزی جدید",
|
||||
"agentManager.terminal.addTerminal": "زبانه ترمینال جدید در نوار کناری",
|
||||
"agentManager.terminal.add": "ترمینال جدید",
|
||||
"agentManager.terminal.ended": "ترمینال پایان یافت — برای بستن، تب را ببندید",
|
||||
"agentManager.terminal.endedRestartable": "ترمینال پایان یافت - برای شروع پوسته جدید تایپ کنید یا تب را ببندید",
|
||||
|
||||
@@ -62,6 +62,8 @@ export const dict = {
|
||||
"agentManager.sidebarSearch.contexts": "LOCAL ET WORKTREES",
|
||||
|
||||
"agentManager.terminal.new": "Nouvel onglet de terminal",
|
||||
"agentManager.terminal.addCentral": "Nouvel onglet de terminal central",
|
||||
"agentManager.terminal.addTerminal": "Nouvel onglet de terminal dans la barre latérale",
|
||||
"agentManager.terminal.ended": "terminal terminé — fermez l'onglet pour ignorer",
|
||||
"agentManager.terminal.endedRestartable":
|
||||
"terminal terminé - saisissez du texte pour démarrer un nouveau shell ou fermez l'onglet",
|
||||
|
||||
@@ -66,6 +66,8 @@ export const dict = {
|
||||
"agentManager.sidebarSearch.contexts": "LOCALE & WORKTREE",
|
||||
|
||||
"agentManager.terminal.new": "Nuova scheda terminale",
|
||||
"agentManager.terminal.addCentral": "Nuova scheda terminale centrale",
|
||||
"agentManager.terminal.addTerminal": "Nuova scheda terminale nella barra laterale",
|
||||
"agentManager.terminal.ended": "terminale terminato - chiudi la scheda per nasconderlo",
|
||||
"agentManager.terminal.endedRestartable":
|
||||
"terminale terminato - digita per avviare una nuova shell o chiudi la scheda",
|
||||
|
||||
@@ -62,6 +62,8 @@ export const dict = {
|
||||
"agentManager.sidebarSearch.contexts": "ローカル & WORKTREES",
|
||||
|
||||
"agentManager.terminal.new": "新しいターミナルタブ",
|
||||
"agentManager.terminal.addCentral": "新しい中央ターミナルタブ",
|
||||
"agentManager.terminal.addTerminal": "サイドバーに新しいターミナルタブ",
|
||||
"agentManager.terminal.ended": "ターミナルが終了しました — タブを閉じて破棄",
|
||||
"agentManager.terminal.endedRestartable":
|
||||
"ターミナルが終了しました - 入力して新しいシェルを開始するか、タブを閉じてください",
|
||||
|
||||
@@ -61,6 +61,8 @@ export const dict = {
|
||||
"agentManager.sidebarSearch.contexts": "로컬 & WORKTREES",
|
||||
|
||||
"agentManager.terminal.new": "새 터미널 탭",
|
||||
"agentManager.terminal.addCentral": "새 중앙 터미널 탭",
|
||||
"agentManager.terminal.addTerminal": "사이드바에 새 터미널 탭",
|
||||
"agentManager.terminal.ended": "터미널 종료됨 — 탭을 닫아 해제",
|
||||
"agentManager.terminal.endedRestartable": "터미널 종료됨 - 입력하여 새 셸을 시작하거나 탭을 닫으세요",
|
||||
"agentManager.terminal.setupFailed": "설정 스크립트 실패",
|
||||
|
||||
@@ -65,6 +65,8 @@ export const dict = {
|
||||
"agentManager.sidebarSearch.contexts": "LOKAAL & WORKTREES",
|
||||
|
||||
"agentManager.terminal.new": "Nieuw terminaltabblad",
|
||||
"agentManager.terminal.addCentral": "Nieuw centraal terminaltabblad",
|
||||
"agentManager.terminal.addTerminal": "Nieuw terminaltabblad in de zijbalk",
|
||||
"agentManager.terminal.ended": "terminal beëindigd — sluit tabblad om te negeren",
|
||||
"agentManager.terminal.endedRestartable":
|
||||
"terminal beëindigd - typ om een nieuwe shell te starten of sluit het tabblad",
|
||||
|
||||
@@ -61,6 +61,8 @@ export const dict = {
|
||||
"agentManager.sidebarSearch.contexts": "LOKAL & WORKTREES",
|
||||
|
||||
"agentManager.terminal.new": "Ny terminalfane",
|
||||
"agentManager.terminal.addCentral": "Ny sentral terminalfane",
|
||||
"agentManager.terminal.addTerminal": "Ny terminalfane i sidepanelet",
|
||||
"agentManager.terminal.ended": "terminal avsluttet — lukk fanen for å avvise",
|
||||
"agentManager.terminal.endedRestartable": "terminal avsluttet - skriv for å starte et nytt skall, eller lukk fanen",
|
||||
"agentManager.terminal.setupFailed": "oppsettskript mislyktes",
|
||||
|
||||
@@ -62,6 +62,8 @@ export const dict = {
|
||||
"agentManager.sidebarSearch.contexts": "LOKALNE & WORKTREES",
|
||||
|
||||
"agentManager.terminal.new": "Nowa karta terminala",
|
||||
"agentManager.terminal.addCentral": "Nowa centralna karta terminala",
|
||||
"agentManager.terminal.addTerminal": "Nowa karta terminala na pasku bocznym",
|
||||
"agentManager.terminal.ended": "terminal zakończony — zamknij kartę, aby zamknąć",
|
||||
"agentManager.terminal.endedRestartable":
|
||||
"terminal zakończony - wpisz tekst, aby uruchomić nową powłokę, lub zamknij kartę",
|
||||
|
||||
@@ -62,6 +62,8 @@ export const dict = {
|
||||
"agentManager.sidebarSearch.contexts": "ЛОКАЛЬНЫЙ & WORKTREES",
|
||||
|
||||
"agentManager.terminal.new": "Новая вкладка терминала",
|
||||
"agentManager.terminal.addCentral": "Новая центральная вкладка терминала",
|
||||
"agentManager.terminal.addTerminal": "Новая вкладка терминала на боковой панели",
|
||||
"agentManager.terminal.ended": "терминал завершен — закройте вкладку, чтобы скрыть",
|
||||
"agentManager.terminal.endedRestartable":
|
||||
"терминал завершен - введите текст, чтобы запустить новую оболочку, или закройте вкладку",
|
||||
|
||||
@@ -61,6 +61,8 @@ export const dict = {
|
||||
"agentManager.sidebarSearch.contexts": "ในเครื่อง & WORKTREES",
|
||||
|
||||
"agentManager.terminal.new": "แท็บเทอร์มินัลใหม่",
|
||||
"agentManager.terminal.addCentral": "แท็บเทอร์มินัลกลางใหม่",
|
||||
"agentManager.terminal.addTerminal": "แท็บเทอร์มินัลใหม่ในแถบด้านข้าง",
|
||||
"agentManager.terminal.ended": "เทอร์มินัลสิ้นสุด — ปิดแท็บเพื่อยกเลิก",
|
||||
"agentManager.terminal.endedRestartable": "เทอร์มินัลสิ้นสุด - พิมพ์เพื่อเริ่มเชลล์ใหม่หรือปิดแท็บ",
|
||||
"agentManager.terminal.setupFailed": "สคริปต์ติดตั้งล้มเหลว",
|
||||
|
||||
@@ -66,6 +66,8 @@ export const dict = {
|
||||
"agentManager.sidebarSearch.contexts": "YEREL & WORKTREE'LER",
|
||||
|
||||
"agentManager.terminal.new": "Yeni Terminal Sekmesi",
|
||||
"agentManager.terminal.addCentral": "Yeni merkezi terminal sekmesi",
|
||||
"agentManager.terminal.addTerminal": "Kenar çubuğunda yeni terminal sekmesi",
|
||||
"agentManager.terminal.ended": "terminal sona erdi — kapatmak için sekmeyi kapatın",
|
||||
"agentManager.terminal.endedRestartable":
|
||||
"terminal sona erdi - yeni bir kabuk başlatmak için yazın veya sekmeyi kapatın",
|
||||
|
||||
@@ -66,6 +66,8 @@ export const dict = {
|
||||
"agentManager.sidebarSearch.contexts": "ЛОКАЛЬНИЙ & РОБОЧІ ДЕРЕВА",
|
||||
|
||||
"agentManager.terminal.new": "Нова вкладка термінала",
|
||||
"agentManager.terminal.addCentral": "Нова центральна вкладка термінала",
|
||||
"agentManager.terminal.addTerminal": "Нова вкладка термінала на бічній панелі",
|
||||
"agentManager.terminal.ended": "термінал завершено — закрийте вкладку, щоб відхилити",
|
||||
"agentManager.terminal.endedRestartable":
|
||||
"термінал завершено - введіть текст, щоб запустити нову оболонку, або закрийте вкладку",
|
||||
|
||||
@@ -61,6 +61,8 @@ export const dict = {
|
||||
"agentManager.sidebarSearch.contexts": "本地 & WORKTREES",
|
||||
|
||||
"agentManager.terminal.new": "新建终端标签页",
|
||||
"agentManager.terminal.addCentral": "新建中央终端标签页",
|
||||
"agentManager.terminal.addTerminal": "在侧边栏新建终端标签页",
|
||||
"agentManager.terminal.ended": "终端已结束 — 关闭标签页以消除",
|
||||
"agentManager.terminal.endedRestartable": "终端已结束 - 输入以启动新 shell,或关闭标签页",
|
||||
"agentManager.terminal.setupFailed": "设置脚本失败",
|
||||
|
||||
@@ -61,6 +61,8 @@ export const dict = {
|
||||
"agentManager.sidebarSearch.contexts": "本機 & WORKTREES",
|
||||
|
||||
"agentManager.terminal.new": "新增終端分頁",
|
||||
"agentManager.terminal.addCentral": "新增中央終端機分頁",
|
||||
"agentManager.terminal.addTerminal": "在側邊欄新增終端機分頁",
|
||||
"agentManager.terminal.ended": "終端已結束 — 關閉分頁以消除",
|
||||
"agentManager.terminal.endedRestartable": "終端已結束 - 輸入以啟動新的 shell,或關閉分頁",
|
||||
"agentManager.terminal.setupFailed": "設定腳本失敗",
|
||||
|
||||
@@ -12,7 +12,8 @@ export const defaultBindings: Record<string, string> = {
|
||||
nextTerminal: isMac ? "⌘⇧]" : "Ctrl+Shift+]",
|
||||
search: isMac ? "⌘F" : "Ctrl+F",
|
||||
showTerminal: isMac ? "⌘/" : "Ctrl+/",
|
||||
newTerminal: isMac ? "⌘⇧T" : "Ctrl+Shift+T",
|
||||
newTerminalCenter: isMac ? "⌘⇧T" : "Ctrl+Shift+T",
|
||||
newTerminalTerminal: isMac ? "⌘T" : "Ctrl+T",
|
||||
runScript: isMac ? "⌘E" : "Ctrl+E",
|
||||
toggleDiff: isMac ? "⌘D" : "Ctrl+D",
|
||||
showShortcuts: isMac ? "⌘⇧/" : "Ctrl+Shift+/",
|
||||
|
||||
@@ -51,6 +51,13 @@ export function createProjectStore(id: string, opts: { tabs?: string[] } = {}) {
|
||||
get: (sel: string) => memory()[sel],
|
||||
set: (sel: string, tab: string) => setMemory((prev) => (prev[sel] === tab ? prev : { ...prev, [sel]: tab })),
|
||||
}
|
||||
/** Last session tab to restore after leaving a central terminal. */
|
||||
const [sessionMemory, setSessionMemory] = createSignal<Record<string, string>>({})
|
||||
const sessionRestore = {
|
||||
all: sessionMemory,
|
||||
get: (sel: string) => sessionMemory()[sel],
|
||||
set: (sel: string, tab: string) => setSessionMemory((prev) => (prev[sel] === tab ? prev : { ...prev, [sel]: tab })),
|
||||
}
|
||||
|
||||
const [worktrees, setWorktrees] = field<WorktreeState[]>([])
|
||||
const [managedSessions, setManagedSessions] = field<ManagedSessionState[]>([])
|
||||
@@ -96,6 +103,7 @@ export function createProjectStore(id: string, opts: { tabs?: string[] } = {}) {
|
||||
tabs,
|
||||
applyState,
|
||||
tabMemory,
|
||||
sessionRestore,
|
||||
worktrees,
|
||||
setWorktrees,
|
||||
managedSessions,
|
||||
|
||||
@@ -37,6 +37,53 @@ export interface SelectionActionDeps<T extends SessionLike> {
|
||||
isReviewTab: (remembered: string | undefined, sel: string) => boolean
|
||||
}
|
||||
|
||||
export function restoreSessionAfterTerminal<T extends SessionLike>(input: {
|
||||
terminal: string | undefined
|
||||
remembered: string | undefined
|
||||
sessions: T[]
|
||||
isPending: (id: string) => boolean
|
||||
select: (id: string, pending: boolean) => void
|
||||
create: () => "ready" | "pending"
|
||||
}): "none" | "ready" | "pending" {
|
||||
if (!input.terminal) return "none"
|
||||
const target = input.sessions.find((item) => item.id === input.remembered) ?? input.sessions[0]
|
||||
if (target) input.select(target.id, input.isPending(target.id))
|
||||
else return input.create()
|
||||
return "ready"
|
||||
}
|
||||
|
||||
export function createSessionRestore<T extends SessionLike>(deps: {
|
||||
terminal: () => string | undefined
|
||||
selection: () => string | null
|
||||
remembered: (selection: string) => string | undefined
|
||||
sessions: () => T[]
|
||||
current: () => string | undefined
|
||||
pending: () => string | undefined
|
||||
isPending: (id: string) => boolean
|
||||
select: (id: string, pending: boolean) => void
|
||||
create: () => "ready" | "pending"
|
||||
remember: (selection: string, id: string) => void
|
||||
}) {
|
||||
return {
|
||||
remember: () => {
|
||||
const selection = deps.selection()
|
||||
const id = deps.current() ?? deps.pending()
|
||||
if (selection !== null && id) deps.remember(selection, id)
|
||||
},
|
||||
restore: () => {
|
||||
const selection = deps.selection()
|
||||
return restoreSessionAfterTerminal({
|
||||
terminal: deps.terminal(),
|
||||
remembered: selection === null ? undefined : deps.remembered(selection),
|
||||
sessions: deps.sessions(),
|
||||
isPending: deps.isPending,
|
||||
select: deps.select,
|
||||
create: deps.create,
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/** Select the Local context: restore its remembered tab or fall back to the first session/draft. */
|
||||
export function selectLocalAction<T extends SessionLike>(deps: SelectionActionDeps<T>, locals: T[]): void {
|
||||
deps.saveTabMemory()
|
||||
|
||||
@@ -53,7 +53,8 @@ export function buildShortcutCategories(
|
||||
title: t("agentManager.shortcuts.category.terminal"),
|
||||
shortcuts: [
|
||||
{ label: t("agentManager.shortcuts.toggleTerminal"), binding: bind("showTerminal") },
|
||||
{ label: t("agentManager.terminal.add"), binding: bind("newTerminal") },
|
||||
{ label: t("agentManager.terminal.addCentral"), binding: bind("newTerminalCenter") },
|
||||
{ label: t("agentManager.terminal.addTerminal"), binding: bind("newTerminalTerminal") },
|
||||
{
|
||||
label: `${t("agentManager.shortcuts.previousTab")} (${t("agentManager.tab.terminal")})`,
|
||||
binding: bind("previousTerminal"),
|
||||
|
||||
@@ -236,6 +236,10 @@ export interface NewTabButtonDeps {
|
||||
onNewTerminal: () => void
|
||||
}
|
||||
|
||||
function keybind(deps: NewTabButtonDeps, name: string): string {
|
||||
return deps.kb()[name] ?? ""
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the tab bar's "new" affordance: a split button with the plus
|
||||
* icon (primary action: new agent session) and a chevron that opens a
|
||||
@@ -250,7 +254,7 @@ export function renderNewTabButton(deps: NewTabButtonDeps): JSX.Element {
|
||||
<div class="am-split-button am-tab-add-split">
|
||||
<TooltipKeybind
|
||||
title={deps.newSessionLabel}
|
||||
keybind={deps.kb().newTab ?? ""}
|
||||
keybind={keybind(deps, "newTab")}
|
||||
placement="top"
|
||||
gutter={8}
|
||||
openDelay={0}
|
||||
@@ -273,7 +277,7 @@ export function renderNewTabButton(deps: NewTabButtonDeps): JSX.Element {
|
||||
<Icon name="plus" size="small" />
|
||||
<DropdownMenu.ItemLabel>{deps.newSessionMenuLabel}</DropdownMenu.ItemLabel>
|
||||
<span class="am-menu-shortcut">
|
||||
{parseBindingTokens(deps.kb().newTab ?? "").map((token) => (
|
||||
{parseBindingTokens(keybind(deps, "newTab")).map((token) => (
|
||||
<kbd class="am-menu-key">{token}</kbd>
|
||||
))}
|
||||
</span>
|
||||
@@ -282,7 +286,7 @@ export function renderNewTabButton(deps: NewTabButtonDeps): JSX.Element {
|
||||
<Icon name="console" size="small" />
|
||||
<DropdownMenu.ItemLabel>{deps.newTerminalLabel}</DropdownMenu.ItemLabel>
|
||||
<span class="am-menu-shortcut">
|
||||
{parseBindingTokens(deps.kb().newTerminal ?? "").map((token) => (
|
||||
{parseBindingTokens(keybind(deps, "newTerminalCenter")).map((token) => (
|
||||
<kbd class="am-menu-key">{token}</kbd>
|
||||
))}
|
||||
</span>
|
||||
|
||||
@@ -40,6 +40,7 @@ interface Props {
|
||||
/** Deliberately stop a running script terminal. */
|
||||
onStop: (terminalId: string) => void
|
||||
onFocusPrompt: () => void
|
||||
onFocusChange?: (focused: boolean) => void
|
||||
}
|
||||
|
||||
export const SideTerminalPanel: Component<Props> = (props) => {
|
||||
@@ -127,6 +128,7 @@ export const SideTerminalPanel: Component<Props> = (props) => {
|
||||
contextKey: props.contextKey,
|
||||
visible: props.visible,
|
||||
onFocusPrompt: props.onFocusPrompt,
|
||||
onFocusChange: props.onFocusChange,
|
||||
})}
|
||||
<Show when={props.visible() && sides().length === 0 && pending()}>
|
||||
<div class="am-side-terminal-state" role="status">
|
||||
|
||||
@@ -182,7 +182,7 @@ export const TerminalTab: Component<Props> = (props) => {
|
||||
term.unicode.activeVersion = "15-graphemes"
|
||||
|
||||
// Pass Agent Manager hotkeys through to the parent key handler so
|
||||
// ⌘T / ⌘⇧T / ⌘W / terminal cycling / ⌘⌥← still work while focused.
|
||||
// ⌘T / ⌘W / terminal cycling / ⌘⌥← still work while focused.
|
||||
term.attachCustomKeyEventHandler((event) => {
|
||||
const prompt =
|
||||
(event.metaKey || event.ctrlKey) && event.shiftKey && !event.altKey && event.key.toLowerCase() === "m"
|
||||
@@ -196,10 +196,11 @@ export const TerminalTab: Component<Props> = (props) => {
|
||||
// Track DOM focus so the state layer knows which terminal holds the
|
||||
// cursor (drives Cmd+W targeting). focusout is ignored when focus
|
||||
// moves within the same host (xterm shuffles inner nodes).
|
||||
const onFocusIn = () => props.onFocusChange?.(true)
|
||||
const reportFocus = () => props.onFocusChange?.(host.contains(document.activeElement))
|
||||
const onFocusIn = () => queueMicrotask(reportFocus)
|
||||
const onFocusOut = (event: FocusEvent) => {
|
||||
if (event.relatedTarget instanceof Node && host.contains(event.relatedTarget)) return
|
||||
props.onFocusChange?.(false)
|
||||
queueMicrotask(reportFocus)
|
||||
}
|
||||
host.addEventListener("focusin", onFocusIn)
|
||||
host.addEventListener("focusout", onFocusOut)
|
||||
|
||||
@@ -2,6 +2,39 @@ import { truncateTerminalOutput } from "../../../src/services/terminal/truncate"
|
||||
|
||||
type Reader = () => string
|
||||
|
||||
type Term = { id: string }
|
||||
|
||||
export function createEmbeddedTerminalReader(deps: {
|
||||
key: (context?: string) => string
|
||||
local: string
|
||||
side: (key: string) => Term[]
|
||||
tabs: (key: string) => Term[]
|
||||
focused: () => string | undefined
|
||||
sideActive: (key: string) => string | undefined
|
||||
active: () => string | undefined
|
||||
}) {
|
||||
return async (context?: string) => {
|
||||
const key = deps.key(context ?? deps.local)
|
||||
return resolveEmbeddedTerminal(deps.side(key), deps.tabs(key), deps.focused(), deps.sideActive(key), deps.active())
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveEmbeddedTerminal(
|
||||
side: Term[],
|
||||
tabs: Term[],
|
||||
focused: string | undefined,
|
||||
sideActive: string | undefined,
|
||||
active: string | undefined,
|
||||
): string | undefined {
|
||||
const focus = side.find((term) => term.id === focused) ?? tabs.find((term) => term.id === focused)
|
||||
const sideTerm = side.find((term) => term.id === sideActive)
|
||||
const tab = tabs.find((term) => term.id === active)
|
||||
const id = focus?.id ?? sideTerm?.id ?? tab?.id
|
||||
return id && (side.some((term) => term.id === id) || tabs.some((term) => term.id === id))
|
||||
? readTerminalOutput(id)
|
||||
: undefined
|
||||
}
|
||||
|
||||
const readers = new Map<string, Reader>()
|
||||
|
||||
export function registerTerminalOutput(id: string, read: Reader): void {
|
||||
|
||||
@@ -91,7 +91,11 @@ export function renderTerminalTab(deps: TerminalTabRenderDeps): JSX.Element {
|
||||
* exists; that boundary never flips under a live xterm, since removing
|
||||
* the last terminal disposes its instance first.
|
||||
*/
|
||||
export function renderTerminalLayer(props: { state: TerminalStateControls; onFocusPrompt: () => void }): JSX.Element {
|
||||
export function renderTerminalLayer(props: {
|
||||
state: TerminalStateControls
|
||||
onFocusPrompt: () => void
|
||||
onFocusChange?: (focused: boolean) => void
|
||||
}): JSX.Element {
|
||||
const layerActive = () => props.state.activeId() !== undefined
|
||||
const slotVisible = (termId: string, contextKey: string) =>
|
||||
props.state.activeId() === termId && props.state.currentKey() === contextKey
|
||||
@@ -110,7 +114,11 @@ export function renderTerminalLayer(props: { state: TerminalStateControls; onFoc
|
||||
active={visible()}
|
||||
focusSerial={focusSerial(props.state, term.id)}
|
||||
font={term.font}
|
||||
onFocusChange={(focused) => props.state.setFocusedId(focused ? term.id : undefined)}
|
||||
onFocusChange={(focused) => {
|
||||
if (focused) props.state.setFocusedId(term.id)
|
||||
else if (props.state.focusedId() === term.id) props.state.setFocusedId(undefined)
|
||||
props.onFocusChange?.(focused)
|
||||
}}
|
||||
onFocusPrompt={props.onFocusPrompt}
|
||||
onTitleChange={(title) => props.state.setTitle(term.id, title)}
|
||||
/>
|
||||
@@ -138,6 +146,7 @@ export function renderSideTerminalLayer(props: {
|
||||
contextKey: Accessor<string>
|
||||
visible: Accessor<boolean>
|
||||
onFocusPrompt: () => void
|
||||
onFocusChange?: (focused: boolean) => void
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<div class={`am-side-terminal-layer ${props.visible() ? "am-side-terminal-layer-active" : ""}`}>
|
||||
@@ -158,7 +167,11 @@ export function renderSideTerminalLayer(props: {
|
||||
font={term.font}
|
||||
status={() => props.state.scriptStatus(term.id)}
|
||||
restartable={term.kind === undefined}
|
||||
onFocusChange={(focused) => props.state.setFocusedId(focused ? term.id : undefined)}
|
||||
onFocusChange={(focused) => {
|
||||
if (focused) props.state.setFocusedId(term.id)
|
||||
else if (props.state.focusedId() === term.id) props.state.setFocusedId(undefined)
|
||||
props.onFocusChange?.(focused)
|
||||
}}
|
||||
onFocusPrompt={props.onFocusPrompt}
|
||||
onTitleChange={(title) => props.state.setTitle(term.id, title)}
|
||||
/>
|
||||
|
||||
@@ -906,6 +906,8 @@ export interface TerminalMessageHandlerDeps {
|
||||
state: TerminalStateControls
|
||||
activate: (id: string) => void
|
||||
saveTabMemory: () => void
|
||||
/** Remember the current session before a central terminal is selected. */
|
||||
rememberSession?: () => void
|
||||
setSelection: (sel: string | typeof LOCAL) => void
|
||||
showError: (message: string) => void
|
||||
postMessage: (message: unknown) => void
|
||||
@@ -960,6 +962,7 @@ function handleCreated(deps: TerminalMessageHandlerDeps, msg: CreatedMessage) {
|
||||
}
|
||||
return
|
||||
}
|
||||
deps.rememberSession?.()
|
||||
deps.state.add(key === LOCAL ? null : key, term)
|
||||
deps.onCreated?.(target, msg.terminalId)
|
||||
deps.saveTabMemory()
|
||||
|
||||
@@ -1279,7 +1279,12 @@ export interface ClipboardWriteResultMessage {
|
||||
error?: string
|
||||
}
|
||||
|
||||
export interface AgentManagerFocusContextRequestedMessage {
|
||||
type: "agentManager.focusContextRequested"
|
||||
}
|
||||
|
||||
export type ExtensionMessage =
|
||||
| AgentManagerFocusContextRequestedMessage
|
||||
| ReadyMessage
|
||||
| FontSizeChangedMessage
|
||||
| GitStatusMessage
|
||||
|
||||
@@ -191,6 +191,11 @@ export interface WebviewFocusChangedRequest {
|
||||
focused: boolean
|
||||
}
|
||||
|
||||
export interface AgentManagerFocusChangedRequest {
|
||||
type: "agentManagerFocusChanged"
|
||||
target: "prompt" | "mainTerminal" | "sideTerminal" | "other"
|
||||
}
|
||||
|
||||
export interface SelectSourceRequest {
|
||||
type: "selectSource"
|
||||
id: string
|
||||
@@ -1422,6 +1427,7 @@ export type WebviewMessage =
|
||||
| SetOrganizationRequest
|
||||
| WebviewReadyRequest
|
||||
| WebviewFocusChangedRequest
|
||||
| AgentManagerFocusChangedRequest
|
||||
| SelectSourceRequest
|
||||
| RequestProvidersMessage
|
||||
| CompactRequest
|
||||
|
||||
Reference in New Issue
Block a user