mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-08-28 19:11:03 +08:00
Merge remote-tracking branch 'origin/main' into johnnyeric/kilo-opencode-v1.18.0
This commit is contained in:
@@ -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.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Fix the sidebar navigation bar (New Task, History, Agent Manager, KiloClaw, Marketplace, Profile, Settings) disappearing when the Kilo Code view is docked in the Secondary Side Bar. The navigation is now rendered inside the webview itself, so it stays visible regardless of where the view is docked.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/cli": patch
|
||||
---
|
||||
|
||||
Support the Agent Manager tool with llama.cpp servers that reject prefix-only JSON Schema patterns.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Preserve the selected reasoning effort when switching to a model that supports the same or nearest available variant.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/cli": patch
|
||||
---
|
||||
|
||||
Avoid printing an error when closing the TUI cancels in-flight startup refreshes.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Translate the Agent Manager terminal focus shortcut label in all supported locales.
|
||||
@@ -458,6 +458,7 @@
|
||||
"@opencode-ai/ui": "workspace:*",
|
||||
"@pierre/diffs": "catalog:",
|
||||
"@thisbeyond/solid-dnd": "0.7.5",
|
||||
"@vscode/codicons": "^0.0.44",
|
||||
"@xterm/addon-clipboard": "0.2.0",
|
||||
"@xterm/addon-fit": "0.11.0",
|
||||
"@xterm/addon-unicode-graphemes": "0.4.0",
|
||||
|
||||
+4
-4
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"nodeModules": {
|
||||
"x86_64-linux": "sha256-XCtBeP2R+Tx2S4LEXn1RRZWoquOUdIFB+8O58n2krHI=",
|
||||
"aarch64-linux": "sha256-gnf+k+mI7JaqoCrVzFeEVBmKELUfZ9D6XpqzlBXKYzI=",
|
||||
"aarch64-darwin": "sha256-j9nQgeurmHs00dGOIuxGoV1W/tQ/DJnWrfllr66y9yg=",
|
||||
"x86_64-darwin": "sha256-+arUGf3HZvSRumkfX082dM0wErzzDexOz3zZcVDMlSI="
|
||||
"x86_64-linux": "sha256-epitmtKUd9fAucKdH/sDdqv5WmwfpiPH+h/PNt55gd4=",
|
||||
"aarch64-linux": "sha256-FHVsi2iho+U5aj6Z9lg2GTIu1ViUuqI2c/nZuwjmPH8=",
|
||||
"aarch64-darwin": "sha256-QDPVLcbTyaZRZvSnqvWNR/mIkS1TrZLX0HjUlg6xwwI=",
|
||||
"x86_64-darwin": "sha256-SSPc9b3WwcYCnuywcyMRJjVlugoXuo0UoPJ+qzPGajk="
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 |
|
||||
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:1bc2a28b80d8dc5aea4c03609efc13a23a2c88103e56df741155d9e794fc96f7
|
||||
size 2422
|
||||
@@ -141,41 +141,6 @@
|
||||
"category": "Kilo Code",
|
||||
"icon": "$(settings-gear)"
|
||||
},
|
||||
{
|
||||
"command": "kilo-code.new.sidebarTitle.plusButtonClicked",
|
||||
"title": "New Task",
|
||||
"icon": "$(add)"
|
||||
},
|
||||
{
|
||||
"command": "kilo-code.new.sidebarTitle.agentManagerOpen",
|
||||
"title": "Agent Manager",
|
||||
"icon": "$(organization)"
|
||||
},
|
||||
{
|
||||
"command": "kilo-code.new.sidebarTitle.kiloClawOpen",
|
||||
"title": "KiloClaw",
|
||||
"icon": "$(comment-discussion)"
|
||||
},
|
||||
{
|
||||
"command": "kilo-code.new.sidebarTitle.marketplaceButtonClicked",
|
||||
"title": "Marketplace",
|
||||
"icon": "$(extensions)"
|
||||
},
|
||||
{
|
||||
"command": "kilo-code.new.sidebarTitle.historyButtonClicked",
|
||||
"title": "History",
|
||||
"icon": "$(history)"
|
||||
},
|
||||
{
|
||||
"command": "kilo-code.new.sidebarTitle.profileButtonClicked",
|
||||
"title": "Profile",
|
||||
"icon": "$(account)"
|
||||
},
|
||||
{
|
||||
"command": "kilo-code.new.sidebarTitle.settingsButtonClicked",
|
||||
"title": "Settings",
|
||||
"icon": "$(settings-gear)"
|
||||
},
|
||||
{
|
||||
"command": "kilo-code.new.openInTab",
|
||||
"title": "Open in Tab",
|
||||
@@ -245,6 +210,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",
|
||||
@@ -461,76 +436,12 @@
|
||||
],
|
||||
"menus": {
|
||||
"commandPalette": [
|
||||
{
|
||||
"command": "kilo-code.new.sidebarTitle.plusButtonClicked",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "kilo-code.new.sidebarTitle.historyButtonClicked",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "kilo-code.new.sidebarTitle.agentManagerOpen",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "kilo-code.new.sidebarTitle.kiloClawOpen",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "kilo-code.new.agentManager.advancedWorktree",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "kilo-code.new.sidebarTitle.marketplaceButtonClicked",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "kilo-code.new.sidebarTitle.profileButtonClicked",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "kilo-code.new.sidebarTitle.settingsButtonClicked",
|
||||
"when": "false"
|
||||
}
|
||||
],
|
||||
"view/title": [
|
||||
{
|
||||
"command": "kilo-code.new.sidebarTitle.plusButtonClicked",
|
||||
"group": "navigation@0",
|
||||
"when": "view == kilo-code.SidebarProvider"
|
||||
},
|
||||
{
|
||||
"command": "kilo-code.new.sidebarTitle.historyButtonClicked",
|
||||
"group": "navigation@1",
|
||||
"when": "view == kilo-code.SidebarProvider"
|
||||
},
|
||||
{
|
||||
"command": "kilo-code.new.sidebarTitle.agentManagerOpen",
|
||||
"group": "navigation@2",
|
||||
"when": "view == kilo-code.SidebarProvider"
|
||||
},
|
||||
{
|
||||
"command": "kilo-code.new.sidebarTitle.kiloClawOpen",
|
||||
"group": "navigation@3",
|
||||
"when": "view == kilo-code.SidebarProvider"
|
||||
},
|
||||
{
|
||||
"command": "kilo-code.new.sidebarTitle.marketplaceButtonClicked",
|
||||
"group": "navigation@4",
|
||||
"when": "view == kilo-code.SidebarProvider"
|
||||
},
|
||||
{
|
||||
"command": "kilo-code.new.sidebarTitle.profileButtonClicked",
|
||||
"group": "navigation@5",
|
||||
"when": "view == kilo-code.SidebarProvider"
|
||||
},
|
||||
{
|
||||
"command": "kilo-code.new.sidebarTitle.settingsButtonClicked",
|
||||
"group": "navigation@6",
|
||||
"when": "view == kilo-code.SidebarProvider"
|
||||
}
|
||||
],
|
||||
"view/title": [],
|
||||
"scm/title": [
|
||||
{
|
||||
"command": "kilo-code.new.generateCommitMessage",
|
||||
@@ -550,26 +461,6 @@
|
||||
"command": "kilo-code.new.openInTab",
|
||||
"group": "navigation",
|
||||
"when": "true"
|
||||
},
|
||||
{
|
||||
"command": "kilo-code.new.plusButtonClicked",
|
||||
"group": "navigation@0",
|
||||
"when": "activeWebviewPanelId == kilo-code.new.TabPanel"
|
||||
},
|
||||
{
|
||||
"command": "kilo-code.new.historyButtonClicked",
|
||||
"group": "navigation@1",
|
||||
"when": "activeWebviewPanelId == kilo-code.new.TabPanel"
|
||||
},
|
||||
{
|
||||
"command": "kilo-code.new.profileButtonClicked",
|
||||
"group": "navigation@2",
|
||||
"when": "activeWebviewPanelId == kilo-code.new.TabPanel"
|
||||
},
|
||||
{
|
||||
"command": "kilo-code.new.settingsButtonClicked",
|
||||
"group": "navigation@3",
|
||||
"when": "activeWebviewPanelId == kilo-code.new.TabPanel"
|
||||
}
|
||||
],
|
||||
"editor/context": [
|
||||
@@ -633,6 +524,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",
|
||||
@@ -668,6 +564,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",
|
||||
@@ -1271,6 +1179,7 @@
|
||||
"@opencode-ai/ui": "workspace:*",
|
||||
"@pierre/diffs": "catalog:",
|
||||
"@thisbeyond/solid-dnd": "0.7.5",
|
||||
"@vscode/codicons": "^0.0.44",
|
||||
"@xterm/addon-clipboard": "0.2.0",
|
||||
"@xterm/addon-fit": "0.11.0",
|
||||
"@xterm/addon-unicode-graphemes": "0.4.0",
|
||||
|
||||
@@ -1048,6 +1048,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
|
||||
openAdvancedWorktree: () => vscode.commands.executeCommand("kilo-code.new.agentManager.advancedWorktree"),
|
||||
openChanges: (sessionId?: string, turnId?: string) =>
|
||||
vscode.commands.executeCommand("kilo-code.new.showChanges", { sessionId, turnId }),
|
||||
openProfile: () => vscode.commands.executeCommand("kilo-code.new.profileButtonClicked"),
|
||||
currentSessionId: this.currentSession?.id,
|
||||
createWorktree: async (baseBranch, branchName) => {
|
||||
await this.createWorktreeHandler?.(baseBranch, branchName)
|
||||
@@ -4908,6 +4909,8 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
|
||||
title: "Kilo Code",
|
||||
port: this.connectionService.getServerInfo()?.port,
|
||||
extraStyles: `.container { height: 100vh; }`,
|
||||
topBar: this.opts.hideTopBar !== true,
|
||||
topBarSurface: this.opts.topBarSurface === "tab" ? "tab_title" : "sidebar_title",
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -103,6 +103,7 @@ export class SettingsEditorProvider implements vscode.Disposable {
|
||||
// backend connectivity (config, providers, agents, profile, auth).
|
||||
const provider = new KiloProvider(this.extensionUri, this.connectionService, this.context, {
|
||||
projectDirectory,
|
||||
hideTopBar: true,
|
||||
})
|
||||
if (this.remoteService) {
|
||||
provider.setRemoteService(this.remoteService)
|
||||
|
||||
@@ -41,7 +41,7 @@ export class SubAgentViewerProvider implements vscode.Disposable {
|
||||
dark: vscode.Uri.joinPath(this.extensionUri, "assets", "icons", "kilo-dark.svg"),
|
||||
}
|
||||
|
||||
const provider = new KiloProvider(this.extensionUri, this.connectionService, this.context)
|
||||
const provider = new KiloProvider(this.extensionUri, this.connectionService, this.context, { hideTopBar: true })
|
||||
// Start accepting this session's SSE events as soon as the panel subscribes.
|
||||
// Reasoning deltas are not persisted until the reasoning part finishes.
|
||||
provider.trackSession(sessionID)
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
@@ -1693,9 +1693,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 {
|
||||
|
||||
@@ -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" })
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ import { ensureBackendForAutocomplete } from "./services/autocomplete/ensure-bac
|
||||
import { AutocompleteServiceManager } from "./services/autocomplete/AutocompleteServiceManager"
|
||||
import { AttentionService } from "./services/attention"
|
||||
import { BrowserAutomationService } from "./services/browser-automation"
|
||||
import { TelemetryEventName, TelemetryProxy } from "./services/telemetry"
|
||||
import { TelemetryProxy } from "./services/telemetry"
|
||||
import { registerCommitMessageService } from "./services/commit-message"
|
||||
import { registerCodeActions, registerTerminalActions, KiloCodeActionProvider } from "./services/code-actions"
|
||||
import { registerToggleAutoApprove } from "./commands/toggle-auto-approve"
|
||||
@@ -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)
|
||||
|
||||
@@ -232,6 +237,7 @@ export function activate(context: vscode.ExtensionContext) {
|
||||
deserializeWebviewPanel(panel: vscode.WebviewPanel) {
|
||||
const tabProvider = new KiloProvider(context.extensionUri, connectionService, context, {
|
||||
tabTitle: panelTitleHandler(panel),
|
||||
topBarSurface: "tab",
|
||||
})
|
||||
tabProvider.setRemoteService(remoteService)
|
||||
tabProvider.setAutoApproveController(autoApprove)
|
||||
@@ -333,39 +339,8 @@ export function activate(context: vscode.ExtensionContext) {
|
||||
}),
|
||||
)
|
||||
|
||||
// Sidebar menus use wrapper commands so this event measures real title button presses,
|
||||
// not programmatic opens, shortcuts, or editor title commands.
|
||||
const track = (button: string, command: string) => {
|
||||
TelemetryProxy.capture(TelemetryEventName.TITLE_BUTTON_CLICKED, {
|
||||
button,
|
||||
surface: "sidebar_title",
|
||||
})
|
||||
void vscode.commands.executeCommand(command)
|
||||
}
|
||||
|
||||
// Register toolbar button command handlers
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand("kilo-code.new.sidebarTitle.plusButtonClicked", () => {
|
||||
track("new_task", "kilo-code.new.plusButtonClicked")
|
||||
}),
|
||||
vscode.commands.registerCommand("kilo-code.new.sidebarTitle.historyButtonClicked", () => {
|
||||
track("history", "kilo-code.new.historyButtonClicked")
|
||||
}),
|
||||
vscode.commands.registerCommand("kilo-code.new.sidebarTitle.agentManagerOpen", () => {
|
||||
track("agent_manager", "kilo-code.new.agentManagerOpen")
|
||||
}),
|
||||
vscode.commands.registerCommand("kilo-code.new.sidebarTitle.kiloClawOpen", () => {
|
||||
track("kiloclaw", "kilo-code.new.kiloClawOpen")
|
||||
}),
|
||||
vscode.commands.registerCommand("kilo-code.new.sidebarTitle.marketplaceButtonClicked", () => {
|
||||
track("marketplace", "kilo-code.new.marketplaceButtonClicked")
|
||||
}),
|
||||
vscode.commands.registerCommand("kilo-code.new.sidebarTitle.profileButtonClicked", () => {
|
||||
track("profile", "kilo-code.new.profileButtonClicked")
|
||||
}),
|
||||
vscode.commands.registerCommand("kilo-code.new.sidebarTitle.settingsButtonClicked", () => {
|
||||
track("settings", "kilo-code.new.settingsButtonClicked")
|
||||
}),
|
||||
vscode.commands.registerCommand("kilo-code.new.plusButtonClicked", () => {
|
||||
const tab = activeTabProvider()
|
||||
if (tab) tab.postMessage({ type: "action", action: "plusButtonClicked" })
|
||||
@@ -476,6 +451,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" })
|
||||
}),
|
||||
@@ -627,6 +608,7 @@ function openKiloInNewTab(
|
||||
|
||||
const tabProvider = new KiloProvider(context.extensionUri, connectionService, context, {
|
||||
tabTitle: panelTitleHandler(panel),
|
||||
topBarSurface: "tab",
|
||||
})
|
||||
tabProvider.setRemoteService(remoteService)
|
||||
tabProvider.setAutoApproveController(autoApprove)
|
||||
|
||||
@@ -32,4 +32,13 @@ export type KiloProviderOptions = {
|
||||
* is ambiguous.
|
||||
*/
|
||||
projectQualifier?: () => { projectId: string } | undefined
|
||||
/**
|
||||
* Hides the in-webview sidebar top bar (New Task, History, Agent Manager,
|
||||
* etc.) for dedicated single-purpose panels — Settings, Profile, and the
|
||||
* Sub-Agent Viewer — where it doesn't apply and would let users navigate
|
||||
* away from the panel's one job. Sidebar and "Open in Tab" leave this unset.
|
||||
*/
|
||||
hideTopBar?: boolean
|
||||
/** Reports "Open in Tab" as the top bar's telemetry surface instead of the sidebar default. */
|
||||
topBarSurface?: "tab"
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ interface Ctx {
|
||||
openAgentManager: () => Thenable<unknown>
|
||||
openAdvancedWorktree: () => Thenable<unknown>
|
||||
openChanges: (sessionId?: string, turnId?: string) => Thenable<unknown>
|
||||
openProfile: () => Thenable<unknown>
|
||||
currentSessionId?: string
|
||||
createWorktree?: (baseBranch?: string, branchName?: string) => Promise<void>
|
||||
continueInWorktree?: (
|
||||
@@ -59,6 +60,11 @@ export async function handleSidebarWorktreeMessage(message: Msg, ctx: Ctx) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (message.type === "openProfilePanel") {
|
||||
await ctx.openProfile()
|
||||
return true
|
||||
}
|
||||
|
||||
if (message.type !== "continueInWorktree") return false
|
||||
handleContinueInWorktree({
|
||||
sessionId: message.sessionId,
|
||||
|
||||
@@ -41,6 +41,9 @@ export function buildWebviewHtml(
|
||||
title: string
|
||||
port?: number
|
||||
extraStyles?: string
|
||||
/** Sidebar top bar visibility and telemetry surface for the shared webview bundle (App.tsx). Unused by the Agent Manager bundle. */
|
||||
topBar?: boolean
|
||||
topBarSurface?: string
|
||||
},
|
||||
): string {
|
||||
const nonce = getNonce()
|
||||
@@ -83,7 +86,7 @@ export function buildWebviewHtml(
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script nonce="${nonce}">window.ICONS_BASE_URI = "${opts.iconsBaseUri}"; window.KILO_SHIKI_WORKER_URI = "${opts.workerUri}"; window.KILO_MARKDOWN_SHIKI_WORKER_URI = "${markdownWorkerUri}";</script>
|
||||
<script nonce="${nonce}">window.ICONS_BASE_URI = "${opts.iconsBaseUri}"; window.KILO_SHIKI_WORKER_URI = "${opts.workerUri}"; window.KILO_MARKDOWN_SHIKI_WORKER_URI = "${markdownWorkerUri}"; window.KILO_TOP_BAR = ${opts.topBar !== false}; window.KILO_TOP_BAR_SURFACE = "${opts.topBarSurface ?? "sidebar_title"}";</script>
|
||||
<script nonce="${nonce}" src="${opts.scriptUri}"></script>
|
||||
</body>
|
||||
</html>`
|
||||
|
||||
@@ -379,6 +379,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"')
|
||||
|
||||
@@ -5,6 +5,7 @@ import { join } from "node:path"
|
||||
const root = join(__dirname, "..", "..")
|
||||
const dialog = readFileSync(join(root, "webview-ui", "agent-manager", "NewWorktreeDialog.tsx"), "utf8")
|
||||
const app = readFileSync(join(root, "webview-ui", "agent-manager", "AgentManagerApp.tsx"), "utf8")
|
||||
const pending = readFileSync(join(root, "webview-ui", "agent-manager", "pending-create.ts"), "utf8")
|
||||
const importer = readFileSync(join(root, "src", "agent-manager", "worktree-importer.ts"), "utf8")
|
||||
const css = readFileSync(join(root, "webview-ui", "agent-manager", "agent-manager.css"), "utf8")
|
||||
|
||||
@@ -20,9 +21,10 @@ describe("Agent Manager New Worktree project targeting", () => {
|
||||
})
|
||||
|
||||
it("does not replace a pending cross-project activation", () => {
|
||||
expect(app).toContain("if (pendingCreate()) return")
|
||||
expect(pending).toContain("if (pending()) return")
|
||||
expect(app).toContain("usePendingCreate(activeProjectId")
|
||||
expect(app).toContain('msg.type === "agentManager.importResult"')
|
||||
expect(app).toContain("!msg.success && pendingCreate()?.projectId === msg.projectId")
|
||||
expect(app).toContain("!msg.success) creation.abandon(msg.projectId)")
|
||||
})
|
||||
|
||||
it("tags branch and import responses with their owning project", () => {
|
||||
|
||||
@@ -12,6 +12,8 @@ function scene(
|
||||
saved?: "vscode" | "agentManager"
|
||||
visible?: boolean
|
||||
focusedId?: string
|
||||
count?: number
|
||||
script?: boolean
|
||||
mac?: boolean
|
||||
} = {},
|
||||
) {
|
||||
@@ -33,6 +35,7 @@ function scene(
|
||||
requestSide: () => {
|
||||
calls.requestSide++
|
||||
visible = true
|
||||
focusedId ??= "terminal:side"
|
||||
},
|
||||
ensureSide: () => calls.ensureSide++,
|
||||
closeSide: (terminalId) => {
|
||||
@@ -43,6 +46,8 @@ function scene(
|
||||
},
|
||||
visible: () => visible,
|
||||
focusedId: () => focusedId,
|
||||
count: () => opts.count ?? 2,
|
||||
isScript: () => opts.script ?? false,
|
||||
hide: () => {
|
||||
calls.hide++
|
||||
visible = false
|
||||
@@ -60,7 +65,7 @@ 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)
|
||||
@@ -68,7 +73,8 @@ describe("Agent Manager side terminal controller", () => {
|
||||
|
||||
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 })
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -18,6 +18,7 @@ function scene(initial: string | null = LOCAL) {
|
||||
const events = {
|
||||
activated: [] as string[],
|
||||
selected: [] as string[],
|
||||
cleared: 0,
|
||||
saved: 0,
|
||||
shown: [] as string[],
|
||||
errors: 0,
|
||||
@@ -29,7 +30,7 @@ function scene(initial: string | null = LOCAL) {
|
||||
tabIds: tabs,
|
||||
selectReview: () => undefined,
|
||||
selectSessionTab: () => undefined,
|
||||
clearSession: () => undefined,
|
||||
clearSession: () => events.cleared++,
|
||||
resetOthers: () => undefined,
|
||||
isPendingId: () => false,
|
||||
findTab: () => undefined,
|
||||
@@ -401,6 +402,69 @@ describe("Agent Manager terminal state", () => {
|
||||
})
|
||||
})
|
||||
|
||||
it("cycles side terminals in both directions and wraps", () => {
|
||||
createRoot((dispose) => {
|
||||
const item = scene()
|
||||
item.state.add(null, { id: "terminal:one", title: "Terminal 1", wsUrl: "ws://one", font, placement: "side" })
|
||||
item.state.add(null, { id: "terminal:two", title: "Terminal 2", wsUrl: "ws://two", font, placement: "side" })
|
||||
item.state.setSideActive(LOCAL, "terminal:one")
|
||||
|
||||
expect(item.handlers.cycle("next", "side")).toBe(true)
|
||||
expect(item.state.sideActiveFor(LOCAL)).toBe("terminal:two")
|
||||
expect(item.state.focusRequest()?.id).toBe("terminal:two")
|
||||
expect(item.handlers.cycle("next", "side")).toBe(true)
|
||||
expect(item.state.sideActiveFor(LOCAL)).toBe("terminal:one")
|
||||
expect(item.handlers.cycle("previous", "side")).toBe(true)
|
||||
expect(item.state.sideActiveFor(LOCAL)).toBe("terminal:two")
|
||||
dispose()
|
||||
})
|
||||
})
|
||||
|
||||
it("cycles main terminal tabs independently from side terminals", () => {
|
||||
createRoot((dispose) => {
|
||||
const item = scene()
|
||||
item.state.add(null, { id: "terminal:one", title: "Terminal 1", wsUrl: "ws://one", font, placement: "tab" })
|
||||
item.state.add(null, { id: "terminal:two", title: "Terminal 2", wsUrl: "ws://two", font, placement: "tab" })
|
||||
item.state.setActiveId("terminal:one")
|
||||
|
||||
expect(item.handlers.cycle("next", "tab")).toBe(true)
|
||||
expect(item.state.activeId()).toBe("terminal:two")
|
||||
expect(item.handlers.cycle("next", "tab")).toBe(true)
|
||||
expect(item.state.activeId()).toBe("terminal:one")
|
||||
dispose()
|
||||
})
|
||||
})
|
||||
|
||||
it("starts terminal cycling at the boundary when no terminal is active", () => {
|
||||
createRoot((dispose) => {
|
||||
const item = scene()
|
||||
item.state.add(null, { id: "terminal:one", title: "Terminal 1", wsUrl: "ws://one", font, placement: "tab" })
|
||||
item.state.add(null, { id: "terminal:two", title: "Terminal 2", wsUrl: "ws://two", font, placement: "tab" })
|
||||
item.state.setActiveId(undefined)
|
||||
|
||||
expect(item.handlers.cycle("next", "tab")).toBe(true)
|
||||
expect(item.state.activeId()).toBe("terminal:one")
|
||||
item.state.setActiveId(undefined)
|
||||
expect(item.handlers.cycle("previous", "tab")).toBe(true)
|
||||
expect(item.state.activeId()).toBe("terminal:two")
|
||||
dispose()
|
||||
})
|
||||
})
|
||||
|
||||
it("keeps the session open when its last main terminal closes", () => {
|
||||
createRoot((dispose) => {
|
||||
const item = scene()
|
||||
item.state.add(null, { id: "terminal:one", title: "Terminal 1", wsUrl: "ws://one", font, placement: "tab" })
|
||||
item.state.setActiveId("terminal:one")
|
||||
item.state.setFocusedId("terminal:one")
|
||||
|
||||
expect(item.handlers.closeFocused()).toBe(true)
|
||||
expect(item.state.current()).toEqual([])
|
||||
expect(item.events.cleared).toBe(0)
|
||||
dispose()
|
||||
})
|
||||
})
|
||||
|
||||
it("moves activation to the last remaining side terminal on close", () => {
|
||||
createRoot((dispose) => {
|
||||
const item = scene()
|
||||
@@ -410,6 +474,7 @@ describe("Agent Manager terminal state", () => {
|
||||
|
||||
expect(item.handlers.closeSide("terminal:two")).toBe(true)
|
||||
expect(item.state.sideActiveFor(LOCAL)).toBe("terminal:one")
|
||||
expect(item.state.focusRequest()?.id).toBe("terminal:one")
|
||||
expect(item.posted).toEqual([{ type: "agentManager.terminal.close", terminalId: "terminal:two" }])
|
||||
|
||||
expect(item.handlers.closeSide("terminal:one")).toBe(true)
|
||||
|
||||
@@ -39,6 +39,11 @@ describe("formatKeybinding", () => {
|
||||
it("formats plain key", () => {
|
||||
expect(formatKeybinding("cmd+/", true)).toBe("⌘/")
|
||||
})
|
||||
|
||||
it("formats bracket keys", () => {
|
||||
expect(formatKeybinding("cmd+shift+[", true)).toBe("⌘⇧[")
|
||||
expect(formatKeybinding("cmd+shift+]", true)).toBe("⌘⇧]")
|
||||
})
|
||||
})
|
||||
|
||||
describe("windows/linux", () => {
|
||||
@@ -77,4 +82,11 @@ describe("buildKeybindingMap", () => {
|
||||
expect(buildKeybindingMap(bindings, true).search).toBe("⌘F")
|
||||
expect(buildKeybindingMap(bindings, false).search).toBe("Ctrl+F")
|
||||
})
|
||||
|
||||
it("provides terminal navigation fallbacks", () => {
|
||||
expect(buildKeybindingMap([], true).previousTerminal).toBe("⌘⇧[")
|
||||
expect(buildKeybindingMap([], true).nextTerminal).toBe("⌘⇧]")
|
||||
expect(buildKeybindingMap([], false).previousTerminal).toBe("Ctrl+Shift+[")
|
||||
expect(buildKeybindingMap([], false).nextTerminal).toBe("Ctrl+Shift+]")
|
||||
})
|
||||
})
|
||||
|
||||
@@ -13,8 +13,15 @@ describe("modelPatch", () => {
|
||||
})
|
||||
})
|
||||
|
||||
it("clears stale variant when next model does not support it", () => {
|
||||
it("keeps the nearest supported effort when the exact variant is unavailable", () => {
|
||||
expect(modelPatch("kilo", "anthropic/claude-sonnet-4-6", ["low", "medium"], "high")).toEqual({
|
||||
model: "kilo/anthropic/claude-sonnet-4-6",
|
||||
variant: "medium",
|
||||
})
|
||||
})
|
||||
|
||||
it("clears an unknown variant when next model does not support it", () => {
|
||||
expect(modelPatch("kilo", "anthropic/claude-sonnet-4-6", ["low", "medium"], "thinking")).toEqual({
|
||||
model: "kilo/anthropic/claude-sonnet-4-6",
|
||||
variant: null,
|
||||
})
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
cycleVariant,
|
||||
getAgentVariant,
|
||||
getVariant,
|
||||
preserveVariant,
|
||||
sessionVariantKeys,
|
||||
sessionVariants,
|
||||
transferVariants,
|
||||
@@ -121,3 +122,24 @@ describe("cycleVariant", () => {
|
||||
expect(cycleVariant("low", [])).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe("preserveVariant", () => {
|
||||
it("keeps an exact variant", () => {
|
||||
expect(preserveVariant("high", ["low", "high"])).toBe("high")
|
||||
expect(preserveVariant("thinking", ["instant", "thinking"])).toBe("thinking")
|
||||
expect(preserveVariant("default", ["default", "thinking"])).toBe("default")
|
||||
})
|
||||
|
||||
it("falls back to the nearest supported effort", () => {
|
||||
expect(preserveVariant("max", ["high", "xhigh"])).toBe("xhigh")
|
||||
expect(preserveVariant("high", ["low", "medium"])).toBe("medium")
|
||||
expect(preserveVariant("max", ["none", "low"])).toBe("low")
|
||||
})
|
||||
|
||||
it("does not cross binary or custom variant families", () => {
|
||||
expect(preserveVariant("thinking", ["low", "high"])).toBeUndefined()
|
||||
expect(preserveVariant("instant", ["low", "high"])).toBeUndefined()
|
||||
expect(preserveVariant("turbo", ["low", "high"])).toBeUndefined()
|
||||
expect(preserveVariant("high", ["instant", "thinking"])).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -27,7 +27,6 @@ import type {
|
||||
AgentManagerWorktreeDiffLoadingMessage,
|
||||
AgentManagerWorktreeDiffNoticeMessage,
|
||||
AgentManagerDiffBranchesMessage,
|
||||
AgentManagerImportResultMessage,
|
||||
AgentManagerApplyWorktreeDiffResultMessage,
|
||||
AgentManagerWorktreeStatsMessage,
|
||||
AgentManagerLocalStatsMessage,
|
||||
@@ -181,6 +180,7 @@ import { clampPanelWidth, createPanelResize, maxPanelWidth, minPanelWidth } from
|
||||
import { buildShortcutCategories } from "./shortcuts"
|
||||
import { tracker } from "./telemetry"
|
||||
import { createChatFocus, hasQuestionOption } from "./focus"
|
||||
import { usePendingCreate } from "./pending-create"
|
||||
import "./agent-manager.css"
|
||||
import "./agent-manager-review.css"
|
||||
import { cycleAgent as cycle } from "../src/context/session-agent"
|
||||
@@ -207,6 +207,8 @@ const defaultBindings: Record<string, string> = {
|
||||
nextSession: isMac ? "⌘⌥↓" : "Ctrl+Alt+↓",
|
||||
previousTab: isMac ? "⌘⌥←" : "Ctrl+Alt+←",
|
||||
nextTab: isMac ? "⌘⌥→" : "Ctrl+Alt+→",
|
||||
previousTerminal: isMac ? "⌘⇧[" : "Ctrl+Shift+[",
|
||||
nextTerminal: isMac ? "⌘⇧]" : "Ctrl+Shift+]",
|
||||
search: isMac ? "⌘F" : "Ctrl+F",
|
||||
showTerminal: isMac ? "⌘/" : "Ctrl+/",
|
||||
newTerminal: isMac ? "⌘⇧T" : "Ctrl+Shift+T",
|
||||
@@ -267,12 +269,12 @@ const AgentManagerContent: Component = () => {
|
||||
const [currentProjectId, setCurrentProjectId] = createSignal<string | undefined>()
|
||||
const [projectStates, setProjectStates] = createSignal<Record<string, AgentManagerStateMessage>>({})
|
||||
const activeProjectId = () => projectList().find((p) => p.active)?.id ?? currentProjectId()
|
||||
const [pendingCreate, setPendingCreate] = createSignal<{ projectId: string }>()
|
||||
const scheduleCreate = (projectId: string) => {
|
||||
if (projectId === activeProjectId()) return
|
||||
if (pendingCreate()) return
|
||||
setPendingCreate({ projectId })
|
||||
}
|
||||
const creation = usePendingCreate(activeProjectId, (projectId, worktreeId) =>
|
||||
vscode.postMessage({
|
||||
type: "agentManager.activateSelection",
|
||||
target: { projectId, kind: "worktree", worktreeId },
|
||||
}),
|
||||
)
|
||||
const isActivePayload = (pid: string | undefined) =>
|
||||
projectList().length === 0 || pid === undefined || pid === activeProjectId()
|
||||
|
||||
@@ -405,9 +407,16 @@ const AgentManagerContent: Component = () => {
|
||||
{ defer: true },
|
||||
),
|
||||
)
|
||||
|
||||
type FocusOwner = "prompt" | { terminal: string }
|
||||
const focusMemory = new Map<string, FocusOwner>()
|
||||
let focusInputUntil = 0
|
||||
const focusPrompt = () => {
|
||||
focusInputUntil = Date.now() + 500
|
||||
if (document.activeElement instanceof HTMLElement) document.activeElement.blur()
|
||||
terms.setActiveId(undefined)
|
||||
terms.setFocusedId(undefined)
|
||||
requestChatFocus(true)
|
||||
}
|
||||
const focusKey = () => {
|
||||
const context = terms.sideKey()
|
||||
const sessionID = session.currentSessionID() ?? activePendingId() ?? "new"
|
||||
@@ -439,6 +448,7 @@ const AgentManagerContent: Component = () => {
|
||||
return terminalVisible() ? false : true
|
||||
}
|
||||
const restoreFocus = () => {
|
||||
if (Date.now() < focusInputUntil) return
|
||||
const key = focusKey()
|
||||
const owner = focusMemory.get(key)
|
||||
if (owner && owner !== "prompt") {
|
||||
@@ -1175,6 +1185,8 @@ const AgentManagerContent: Component = () => {
|
||||
else if (msg.action === "sessionNext") projectNav.step("down")
|
||||
else if (msg.action === "tabPrevious") navigateTab("left")
|
||||
else if (msg.action === "tabNext") navigateTab("right")
|
||||
else if (msg.action === "terminalPrevious") cycleTerminal("previous")
|
||||
else if (msg.action === "terminalNext") cycleTerminal("next")
|
||||
else if (msg.action === "search") {
|
||||
if (!sidebarCollapsed()) sidebarSearchMenu?.open()
|
||||
else {
|
||||
@@ -1198,11 +1210,13 @@ 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") requestChatFocus(true)
|
||||
else if (msg.action === "focusInput") focusPrompt()
|
||||
else if (msg.action === "focusSearch")
|
||||
focusChatSearch({ history: setHistory, review: setReviewActive, terminal: () => terms.setActiveId(undefined) })
|
||||
else if (msg.action === "newTerminal") termHandlers.requestNew()
|
||||
else if (msg.action === "cycleAgentMode" && document.hasFocus()) {
|
||||
else if (msg.action === "newTerminal") {
|
||||
if (terms.sideFocusedId()) termHandlers.addSide()
|
||||
else termHandlers.requestNew()
|
||||
} 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)
|
||||
@@ -1213,7 +1227,6 @@ const AgentManagerContent: Component = () => {
|
||||
}
|
||||
}
|
||||
window.addEventListener("message", handler)
|
||||
|
||||
// Prevent Cmd/Ctrl shortcuts from triggering native browser actions
|
||||
const preventDefaults = (e: KeyboardEvent) => {
|
||||
if (!(e.metaKey || e.ctrlKey)) return
|
||||
@@ -1227,8 +1240,9 @@ const AgentManagerContent: Component = () => {
|
||||
if (["t", "w", "n", "d", "e", "f"].includes(e.key.toLowerCase()) && !e.shiftKey) {
|
||||
e.preventDefault()
|
||||
}
|
||||
// Prevent defaults for shift variants (close worktree, advanced/new/open worktree, open PR)
|
||||
if (["w", "n", "o", "r"].includes(e.key.toLowerCase()) && e.shiftKey) {
|
||||
// Prevent browser defaults for shift variants (new 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()
|
||||
}
|
||||
// Prevent browser defaults for shortcuts help (Cmd/Ctrl+Shift+/)
|
||||
@@ -1377,15 +1391,7 @@ const AgentManagerContent: Component = () => {
|
||||
|
||||
if (msg.type === "agentManager.worktreeSetup") {
|
||||
const ev = msg as AgentManagerWorktreeSetupMessage
|
||||
const pending = pendingCreate()
|
||||
if (ev.status === "ready" && ev.projectId && pending?.projectId === ev.projectId && ev.worktreeId) {
|
||||
setPendingCreate(undefined)
|
||||
vscode.postMessage({
|
||||
type: "agentManager.activateSelection",
|
||||
target: { projectId: ev.projectId, kind: "worktree", worktreeId: ev.worktreeId },
|
||||
})
|
||||
}
|
||||
if (ev.status === "error" && pending?.projectId === ev.projectId) setPendingCreate(undefined)
|
||||
creation.setup(ev)
|
||||
const store = ev.projectId ? registry.ensure(ev.projectId) : registry.active()
|
||||
const updateBusy: Setter<Map<string, WorktreeBusyState>> = (value) => store.setBusy(value)
|
||||
if (ev.status === "ready" || ev.status === "error") {
|
||||
@@ -1427,8 +1433,7 @@ const AgentManagerContent: Component = () => {
|
||||
}
|
||||
}
|
||||
|
||||
if (msg.type === "agentManager.importResult" && !msg.success && pendingCreate()?.projectId === msg.projectId)
|
||||
setPendingCreate(undefined)
|
||||
if (msg.type === "agentManager.importResult" && !msg.success) creation.abandon(msg.projectId)
|
||||
|
||||
if (msg.type === "agentManager.sessionAdded") {
|
||||
const ev = msg as { type: string; sessionId: string; worktreeId: string }
|
||||
@@ -1471,7 +1476,7 @@ const AgentManagerContent: Component = () => {
|
||||
// When a multi-version progress update arrives, mark newly created worktrees as loading
|
||||
if ((msg as { type: string }).type === "agentManager.multiVersionProgress") {
|
||||
const ev = msg as unknown as AgentManagerMultiVersionProgressMessage
|
||||
if (ev.status === "done" && pendingCreate()?.projectId === ev.projectId) setPendingCreate(undefined)
|
||||
if (ev.status === "done") creation.abandon(ev.projectId)
|
||||
if (ev.status === "done" && ev.groupId) {
|
||||
// Clear busy state for all worktrees in this group
|
||||
const store = ev.projectId ? registry.ensure(ev.projectId) : registry.active()
|
||||
@@ -1897,7 +1902,7 @@ const AgentManagerContent: Component = () => {
|
||||
projects={multiProject() ? projectList : undefined}
|
||||
activeProjectId={activeProjectId()}
|
||||
defaultBase={defaultBase}
|
||||
onCreate={scheduleCreate}
|
||||
onCreate={creation.schedule}
|
||||
/>
|
||||
))
|
||||
}
|
||||
@@ -2107,6 +2112,8 @@ const AgentManagerContent: Component = () => {
|
||||
handlers: termHandlers,
|
||||
visible: () => sidePanel() === "terminal" && !history() && !reviewActive(),
|
||||
focusedId: () => terms.sideFocusedId(),
|
||||
count: () => terms.sidesForContext(terms.sideKey()).length,
|
||||
isScript: terms.isScript,
|
||||
hide: () => {
|
||||
cancelAmbientSetup()
|
||||
setSidePanel(null)
|
||||
@@ -2225,16 +2232,24 @@ const AgentManagerContent: Component = () => {
|
||||
})
|
||||
}
|
||||
const tabFocus = createTabFocus({ ids: () => tabIds(), select: focusTab })
|
||||
const cycleTerminal = (direction: "previous" | "next") => {
|
||||
const focused = terms.focusedId()
|
||||
const placement = terms.sideFocusedId() || (!focused && terminalVisible()) ? "side" : "tab"
|
||||
return termHandlers.cycle(direction, placement)
|
||||
}
|
||||
|
||||
// Close the currently active tab via keyboard shortcut.
|
||||
// If no tabs remain, fall through to close the selected worktree.
|
||||
const closeActiveTab = () => {
|
||||
// A focused side terminal owns Cmd+W while its panel is visible —
|
||||
// closing a chat tab out from under the user's cursor would be
|
||||
// surprising. Only that terminal dies; the panel keeps the rest.
|
||||
// A focused side terminal owns Cmd+W while its panel is visible.
|
||||
// Closing a chat tab out from under the user's cursor would be surprising.
|
||||
if (sidePanel() === "terminal" && terms.sideFocusedId()) {
|
||||
if (sideCtl.close()) return
|
||||
}
|
||||
if (termHandlers.closeFocused()) {
|
||||
tabFocus.restore()
|
||||
return
|
||||
}
|
||||
if (termHandlers.closeActive()) {
|
||||
tabFocus.restore()
|
||||
return
|
||||
@@ -2376,7 +2391,7 @@ const AgentManagerContent: Component = () => {
|
||||
currentSessionID={session.currentSessionID}
|
||||
mode={mode}
|
||||
defaultBase={defaultBase}
|
||||
onCreate={scheduleCreate}
|
||||
onCreate={creation.schedule}
|
||||
bindings={kb()}
|
||||
t={t}
|
||||
onSearchRef={(ref) => (sidebarSearchMenu = ref)}
|
||||
@@ -2485,6 +2500,9 @@ const AgentManagerContent: Component = () => {
|
||||
onToggleReview={metrics.click("fullscreen_review", "tab_toolbar", toggleReviewTab)}
|
||||
terminalDestination={sideCtl.destination}
|
||||
terminalDestinationActive={() => sidePanel() === "terminal"}
|
||||
terminalDestinationFocused={() =>
|
||||
sideCtl.destination() === "agentManager" && terms.sideFocusedId() !== undefined
|
||||
}
|
||||
terminalKeybind={() => kb().showTerminal ?? ""}
|
||||
onTerminalDestinationOpen={() => {
|
||||
cancelAmbientSetup()
|
||||
@@ -2553,7 +2571,7 @@ 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 })}
|
||||
{renderTerminalLayer({ state: terms, onFocusPrompt: focusPrompt })}
|
||||
{/* 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. */}
|
||||
@@ -2711,6 +2729,9 @@ const AgentManagerContent: Component = () => {
|
||||
state={terms}
|
||||
contextKey={terms.sideKey}
|
||||
visible={() => sidePanel() === "terminal"}
|
||||
nextKeybind={kb().nextTerminal ?? ""}
|
||||
closeKeybind={kb().closeTab ?? ""}
|
||||
onFocusPrompt={focusPrompt}
|
||||
onSelect={(id) => termHandlers.selectSide(id)}
|
||||
onClose={(id) => {
|
||||
cancelAmbientSetup()
|
||||
|
||||
@@ -23,7 +23,7 @@ import { useServer } from "../src/context/server"
|
||||
import { useSession } from "../src/context/session"
|
||||
import { useProvider } from "../src/context/provider"
|
||||
import { useConfig } from "../src/context/config"
|
||||
import { cycleVariant } from "../src/context/session-variant-store"
|
||||
import { cycleVariant, preserveVariant } from "../src/context/session-variant-store"
|
||||
import { ModelSelectorBase } from "../src/components/shared/ModelSelector"
|
||||
import { ModeSwitcherBase } from "../src/components/shared/ModeSwitcher"
|
||||
import { SpeechToTextButton } from "../src/components/speech-to-text/SpeechToTextButton"
|
||||
@@ -268,7 +268,7 @@ export const NewWorktreeDialog: Component<{
|
||||
return
|
||||
}
|
||||
const stored = variant()
|
||||
if (!stored || !list.includes(stored)) setVariant(list[0])
|
||||
if (!stored || !list.includes(stored)) setVariant(preserveVariant(stored, list) ?? list[0])
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
@@ -871,7 +871,12 @@ export const NewWorktreeDialog: Component<{
|
||||
<ModelSelectorBase
|
||||
value={model()}
|
||||
onSelect={(pid, mid) => {
|
||||
if (pid && mid) setModel({ providerID: pid, modelID: mid })
|
||||
if (!pid || !mid) return
|
||||
const current = effectiveVariant()
|
||||
const next = { providerID: pid, modelID: mid }
|
||||
const list = Object.keys(provider.findModel(next)?.variants ?? {})
|
||||
setModel(next)
|
||||
setVariant(preserveVariant(current, list))
|
||||
}}
|
||||
onPick={restorePrompt}
|
||||
onCancel={restorePrompt}
|
||||
|
||||
@@ -57,6 +57,7 @@ export interface TabBarProps {
|
||||
onToggleReview: () => void
|
||||
terminalDestination: () => TerminalDestination
|
||||
terminalDestinationActive: () => boolean
|
||||
terminalDestinationFocused: () => boolean
|
||||
terminalKeybind: () => string
|
||||
onTerminalDestinationOpen: () => void
|
||||
onTerminalDestinationChoose: (destination: TerminalDestination) => void
|
||||
@@ -253,12 +254,13 @@ export const TabBar: Component<TabBarProps> = (props) => (
|
||||
</Tooltip>
|
||||
</Show>
|
||||
{/* 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 still creates an xterm tab via the `+` menu. */}
|
||||
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. */}
|
||||
<TerminalDestinationButton
|
||||
destination={props.terminalDestination}
|
||||
active={props.terminalDestinationActive}
|
||||
focused={props.terminalDestinationFocused}
|
||||
keybind={props.terminalKeybind}
|
||||
onOpen={props.onTerminalDestinationOpen}
|
||||
onChoose={props.onTerminalDestinationChoose}
|
||||
|
||||
@@ -4754,6 +4754,10 @@ body.vscode-high-contrast-light {
|
||||
outline: 1px solid var(--vscode-contrastActiveBorder, var(--vscode-focusBorder));
|
||||
outline-offset: -1px;
|
||||
}
|
||||
|
||||
.am-terminal-host:focus-within {
|
||||
box-shadow: inset 0 0 0 1px var(--vscode-contrastActiveBorder, var(--vscode-focusBorder));
|
||||
}
|
||||
}
|
||||
|
||||
/* Context menu — restyle to match dropdown-menu visuals.
|
||||
@@ -4799,6 +4803,10 @@ body.vscode-high-contrast-light {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.am-tab-terminal-focused {
|
||||
background: var(--surface-base-hover);
|
||||
}
|
||||
|
||||
/*
|
||||
* Stacking layout for xterm terminal tabs.
|
||||
*
|
||||
@@ -5001,6 +5009,10 @@ body.vscode-high-contrast-light {
|
||||
background: var(--vscode-terminal-background, #1e1e1e);
|
||||
}
|
||||
|
||||
.am-terminal-host:focus-within {
|
||||
box-shadow: inset 0 0 0 1px var(--border-focus, var(--vscode-focusBorder));
|
||||
}
|
||||
|
||||
/* Third-party xterm classes — addressed by attribute selector so the
|
||||
agent-manager "am-* prefix" architecture test does not flag them.
|
||||
FitAddon subtracts padding from xterm itself, not its parent host. */
|
||||
|
||||
@@ -12,7 +12,7 @@ export function createChatFocus(deps: {
|
||||
review: () => boolean
|
||||
}) {
|
||||
const focus = (force: boolean) => {
|
||||
if ((!force && !document.hasFocus()) || deps.term() || deps.history() || deps.review()) return
|
||||
if ((!force && (!document.hasFocus() || deps.term())) || deps.history() || deps.review()) return
|
||||
if (preservesTextFocus(document.activeElement)) return
|
||||
if (!force && document.activeElement?.matches('[role="tab"]')) return
|
||||
if (!force && document.activeElement?.closest('[data-component="question-dock"]')) return
|
||||
|
||||
+1
-1
@@ -99,7 +99,7 @@ export const dict = {
|
||||
"agentManager.shortcuts.nextTab": "علامة التبويب التالية",
|
||||
"agentManager.shortcuts.newTab": "علامة تبويب جديدة",
|
||||
"agentManager.shortcuts.closeTab": "إغلاق علامة التبويب",
|
||||
"agentManager.shortcuts.toggleTerminal": "تبديل الطرفية",
|
||||
"agentManager.shortcuts.toggleTerminal": "التركيز على الطرفية / إخفاء الطرفية",
|
||||
"agentManager.shortcuts.runScript": "تشغيل السكربت",
|
||||
"agentManager.run.options": "خيارات التشغيل",
|
||||
"agentManager.run.configure": "تكوين سكربت التشغيل",
|
||||
|
||||
+1
-1
@@ -101,7 +101,7 @@ export const dict = {
|
||||
"agentManager.shortcuts.nextTab": "Próxima aba",
|
||||
"agentManager.shortcuts.newTab": "Nova aba",
|
||||
"agentManager.shortcuts.closeTab": "Fechar aba",
|
||||
"agentManager.shortcuts.toggleTerminal": "Alternar terminal",
|
||||
"agentManager.shortcuts.toggleTerminal": "Focar / ocultar terminal",
|
||||
"agentManager.shortcuts.runScript": "Executar script",
|
||||
"agentManager.run.options": "Opções de execução",
|
||||
"agentManager.run.configure": "Configurar script de execução",
|
||||
|
||||
+1
-1
@@ -101,7 +101,7 @@ export const dict = {
|
||||
"agentManager.shortcuts.nextTab": "Sljedeća kartica",
|
||||
"agentManager.shortcuts.newTab": "Nova kartica",
|
||||
"agentManager.shortcuts.closeTab": "Zatvori karticu",
|
||||
"agentManager.shortcuts.toggleTerminal": "Prebaci terminal",
|
||||
"agentManager.shortcuts.toggleTerminal": "Fokusiraj / sakrij terminal",
|
||||
"agentManager.shortcuts.runScript": "Pokreni skriptu",
|
||||
"agentManager.run.options": "Opcije pokretanja",
|
||||
"agentManager.run.configure": "Konfiguriši skriptu za pokretanje",
|
||||
|
||||
+1
-1
@@ -102,7 +102,7 @@ export const dict = {
|
||||
"agentManager.shortcuts.nextTab": "Næste fane",
|
||||
"agentManager.shortcuts.newTab": "Ny fane",
|
||||
"agentManager.shortcuts.closeTab": "Luk fane",
|
||||
"agentManager.shortcuts.toggleTerminal": "Skift terminal",
|
||||
"agentManager.shortcuts.toggleTerminal": "Fokusér / skjul terminalen",
|
||||
"agentManager.shortcuts.runScript": "Kør script",
|
||||
"agentManager.run.options": "Kørselsindstillinger",
|
||||
"agentManager.run.configure": "Konfigurer kørselsscript",
|
||||
|
||||
@@ -103,7 +103,7 @@ export const dict = {
|
||||
"agentManager.shortcuts.nextTab": "Nächster Tab",
|
||||
"agentManager.shortcuts.newTab": "Neuer Tab",
|
||||
"agentManager.shortcuts.closeTab": "Tab schließen",
|
||||
"agentManager.shortcuts.toggleTerminal": "Terminal umschalten",
|
||||
"agentManager.shortcuts.toggleTerminal": "Terminal fokussieren / ausblenden",
|
||||
"agentManager.shortcuts.runScript": "Skript ausführen",
|
||||
"agentManager.run.options": "Ausführungsoptionen",
|
||||
"agentManager.run.configure": "Ausführungsskript konfigurieren",
|
||||
|
||||
@@ -105,7 +105,7 @@ export const dict = {
|
||||
"agentManager.shortcuts.nextTab": "Next tab",
|
||||
"agentManager.shortcuts.newTab": "New tab",
|
||||
"agentManager.shortcuts.closeTab": "Close tab",
|
||||
"agentManager.shortcuts.toggleTerminal": "Toggle terminal",
|
||||
"agentManager.shortcuts.toggleTerminal": "Focus / hide terminal",
|
||||
"agentManager.shortcuts.runScript": "Run script",
|
||||
"agentManager.run.options": "Run options",
|
||||
"agentManager.run.configure": "Configure run script",
|
||||
|
||||
+1
-1
@@ -102,7 +102,7 @@ export const dict = {
|
||||
"agentManager.shortcuts.nextTab": "Siguiente pestaña",
|
||||
"agentManager.shortcuts.newTab": "Nueva pestaña",
|
||||
"agentManager.shortcuts.closeTab": "Cerrar pestaña",
|
||||
"agentManager.shortcuts.toggleTerminal": "Alternar terminal",
|
||||
"agentManager.shortcuts.toggleTerminal": "Enfocar / ocultar la terminal",
|
||||
"agentManager.shortcuts.runScript": "Ejecutar script",
|
||||
"agentManager.run.options": "Opciones de ejecución",
|
||||
"agentManager.run.configure": "Configurar script de ejecución",
|
||||
|
||||
+1
-1
@@ -105,7 +105,7 @@ export const dict = {
|
||||
"agentManager.shortcuts.nextTab": "تب بعدی",
|
||||
"agentManager.shortcuts.newTab": "تب جدید",
|
||||
"agentManager.shortcuts.closeTab": "بستن تب",
|
||||
"agentManager.shortcuts.toggleTerminal": "نمایش/پنهان کردن ترمینال",
|
||||
"agentManager.shortcuts.toggleTerminal": "تمرکز روی ترمینال / پنهان کردن ترمینال",
|
||||
"agentManager.shortcuts.runScript": "اجرای اسکریپت",
|
||||
"agentManager.run.options": "گزینههای اجرا",
|
||||
"agentManager.run.configure": "پیکربندی اسکریپت اجرا",
|
||||
|
||||
+1
-1
@@ -102,7 +102,7 @@ export const dict = {
|
||||
"agentManager.shortcuts.nextTab": "Onglet suivant",
|
||||
"agentManager.shortcuts.newTab": "Nouvel onglet",
|
||||
"agentManager.shortcuts.closeTab": "Fermer l'onglet",
|
||||
"agentManager.shortcuts.toggleTerminal": "Basculer le terminal",
|
||||
"agentManager.shortcuts.toggleTerminal": "Focaliser / masquer le terminal",
|
||||
"agentManager.shortcuts.runScript": "Exécuter le script",
|
||||
"agentManager.run.options": "Options d'exécution",
|
||||
"agentManager.run.configure": "Configurer le script d'exécution",
|
||||
|
||||
+1
-1
@@ -107,7 +107,7 @@ export const dict = {
|
||||
"agentManager.shortcuts.nextTab": "Scheda successiva",
|
||||
"agentManager.shortcuts.newTab": "Nuova scheda",
|
||||
"agentManager.shortcuts.closeTab": "Chiudi scheda",
|
||||
"agentManager.shortcuts.toggleTerminal": "Mostra/nascondi terminale",
|
||||
"agentManager.shortcuts.toggleTerminal": "Metti a fuoco / nascondi il terminale",
|
||||
"agentManager.shortcuts.runScript": "Esegui script",
|
||||
"agentManager.run.options": "Opzioni di esecuzione",
|
||||
"agentManager.run.configure": "Configura script di esecuzione",
|
||||
|
||||
+1
-1
@@ -102,7 +102,7 @@ export const dict = {
|
||||
"agentManager.shortcuts.nextTab": "次のタブ",
|
||||
"agentManager.shortcuts.newTab": "新しいタブ",
|
||||
"agentManager.shortcuts.closeTab": "タブを閉じる",
|
||||
"agentManager.shortcuts.toggleTerminal": "ターミナルの切り替え",
|
||||
"agentManager.shortcuts.toggleTerminal": "ターミナルにフォーカス / 非表示にする",
|
||||
"agentManager.shortcuts.runScript": "スクリプトを実行",
|
||||
"agentManager.run.options": "実行オプション",
|
||||
"agentManager.run.configure": "実行スクリプトを設定",
|
||||
|
||||
+1
-1
@@ -100,7 +100,7 @@ export const dict = {
|
||||
"agentManager.shortcuts.nextTab": "다음 탭",
|
||||
"agentManager.shortcuts.newTab": "새 탭",
|
||||
"agentManager.shortcuts.closeTab": "탭 닫기",
|
||||
"agentManager.shortcuts.toggleTerminal": "터미널 전환",
|
||||
"agentManager.shortcuts.toggleTerminal": "터미널에 포커스 / 터미널 숨기기",
|
||||
"agentManager.shortcuts.runScript": "스크립트 실행",
|
||||
"agentManager.run.options": "실행 옵션",
|
||||
"agentManager.run.configure": "실행 스크립트 구성",
|
||||
|
||||
+1
-1
@@ -106,7 +106,7 @@ export const dict = {
|
||||
"agentManager.shortcuts.nextTab": "Volgend tabblad",
|
||||
"agentManager.shortcuts.newTab": "Nieuw tabblad",
|
||||
"agentManager.shortcuts.closeTab": "Tabblad sluiten",
|
||||
"agentManager.shortcuts.toggleTerminal": "Terminal in-/uitschakelen",
|
||||
"agentManager.shortcuts.toggleTerminal": "Terminal focussen / verbergen",
|
||||
"agentManager.shortcuts.runScript": "Script uitvoeren",
|
||||
"agentManager.run.options": "Uitvoeropties",
|
||||
"agentManager.run.configure": "Uitvoerscript configureren",
|
||||
|
||||
+1
-1
@@ -100,7 +100,7 @@ export const dict = {
|
||||
"agentManager.shortcuts.nextTab": "Neste fane",
|
||||
"agentManager.shortcuts.newTab": "Ny fane",
|
||||
"agentManager.shortcuts.closeTab": "Lukk fane",
|
||||
"agentManager.shortcuts.toggleTerminal": "Veksle terminal",
|
||||
"agentManager.shortcuts.toggleTerminal": "Fokuser / skjul terminalen",
|
||||
"agentManager.shortcuts.runScript": "Kjør skript",
|
||||
"agentManager.run.options": "Kjøringsalternativer",
|
||||
"agentManager.run.configure": "Konfigurer kjøreskript",
|
||||
|
||||
+1
-1
@@ -102,7 +102,7 @@ export const dict = {
|
||||
"agentManager.shortcuts.nextTab": "Następna karta",
|
||||
"agentManager.shortcuts.newTab": "Nowa karta",
|
||||
"agentManager.shortcuts.closeTab": "Zamknij kartę",
|
||||
"agentManager.shortcuts.toggleTerminal": "Przełącz terminal",
|
||||
"agentManager.shortcuts.toggleTerminal": "Przenieś fokus do terminala / ukryj terminal",
|
||||
"agentManager.shortcuts.runScript": "Uruchom skrypt",
|
||||
"agentManager.run.options": "Opcje uruchamiania",
|
||||
"agentManager.run.configure": "Konfiguruj skrypt uruchamiania",
|
||||
|
||||
+1
-1
@@ -102,7 +102,7 @@ export const dict = {
|
||||
"agentManager.shortcuts.nextTab": "Следующая вкладка",
|
||||
"agentManager.shortcuts.newTab": "Новая вкладка",
|
||||
"agentManager.shortcuts.closeTab": "Закрыть вкладку",
|
||||
"agentManager.shortcuts.toggleTerminal": "Переключить терминал",
|
||||
"agentManager.shortcuts.toggleTerminal": "Перевести фокус на терминал / скрыть терминал",
|
||||
"agentManager.shortcuts.runScript": "Запустить скрипт",
|
||||
"agentManager.run.options": "Параметры запуска",
|
||||
"agentManager.run.configure": "Настроить скрипт запуска",
|
||||
|
||||
+1
-1
@@ -97,7 +97,7 @@ export const dict = {
|
||||
"agentManager.shortcuts.nextTab": "แท็บถัดไป",
|
||||
"agentManager.shortcuts.newTab": "แท็บใหม่",
|
||||
"agentManager.shortcuts.closeTab": "ปิดแท็บ",
|
||||
"agentManager.shortcuts.toggleTerminal": "สลับเทอร์มินัล",
|
||||
"agentManager.shortcuts.toggleTerminal": "โฟกัสเทอร์มินัล / ซ่อนเทอร์มินัล",
|
||||
"agentManager.shortcuts.runScript": "เรียกใช้สคริปต์",
|
||||
"agentManager.run.options": "ตัวเลือกการเรียกใช้",
|
||||
"agentManager.run.configure": "กำหนดค่าสคริปต์การเรียกใช้",
|
||||
|
||||
+1
-1
@@ -107,7 +107,7 @@ export const dict = {
|
||||
"agentManager.shortcuts.nextTab": "Sonraki sekme",
|
||||
"agentManager.shortcuts.newTab": "Yeni sekme",
|
||||
"agentManager.shortcuts.closeTab": "Sekmeyi kapat",
|
||||
"agentManager.shortcuts.toggleTerminal": "Terminali aç/kapat",
|
||||
"agentManager.shortcuts.toggleTerminal": "Terminale odaklan / terminali gizle",
|
||||
"agentManager.shortcuts.runScript": "Betiği çalıştır",
|
||||
"agentManager.run.options": "Çalıştırma seçenekleri",
|
||||
"agentManager.run.configure": "Çalıştırma betiğini yapılandır",
|
||||
|
||||
+1
-1
@@ -108,7 +108,7 @@ export const dict = {
|
||||
"agentManager.shortcuts.nextTab": "Наступна вкладка",
|
||||
"agentManager.shortcuts.newTab": "Нова вкладка",
|
||||
"agentManager.shortcuts.closeTab": "Закрити вкладку",
|
||||
"agentManager.shortcuts.toggleTerminal": "Перемкнути термінал",
|
||||
"agentManager.shortcuts.toggleTerminal": "Перейти до термінала / приховати термінал",
|
||||
"agentManager.shortcuts.runScript": "Запустити скрипт",
|
||||
"agentManager.run.options": "Параметри запуску",
|
||||
"agentManager.run.configure": "Налаштувати скрипт запуску",
|
||||
|
||||
+1
-1
@@ -97,7 +97,7 @@ export const dict = {
|
||||
"agentManager.shortcuts.nextTab": "下一个标签页",
|
||||
"agentManager.shortcuts.newTab": "新建标签页",
|
||||
"agentManager.shortcuts.closeTab": "关闭标签页",
|
||||
"agentManager.shortcuts.toggleTerminal": "切换终端",
|
||||
"agentManager.shortcuts.toggleTerminal": "聚焦终端 / 隐藏终端",
|
||||
"agentManager.shortcuts.runScript": "运行脚本",
|
||||
"agentManager.run.options": "运行选项",
|
||||
"agentManager.run.configure": "配置运行脚本",
|
||||
|
||||
+1
-1
@@ -97,7 +97,7 @@ export const dict = {
|
||||
"agentManager.shortcuts.nextTab": "下一個分頁",
|
||||
"agentManager.shortcuts.newTab": "新建分頁",
|
||||
"agentManager.shortcuts.closeTab": "關閉分頁",
|
||||
"agentManager.shortcuts.toggleTerminal": "切換終端機",
|
||||
"agentManager.shortcuts.toggleTerminal": "聚焦終端機 / 隱藏終端機",
|
||||
"agentManager.shortcuts.runScript": "執行指令碼",
|
||||
"agentManager.run.options": "執行選項",
|
||||
"agentManager.run.configure": "設定執行指令碼",
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import { createSignal } from "solid-js"
|
||||
|
||||
/**
|
||||
* Tracks a cross-project worktree creation so the target project is activated
|
||||
* once its worktree is ready, and abandons the pending activation when the
|
||||
* creation fails or completes through another flow.
|
||||
*/
|
||||
export function usePendingCreate(
|
||||
active: () => string | undefined,
|
||||
activate: (projectId: string, worktreeId: string) => void,
|
||||
) {
|
||||
const [pending, setPending] = createSignal<{ projectId: string }>()
|
||||
|
||||
const schedule = (projectId: string) => {
|
||||
if (projectId === active()) return
|
||||
if (pending()) return
|
||||
setPending({ projectId })
|
||||
}
|
||||
|
||||
const abandon = (projectId?: string) => {
|
||||
if (pending()?.projectId === projectId) setPending(undefined)
|
||||
}
|
||||
|
||||
const setup = (ev: { status: string; projectId?: string; worktreeId?: string }) => {
|
||||
if (pending()?.projectId !== ev.projectId) return
|
||||
if (ev.status === "ready" && ev.projectId && ev.worktreeId) {
|
||||
setPending(undefined)
|
||||
activate(ev.projectId, ev.worktreeId)
|
||||
}
|
||||
if (ev.status === "error") setPending(undefined)
|
||||
}
|
||||
|
||||
return { schedule, abandon, setup }
|
||||
}
|
||||
@@ -12,15 +12,16 @@ export function buildShortcutCategories(
|
||||
bindings: Record<string, string>,
|
||||
t: (key: string, params?: Record<string, string | number>) => string,
|
||||
): ShortcutCategory[] {
|
||||
const bind = (key: string) => bindings[key] ?? ""
|
||||
return [
|
||||
{
|
||||
title: t("agentManager.shortcuts.category.quickSwitch"),
|
||||
shortcuts: [
|
||||
{ label: t("agentManager.sidebarSearch.label"), binding: bindings.search ?? "" },
|
||||
{ label: t("agentManager.sidebarSearch.label"), binding: bind("search") },
|
||||
{
|
||||
label: t("agentManager.shortcuts.jumpToItem"),
|
||||
binding: (() => {
|
||||
const first = bindings.jumpTo1 ?? ""
|
||||
const first = bind("jumpTo1")
|
||||
const prefix = first.replace(/\d+$/, "")
|
||||
return prefix ? `${prefix}1-9` : ""
|
||||
})(),
|
||||
@@ -30,39 +31,48 @@ export function buildShortcutCategories(
|
||||
{
|
||||
title: t("agentManager.shortcuts.category.sidebar"),
|
||||
shortcuts: [
|
||||
{ label: t("agentManager.shortcuts.previousItem"), binding: bindings.previousSession ?? "" },
|
||||
{ label: t("agentManager.shortcuts.nextItem"), binding: bindings.nextSession ?? "" },
|
||||
{ label: t("agentManager.shortcuts.previousItem"), binding: bind("previousSession") },
|
||||
{ label: t("agentManager.shortcuts.nextItem"), binding: bind("nextSession") },
|
||||
{ label: t("agentManager.shortcuts.advancedWorktree"), binding: bindings.newWorktree ?? "" },
|
||||
{ label: t("agentManager.shortcuts.newWorktree"), binding: bindings.quickWorktree ?? "" },
|
||||
{ label: t("agentManager.shortcuts.deleteWorktree"), binding: bindings.closeWorktree ?? "" },
|
||||
{ label: t("agentManager.shortcuts.openWorktree"), binding: bindings.openWorktree ?? "" },
|
||||
{ label: t("agentManager.shortcuts.openPR"), binding: bindings.openPR ?? "" },
|
||||
{ label: t("agentManager.shortcuts.deleteWorktree"), binding: bind("closeWorktree") },
|
||||
{ label: t("agentManager.shortcuts.openWorktree"), binding: bind("openWorktree") },
|
||||
{ label: t("agentManager.shortcuts.openPR"), binding: bind("openPR") },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: t("agentManager.shortcuts.category.tabs"),
|
||||
shortcuts: [
|
||||
{ label: t("agentManager.shortcuts.previousTab"), binding: bindings.previousTab ?? "" },
|
||||
{ label: t("agentManager.shortcuts.nextTab"), binding: bindings.nextTab ?? "" },
|
||||
{ label: t("agentManager.shortcuts.newTab"), binding: bindings.newTab ?? "" },
|
||||
{ label: t("agentManager.shortcuts.closeTab"), binding: bindings.closeTab ?? "" },
|
||||
{ label: t("agentManager.shortcuts.previousTab"), binding: bind("previousTab") },
|
||||
{ label: t("agentManager.shortcuts.nextTab"), binding: bind("nextTab") },
|
||||
{ label: t("agentManager.shortcuts.newTab"), binding: bind("newTab") },
|
||||
{ label: t("agentManager.shortcuts.closeTab"), binding: bind("closeTab") },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: t("agentManager.shortcuts.category.terminal"),
|
||||
shortcuts: [
|
||||
{ label: t("agentManager.shortcuts.toggleTerminal"), binding: bindings.showTerminal ?? "" },
|
||||
{ label: t("agentManager.shortcuts.runScript"), binding: bindings.runScript ?? "" },
|
||||
{ label: t("agentManager.shortcuts.toggleDiff"), binding: bindings.toggleDiff ?? "" },
|
||||
{ label: t("agentManager.shortcuts.toggleTerminal"), binding: bind("showTerminal") },
|
||||
{ label: t("agentManager.terminal.add"), binding: bind("newTerminal") },
|
||||
{
|
||||
label: `${t("agentManager.shortcuts.previousTab")} (${t("agentManager.tab.terminal")})`,
|
||||
binding: bind("previousTerminal"),
|
||||
},
|
||||
{
|
||||
label: `${t("agentManager.shortcuts.nextTab")} (${t("agentManager.tab.terminal")})`,
|
||||
binding: bind("nextTerminal"),
|
||||
},
|
||||
{ label: t("agentManager.shortcuts.runScript"), binding: bind("runScript") },
|
||||
{ label: t("agentManager.shortcuts.toggleDiff"), binding: bind("toggleDiff") },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: t("agentManager.shortcuts.category.global"),
|
||||
shortcuts: [
|
||||
{ label: t("agentManager.shortcuts.openAgentManager"), binding: bindings.agentManagerOpen ?? "" },
|
||||
{ label: t("agentManager.shortcuts.cycleAgentMode"), binding: bindings.cycleAgentMode ?? "" },
|
||||
{ label: t("agentManager.shortcuts.cyclePreviousAgentMode"), binding: bindings.cyclePreviousAgentMode ?? "" },
|
||||
{ label: t("agentManager.shortcuts.showShortcuts"), binding: bindings.showShortcuts ?? "" },
|
||||
{ label: t("agentManager.shortcuts.openAgentManager"), binding: bind("agentManagerOpen") },
|
||||
{ label: t("agentManager.shortcuts.cycleAgentMode"), binding: bind("cycleAgentMode") },
|
||||
{ label: t("agentManager.shortcuts.cyclePreviousAgentMode"), binding: bind("cyclePreviousAgentMode") },
|
||||
{ label: t("agentManager.shortcuts.showShortcuts"), binding: bind("showShortcuts") },
|
||||
].filter((s) => s.binding),
|
||||
},
|
||||
].filter((c) => c.shortcuts.length > 0)
|
||||
|
||||
@@ -30,7 +30,7 @@ import type { DragEvent } from "@thisbeyond/solid-dnd"
|
||||
import { IconButton } from "@kilocode/kilo-ui/icon-button"
|
||||
import { Button } from "@kilocode/kilo-ui/button"
|
||||
import { Spinner } from "@kilocode/kilo-ui/spinner"
|
||||
import { Tooltip } from "@kilocode/kilo-ui/tooltip"
|
||||
import { Tooltip, TooltipKeybind } from "@kilocode/kilo-ui/tooltip"
|
||||
import { useLanguage } from "../../src/context/language"
|
||||
import { ConstrainDragYAxis } from "../../src/components/chat/TabDnd"
|
||||
import { useTabScroll } from "../../src/utils/tab-scroll"
|
||||
@@ -57,8 +57,11 @@ interface Props {
|
||||
onCloseOthers: (terminalId: string) => void
|
||||
/** Create a new side terminal for this context. */
|
||||
onStart: () => void
|
||||
nextKeybind: string
|
||||
closeKeybind: string
|
||||
/** Deliberately stop a running script terminal. */
|
||||
onStop: (terminalId: string) => void
|
||||
onFocusPrompt: () => void
|
||||
}
|
||||
|
||||
export const SideTerminalPanel: Component<Props> = (props) => {
|
||||
@@ -170,7 +173,10 @@ export const SideTerminalPanel: Component<Props> = (props) => {
|
||||
label={props.state.title(term.id) ?? term.title}
|
||||
tooltip={props.state.title(term.id) ?? term.title}
|
||||
status={props.state.scriptStatus(term.id)}
|
||||
keybind={active() === term.id ? "" : props.nextKeybind}
|
||||
closeKeybind={props.closeKeybind}
|
||||
active={active() === term.id}
|
||||
focused={props.state.sideFocusedId() === term.id}
|
||||
role="tab"
|
||||
selected={active() === term.id}
|
||||
tabIndex={active() === term.id ? 0 : -1}
|
||||
@@ -226,7 +232,12 @@ export const SideTerminalPanel: Component<Props> = (props) => {
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
{renderSideTerminalLayer({ state: props.state, contextKey: props.contextKey, visible: props.visible })}
|
||||
{renderSideTerminalLayer({
|
||||
state: props.state,
|
||||
contextKey: props.contextKey,
|
||||
visible: props.visible,
|
||||
onFocusPrompt: props.onFocusPrompt,
|
||||
})}
|
||||
<Show when={props.visible() && sides().length === 0 && pending()}>
|
||||
<div class="am-side-terminal-state" role="status">
|
||||
<Spinner />
|
||||
|
||||
@@ -27,6 +27,7 @@ export const TerminalTabChrome: Component<{
|
||||
status?: ScriptTerminalStatus
|
||||
keybind?: string
|
||||
closeKeybind?: string
|
||||
focused?: boolean
|
||||
active: boolean
|
||||
role?: "tab"
|
||||
selected?: boolean
|
||||
@@ -46,7 +47,9 @@ export const TerminalTabChrome: Component<{
|
||||
return "console"
|
||||
}
|
||||
return (
|
||||
<div class={`am-tab am-tab-terminal ${props.active ? "am-tab-active" : ""}`}>
|
||||
<div
|
||||
class={`am-tab am-tab-terminal ${props.active ? "am-tab-active" : ""} ${props.focused ? "am-tab-terminal-focused" : ""}`}
|
||||
>
|
||||
<div
|
||||
class="am-tab-target"
|
||||
role={props.role}
|
||||
@@ -126,6 +129,7 @@ export const SortableTerminalTab: Component<{
|
||||
status?: ScriptTerminalStatus
|
||||
keybind?: string
|
||||
closeKeybind?: string
|
||||
focused?: boolean
|
||||
active: boolean
|
||||
role?: "tab"
|
||||
selected?: boolean
|
||||
@@ -148,6 +152,7 @@ export const SortableTerminalTab: Component<{
|
||||
status={props.status}
|
||||
keybind={props.keybind}
|
||||
closeKeybind={props.closeKeybind}
|
||||
focused={props.focused}
|
||||
active={props.active}
|
||||
role={props.role}
|
||||
selected={props.selected}
|
||||
|
||||
+7
-1
@@ -20,6 +20,8 @@ interface Props {
|
||||
destination: Accessor<TerminalDestination>
|
||||
/** True while the embedded terminal panel is showing. */
|
||||
active: Accessor<boolean>
|
||||
/** True while the embedded terminal owns DOM focus. */
|
||||
focused: Accessor<boolean>
|
||||
keybind: Accessor<string>
|
||||
onOpen: () => void
|
||||
onChoose: (destination: TerminalDestination) => void
|
||||
@@ -27,6 +29,10 @@ interface Props {
|
||||
|
||||
export const TerminalDestinationButton: Component<Props> = (props) => {
|
||||
const { t } = useLanguage()
|
||||
const title = () =>
|
||||
props.destination() === "agentManager" && props.focused()
|
||||
? t("agentManager.shortcuts.toggleTerminal")
|
||||
: t("agentManager.tab.openTerminal")
|
||||
const item = (destination: TerminalDestination, label: string) => (
|
||||
<DropdownMenu.Item onSelect={() => props.onChoose(destination)}>
|
||||
<span class="am-menu-check">
|
||||
@@ -39,7 +45,7 @@ export const TerminalDestinationButton: Component<Props> = (props) => {
|
||||
)
|
||||
return (
|
||||
<div class="am-split-button">
|
||||
<TooltipKeybind title={t("agentManager.tab.terminal")} keybind={props.keybind()} placement="bottom">
|
||||
<TooltipKeybind title={title()} keybind={props.keybind()} placement="bottom">
|
||||
<IconButton
|
||||
icon="console"
|
||||
size="small"
|
||||
|
||||
@@ -50,6 +50,9 @@ interface Props {
|
||||
* layer tracks this as `focusedId` so `Cmd+W` can target the
|
||||
* terminal that actually has the cursor. */
|
||||
onFocusChange?: (focused: boolean) => void
|
||||
/** Handle the Agent Manager prompt shortcut locally because xterm's
|
||||
* textarea does not reliably forward custom commands to the workbench. */
|
||||
onFocusPrompt?: () => void
|
||||
/** Reports OSC window-title escape codes (`ESC ] 0/1/2 ; title BEL`)
|
||||
* sent by the shell or running programs — fish sets it to the active
|
||||
* command, oh-my-zsh to user@host:cwd, vim to the file name. The
|
||||
@@ -131,7 +134,7 @@ function isAgentManagerShortcut(e: KeyboardEvent): boolean {
|
||||
const key = e.key.toLowerCase()
|
||||
if (e.altKey && ["arrowleft", "arrowright", "arrowup", "arrowdown"].includes(key)) return true
|
||||
if (["t", "w", "n", "d", "e", "f"].includes(key)) return true
|
||||
if (e.shiftKey && ["w", "n", "o", "r", "m", "/", "?"].includes(key)) return true
|
||||
if (e.shiftKey && ["t", "w", "n", "o", "r", "m", "[", "]", "/", "?"].includes(key)) return true
|
||||
if (/^[1-9]$/.test(key)) return true
|
||||
if (key === "/") return true
|
||||
return false
|
||||
@@ -151,6 +154,7 @@ export const TerminalTab: Component<Props> = (props) => {
|
||||
const term = new Terminal({
|
||||
convertEol: true,
|
||||
cursorBlink: true,
|
||||
cursorInactiveStyle: "outline",
|
||||
fontFamily: props.font.fontFamily,
|
||||
fontSize: props.font.fontSize,
|
||||
scrollback: 5000,
|
||||
@@ -172,9 +176,17 @@ export const TerminalTab: Component<Props> = (props) => {
|
||||
}
|
||||
})
|
||||
|
||||
// Pass agent-manager hotkeys through to the parent key handler so
|
||||
// ⌘T / ⌘W / ⌘⌥← etc. still work while the terminal is focused.
|
||||
term.attachCustomKeyEventHandler((event) => !isAgentManagerShortcut(event))
|
||||
// Pass Agent Manager hotkeys through to the parent key handler so
|
||||
// ⌘T / ⌘⇧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"
|
||||
if (prompt) {
|
||||
if (event.type === "keydown") props.onFocusPrompt?.()
|
||||
return false
|
||||
}
|
||||
return !isAgentManagerShortcut(event)
|
||||
})
|
||||
|
||||
// Track DOM focus so the state layer knows which terminal holds the
|
||||
// cursor (drives Cmd+W targeting). focusout is ignored when focus
|
||||
|
||||
@@ -51,6 +51,7 @@ export function renderTerminalTab(deps: TerminalTabRenderDeps): JSX.Element {
|
||||
status={deps.terms.scriptStatus(deps.id)}
|
||||
keybind={isActive() ? "" : deps.keybind()}
|
||||
closeKeybind={deps.closeKeybind()}
|
||||
focused={deps.terms.focusedId() === deps.id}
|
||||
active={isActive()}
|
||||
role={deps.role}
|
||||
selected={deps.selected}
|
||||
@@ -93,7 +94,7 @@ 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 }): JSX.Element {
|
||||
export function renderTerminalLayer(props: { state: TerminalStateControls; onFocusPrompt: () => void }): JSX.Element {
|
||||
const layerActive = () => props.state.activeId() !== undefined
|
||||
const slotVisible = (termId: string, contextKey: string) =>
|
||||
props.state.activeId() === termId && props.state.currentKey() === contextKey
|
||||
@@ -113,6 +114,7 @@ export function renderTerminalLayer(props: { state: TerminalStateControls }): JS
|
||||
focusSerial={focusSerial(props.state, term.id)}
|
||||
font={term.font}
|
||||
onFocusChange={(focused) => props.state.setFocusedId(focused ? term.id : undefined)}
|
||||
onFocusPrompt={props.onFocusPrompt}
|
||||
onTitleChange={(title) => props.state.setTitle(term.id, title)}
|
||||
/>
|
||||
</div>
|
||||
@@ -138,6 +140,7 @@ export function renderSideTerminalLayer(props: {
|
||||
state: TerminalStateControls
|
||||
contextKey: Accessor<string>
|
||||
visible: Accessor<boolean>
|
||||
onFocusPrompt: () => void
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<div class={`am-side-terminal-layer ${props.visible() ? "am-side-terminal-layer-active" : ""}`}>
|
||||
@@ -159,6 +162,7 @@ export function renderSideTerminalLayer(props: {
|
||||
status={() => props.state.scriptStatus(term.id)}
|
||||
restartable={term.kind === undefined}
|
||||
onFocusChange={(focused) => props.state.setFocusedId(focused ? term.id : undefined)}
|
||||
onFocusPrompt={props.onFocusPrompt}
|
||||
onTitleChange={(title) => props.state.setTitle(term.id, title)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -5,8 +5,9 @@
|
||||
* `max-lines` lint cap. Owns the destination preference plus the toggle
|
||||
* semantics of the toolbar button / `Cmd/Ctrl+/` shortcut, so the
|
||||
* embedded terminal behaves like the diff panel: press once to reveal,
|
||||
* press again to hide. Hiding never kills the terminal — only the
|
||||
* explicit close action (or `Cmd+W` while it holds focus) does.
|
||||
* press again while focused to hide, and press while visible but unfocused
|
||||
* to return focus to the shell. Hiding never kills the terminal — only the
|
||||
* explicit close action does.
|
||||
*
|
||||
* ## Destination state ownership
|
||||
*
|
||||
@@ -71,6 +72,10 @@ export interface SideTerminalDeps {
|
||||
visible: Accessor<boolean>
|
||||
/** Id of the side terminal holding DOM focus, if any. */
|
||||
focusedId: Accessor<string | undefined>
|
||||
/** Number of side terminals in the visible context. */
|
||||
count: Accessor<number>
|
||||
/** Whether the focused terminal is provider-owned Run/Setup output. */
|
||||
isScript: (terminalId: string) => boolean
|
||||
/** Leave terminal mode; the terminal stays alive in the background. */
|
||||
hide: () => void
|
||||
/** Move focus back to the chat composer. */
|
||||
@@ -106,9 +111,12 @@ export function createSideTerminal(deps: SideTerminalDeps) {
|
||||
|
||||
const toggle = () => {
|
||||
if (deps.visible()) {
|
||||
const was = deps.focusedId() !== undefined
|
||||
if (!deps.focusedId()) {
|
||||
deps.handlers.requestSide()
|
||||
return
|
||||
}
|
||||
deps.hide()
|
||||
handoff(was)
|
||||
handoff(true)
|
||||
return
|
||||
}
|
||||
deps.handlers.requestSide()
|
||||
@@ -122,15 +130,18 @@ export function createSideTerminal(deps: SideTerminalDeps) {
|
||||
})
|
||||
}
|
||||
|
||||
/** Kill the focused side terminal (Cmd/Ctrl+W). The panel stays open
|
||||
* on the remaining terminals, or on the empty state when this was
|
||||
* the last one. */
|
||||
/** Leave the focused side terminal without killing its shell when it is
|
||||
* the last terminal or provider-owned script output. */
|
||||
const close = (): boolean => {
|
||||
const id = deps.focusedId()
|
||||
if (!id) return false
|
||||
const done = deps.handlers.closeSide(id)
|
||||
if (done) handoff(true)
|
||||
return done
|
||||
const only = deps.count() === 1
|
||||
if (only || deps.isScript(id)) {
|
||||
deps.hide()
|
||||
handoff(true)
|
||||
return true
|
||||
}
|
||||
return deps.handlers.closeSide(id)
|
||||
}
|
||||
|
||||
/** Toolbar button and `Cmd/Ctrl+/`: follow the user's destination. */
|
||||
|
||||
@@ -746,8 +746,6 @@ export function createTerminalHandlers(deps: TerminalHandlerDeps) {
|
||||
const target = deps.findTab(nextId)
|
||||
if (target) deps.selectSessionTab(target.id, deps.isPendingId(target.id))
|
||||
}
|
||||
} else {
|
||||
deps.clearSession()
|
||||
}
|
||||
}
|
||||
deps.postMessage({ type: "agentManager.terminal.close", terminalId })
|
||||
@@ -764,6 +762,14 @@ export function createTerminalHandlers(deps: TerminalHandlerDeps) {
|
||||
// unmount its xterm while the backend PTY leaks (no close sent).
|
||||
const term = deps.state.sides().find((t) => t.id === terminalId)
|
||||
if (!term) return false
|
||||
const key = term.contextKey
|
||||
const active = deps.state.sideActiveFor(key) === terminalId
|
||||
const rest = active ? deps.state.sidesForContext(key).filter((item) => item.id !== terminalId) : []
|
||||
const survivor = rest[rest.length - 1]
|
||||
if (survivor) {
|
||||
deps.state.setSideActive(key, survivor.id)
|
||||
deps.state.requestFocus(survivor.id)
|
||||
}
|
||||
if (term.kind) {
|
||||
deps.postMessage({ type: "agentManager.terminal.close", terminalId })
|
||||
return true
|
||||
@@ -820,6 +826,33 @@ export function createTerminalHandlers(deps: TerminalHandlerDeps) {
|
||||
return true
|
||||
}
|
||||
|
||||
/** Close the main terminal that actually owns DOM focus, not just the active tab. */
|
||||
const closeFocused = () => {
|
||||
const id = deps.state.focusedId()
|
||||
if (!id || !deps.state.current().some((term) => term.id === id)) return false
|
||||
closeTerminal(id)
|
||||
return true
|
||||
}
|
||||
|
||||
/** Cycle terminals within one placement, wrapping at either end. */
|
||||
const cycle = (direction: "previous" | "next", placement: "side" | "tab") => {
|
||||
const key = deps.state.sideKey()
|
||||
const list = placement === "side" ? deps.state.sidesForContext(key) : deps.state.current()
|
||||
if (list.length === 0) return false
|
||||
const current = placement === "side" ? deps.state.sideActiveFor(key) : deps.state.activeId()
|
||||
const index = list.findIndex((term) => term.id === current)
|
||||
const start = index === -1 ? (direction === "next" ? -1 : list.length) : index
|
||||
const offset = direction === "next" ? 1 : -1
|
||||
const next = list[(start + offset + list.length) % list.length]!
|
||||
if (placement === "side") {
|
||||
deps.state.setSideActive(key, next.id)
|
||||
deps.state.requestFocus(next.id)
|
||||
return true
|
||||
}
|
||||
activate(next.id)
|
||||
return true
|
||||
}
|
||||
|
||||
return {
|
||||
closeTerminal,
|
||||
closeSide,
|
||||
@@ -834,6 +867,8 @@ export function createTerminalHandlers(deps: TerminalHandlerDeps) {
|
||||
ensureSide,
|
||||
addSide,
|
||||
closeActive,
|
||||
closeFocused,
|
||||
cycle,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import { LocalTabsProvider, useLocalTabs } from "./context/local-tabs"
|
||||
import { ProviderShell } from "./context/provider-shell"
|
||||
import { ChatView } from "./components/chat"
|
||||
import { SidebarEmptyState } from "./components/chat/SidebarEmptyState"
|
||||
import { SidebarTopBar } from "./components/chat/SidebarTopBar"
|
||||
import { registerExpandedTaskTool } from "./components/chat/TaskToolExpanded"
|
||||
import { registerVscodeToolOverrides } from "./components/chat/VscodeToolOverrides"
|
||||
|
||||
@@ -308,8 +309,23 @@ const AppContent: Component = () => {
|
||||
<SidebarEmptyState onSelectSession={handleSelectSession} onShowHistory={() => setCurrentView("history")} />
|
||||
)
|
||||
|
||||
// Set synchronously in the webview HTML by KiloProvider so it's available
|
||||
// before this component ever mounts (see buildWebviewHtml/_getHtmlForWebview).
|
||||
// Dedicated single-purpose panels (Settings, Profile, Sub-Agent Viewer) set
|
||||
// KILO_TOP_BAR = false since navigating away from them makes no sense.
|
||||
const host = window as { KILO_TOP_BAR?: boolean; KILO_TOP_BAR_SURFACE?: string }
|
||||
const showTopBar = host.KILO_TOP_BAR !== false
|
||||
const topBarSurface = host.KILO_TOP_BAR_SURFACE ?? "sidebar_title"
|
||||
|
||||
return (
|
||||
<div class="container">
|
||||
<Show when={showTopBar}>
|
||||
<SidebarTopBar
|
||||
onNewTask={() => handleViewAction("plusButtonClicked")}
|
||||
onHistory={() => handleViewAction("historyButtonClicked")}
|
||||
surface={topBarSurface}
|
||||
/>
|
||||
</Show>
|
||||
{/* legacy-migration start — state-driven overlay, independent of currentView */}
|
||||
<Show
|
||||
when={migrationNeeded()}
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
/**
|
||||
* Renders New Task, History, Agent Manager, KiloClaw, Marketplace, Profile, and
|
||||
* Settings inside the webview. VS Code's native `view/title` toolbar renders
|
||||
* outside the webview DOM and disappears in the Secondary Side Bar with no way
|
||||
* to detect or work around that — this bar guarantees the actions stay visible.
|
||||
*/
|
||||
|
||||
import { Component, For } from "solid-js"
|
||||
import { Tooltip } from "@kilocode/kilo-ui/tooltip"
|
||||
import { useVSCode } from "../../context/vscode"
|
||||
import { useLanguage } from "../../context/language"
|
||||
import { TelemetryEventName } from "../../../../src/services/telemetry/types"
|
||||
import "@vscode/codicons/dist/codicon.css"
|
||||
|
||||
export interface SidebarTopBarProps {
|
||||
onNewTask: () => void
|
||||
onHistory: () => void
|
||||
/** Telemetry surface — distinguishes the sidebar from the "Open in Tab" panel, which shares this component. */
|
||||
surface: string
|
||||
}
|
||||
|
||||
/** Codicon names used below. */
|
||||
type Codicon = "add" | "history" | "organization" | "comment-discussion" | "extensions" | "account" | "settings-gear"
|
||||
|
||||
interface Action {
|
||||
key: string
|
||||
codicon: Codicon
|
||||
button: string
|
||||
run: () => void
|
||||
}
|
||||
|
||||
export const SidebarTopBar: Component<SidebarTopBarProps> = (props) => {
|
||||
const vscode = useVSCode()
|
||||
const language = useLanguage()
|
||||
|
||||
// Mirrors the telemetry the native toolbar buttons used to record, so analytics aren't lost.
|
||||
const track = (button: string) =>
|
||||
vscode.postMessage({
|
||||
type: "telemetry",
|
||||
event: TelemetryEventName.TITLE_BUTTON_CLICKED,
|
||||
properties: { button, surface: props.surface },
|
||||
})
|
||||
|
||||
const open = (
|
||||
type: "openAgentManager" | "openKiloClaw" | "openMarketplacePanel" | "openProfilePanel" | "openSettingsPanel",
|
||||
) => vscode.postMessage({ type })
|
||||
|
||||
const actions: (Action | "spacer")[] = [
|
||||
{ key: "newTask", codicon: "add", button: "new_task", run: () => props.onNewTask() },
|
||||
{ key: "history", codicon: "history", button: "history", run: () => props.onHistory() },
|
||||
{ key: "agentManager", codicon: "organization", button: "agent_manager", run: () => open("openAgentManager") },
|
||||
{ key: "kiloClaw", codicon: "comment-discussion", button: "kiloclaw", run: () => open("openKiloClaw") },
|
||||
{ key: "marketplace", codicon: "extensions", button: "marketplace", run: () => open("openMarketplacePanel") },
|
||||
"spacer",
|
||||
{ key: "profile", codicon: "account", button: "profile", run: () => open("openProfilePanel") },
|
||||
{ key: "settings", codicon: "settings-gear", button: "settings", run: () => open("openSettingsPanel") },
|
||||
]
|
||||
|
||||
return (
|
||||
<div class="sidebar-top-bar" role="toolbar" aria-label={language.t("sidebar.topBar.label")}>
|
||||
<For each={actions}>
|
||||
{(action) => {
|
||||
if (action === "spacer") return <div class="sidebar-top-bar-spacer" />
|
||||
const label = language.t(`sidebar.topBar.${action.key}`)
|
||||
return (
|
||||
<Tooltip value={label} placement="bottom">
|
||||
<button
|
||||
type="button"
|
||||
data-component="icon-button"
|
||||
data-variant="ghost"
|
||||
data-size="small"
|
||||
aria-label={label}
|
||||
onClick={() => {
|
||||
track(action.button)
|
||||
action.run()
|
||||
}}
|
||||
>
|
||||
<div data-component="icon" data-size="small">
|
||||
<i class={`codicon codicon-${action.codicon}`} aria-hidden="true" />
|
||||
</div>
|
||||
</button>
|
||||
</Tooltip>
|
||||
)
|
||||
}}
|
||||
</For>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -16,6 +16,7 @@ import { DEFAULT_SPEECH_TO_TEXT_MODEL } from "../../../../src/speech-to-text/mod
|
||||
import { hasSpeechToTextAccess, selectedSpeechToTextModel } from "../speech-to-text/availability"
|
||||
import { speechToTextModelOptions } from "../speech-to-text/model-selector"
|
||||
import { AUTOCOMPLETE_SELECTOR_MODELS, getAutocompleteSelection } from "./autocomplete-model-selector"
|
||||
import { preserveVariant } from "../../context/session-variant-store"
|
||||
|
||||
const ModelsTab: Component = () => {
|
||||
const { config, settings, updateConfig, updateSetting } = useConfig()
|
||||
@@ -64,9 +65,12 @@ const ModelsTab: Component = () => {
|
||||
return
|
||||
}
|
||||
const value = `${providerID}/${modelID}`
|
||||
const list = Object.keys(provider.findModel({ providerID, modelID })?.variants ?? {})
|
||||
const next = preserveVariant(subagentVariant(), list)
|
||||
updateConfig({
|
||||
subagent_model: value,
|
||||
...(config().subagent_model === value ? {} : { subagent_variant: null }),
|
||||
...(next ? { subagent_variant_overrides: { ...config().subagent_variant_overrides, [value]: next } } : {}),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -87,7 +91,17 @@ const ModelsTab: Component = () => {
|
||||
updateConfig({ agent: { [agentName]: { model: null } } })
|
||||
return
|
||||
}
|
||||
updateConfig({ agent: { [agentName]: { model: `${providerID}/${modelID}` } } })
|
||||
const current = config().agent?.[agentName]?.variant ?? undefined
|
||||
const list = Object.keys(provider.findModel({ providerID, modelID })?.variants ?? {})
|
||||
const next = preserveVariant(current, list)
|
||||
updateConfig({
|
||||
agent: {
|
||||
[agentName]: {
|
||||
model: `${providerID}/${modelID}`,
|
||||
...(current && !list.includes(current) ? { variant: next ?? null } : {}),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+3
-1
@@ -9,6 +9,7 @@ import { ModelSelectorBase } from "../../shared/ModelSelector"
|
||||
import { ThinkingSelectorBase } from "../../shared/ThinkingSelector"
|
||||
import { parseModelString } from "../../../../../src/shared/provider-model"
|
||||
import type { CommandConfig } from "../../../types/messages"
|
||||
import { preserveVariant } from "../../../context/session-variant-store"
|
||||
|
||||
const WorkflowsTab: Component = () => {
|
||||
const language = useLanguage()
|
||||
@@ -48,9 +49,10 @@ const WorkflowsTab: Component = () => {
|
||||
const selectModel = (name: string, providerID: string, modelID: string) => {
|
||||
const list = Object.keys(provider.findModel({ providerID, modelID })?.variants ?? {})
|
||||
const current = variant(config().command?.[name] ?? {}, name)
|
||||
const next = preserveVariant(current, list)
|
||||
update(name, {
|
||||
model: providerID && modelID ? `${providerID}/${modelID}` : null,
|
||||
...(current && !list.includes(current) ? { variant: null } : {}),
|
||||
...(current && !list.includes(current) ? { variant: next ?? null } : {}),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { AgentConfig } from "../../types/messages"
|
||||
import { preserveVariant } from "../../context/session-variant-store"
|
||||
|
||||
export function modelPatch(
|
||||
providerID: string,
|
||||
@@ -12,6 +13,6 @@ export function modelPatch(
|
||||
|
||||
return {
|
||||
model: `${providerID}/${modelID}`,
|
||||
...(current && !variants.includes(current) ? { variant: null } : {}),
|
||||
...(current && !variants.includes(current) ? { variant: preserveVariant(current, variants) ?? null } : {}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,26 @@
|
||||
import type { ModelSelection } from "../types/messages"
|
||||
|
||||
const effort = ["none", "minimal", "low", "medium", "high", "xhigh", "max"]
|
||||
|
||||
/** Keep the selected effort when possible, falling back to the nearest known effort. */
|
||||
export function preserveVariant(current: string | undefined, variants: string[]) {
|
||||
if (!current || variants.length === 0) return undefined
|
||||
if (variants.includes(current)) return current
|
||||
|
||||
const rank = effort.indexOf(current)
|
||||
if (rank === -1) return undefined
|
||||
|
||||
return variants
|
||||
.map((value, index) => ({
|
||||
value,
|
||||
index,
|
||||
rank: effort.indexOf(value),
|
||||
distance: Math.abs(effort.indexOf(value) - rank),
|
||||
}))
|
||||
.filter((item) => effort.includes(item.value))
|
||||
.sort((a, b) => a.distance - b.distance || b.rank - a.rank || a.index - b.index)[0]?.value
|
||||
}
|
||||
|
||||
export function legacyVariantKey(sel: ModelSelection) {
|
||||
return `${sel.providerID}/${sel.modelID}`
|
||||
}
|
||||
@@ -21,7 +42,7 @@ export function getVariant(
|
||||
const key = variantKey(sel, agent, session)
|
||||
const fallback = session ? store[variantKey(sel, agent)] : undefined
|
||||
const stored = store[key] ?? fallback ?? store[legacyVariantKey(sel)]
|
||||
return stored && variants.includes(stored) ? stored : variants[0]
|
||||
return preserveVariant(stored, variants) ?? variants[0]
|
||||
}
|
||||
|
||||
export function getAgentVariant(
|
||||
|
||||
@@ -74,7 +74,14 @@ import { errorIDs } from "./session-errors"
|
||||
import { PartStash } from "./part-stash"
|
||||
import { mergeParts, sameParts } from "./session-parts"
|
||||
import { state as todoState } from "./todo-revert"
|
||||
import { getAgentVariant, getVariant, sessionVariantKeys, transferVariants, variantKey } from "./session-variant-store"
|
||||
import {
|
||||
getAgentVariant,
|
||||
getVariant,
|
||||
preserveVariant,
|
||||
sessionVariantKeys,
|
||||
transferVariants,
|
||||
variantKey,
|
||||
} from "./session-variant-store"
|
||||
import { KILO_AUTO, KILO_PROVIDER_ID, parseModelString } from "../../../src/shared/provider-model"
|
||||
import { reviewMetadata, type ReviewMessageData } from "../../../src/shared/review-comments"
|
||||
import { visibleMessages as filterVisibleMessages } from "./session-queue"
|
||||
@@ -663,9 +670,22 @@ export const SessionProvider: ParentComponent = (props) => {
|
||||
})
|
||||
}
|
||||
|
||||
function carryVariant(selection: ModelSelection, current: string | undefined, agent: string, sessionID?: string) {
|
||||
const value = preserveVariant(current, Object.keys(provider.findModel(selection)?.variants ?? {}))
|
||||
if (!value) return
|
||||
const key = variantKey(selection, agent, sessionID)
|
||||
setStore("variantSelections", key, value)
|
||||
if (!sessionID) vscode.postMessage({ type: "persistVariant", key, value })
|
||||
}
|
||||
|
||||
function selectModel(providerID: string, modelID: string, sessionID?: string) {
|
||||
const sid = sessionID ?? currentSessionID()
|
||||
applyModel(agentForScope(sid), { providerID, modelID }, sid)
|
||||
const agent = agentForScope(sid)
|
||||
const current = selected(sid)
|
||||
const value = current ? currentVariant(sid) : undefined
|
||||
const selection = { providerID, modelID }
|
||||
applyModel(agent, selection, sid)
|
||||
carryVariant(selection, value, agent, sid)
|
||||
if (sid) {
|
||||
hideErrors(sid)
|
||||
}
|
||||
@@ -3033,8 +3053,12 @@ export const SessionProvider: ParentComponent = (props) => {
|
||||
// agent may not yet be assigned (sendInitialMessage calls setSessionModel
|
||||
// before setSessionAgent), so the write would land on defaultAgent() and
|
||||
// corrupt the default mode's model for later sessions.
|
||||
const agent = store.agentSelections[sessionID] ?? defaultAgent()
|
||||
const current = selected(sessionID)
|
||||
const value = current ? currentVariant(sessionID) : undefined
|
||||
const model = { providerID, modelID }
|
||||
setStore("sessionOverrides", sessionID, model)
|
||||
carryVariant(model, value, agent, sessionID)
|
||||
},
|
||||
setSessionAgent: (sessionID: string, name: string) => {
|
||||
setStore("agentSelections", sessionID, name)
|
||||
|
||||
+8
@@ -306,6 +306,14 @@ export const dict = {
|
||||
"session.messages.loadEarlier": "تحميل الرسائل السابقة",
|
||||
"session.messages.loading": "جارٍ تحميل الرسائل...",
|
||||
|
||||
"sidebar.topBar.label": "التنقل في Kilo Code",
|
||||
"sidebar.topBar.newTask": "مهمة جديدة",
|
||||
"sidebar.topBar.history": "السجل",
|
||||
"sidebar.topBar.agentManager": "مدير الوكلاء",
|
||||
"sidebar.topBar.kiloClaw": "KiloClaw",
|
||||
"sidebar.topBar.marketplace": "المتجر",
|
||||
"sidebar.topBar.profile": "الملف الشخصي",
|
||||
"sidebar.topBar.settings": "الإعدادات",
|
||||
"sidebar.session.newSession": "جلسة جديدة",
|
||||
"sidebar.session.newSession.tooltip": "ابدأ محادثة جديدة مع الاحتفاظ بالجلسة الحالية كما هي.",
|
||||
"sidebar.session.newWorktree": "Worktree جديد",
|
||||
|
||||
+8
@@ -316,6 +316,14 @@ export const dict = {
|
||||
"session.messages.loadEarlier": "Carregar mensagens anteriores",
|
||||
"session.messages.loading": "Carregando mensagens...",
|
||||
|
||||
"sidebar.topBar.label": "Navegação do Kilo Code",
|
||||
"sidebar.topBar.newTask": "Nova Tarefa",
|
||||
"sidebar.topBar.history": "Histórico",
|
||||
"sidebar.topBar.agentManager": "Agent Manager",
|
||||
"sidebar.topBar.kiloClaw": "KiloClaw",
|
||||
"sidebar.topBar.marketplace": "Marketplace",
|
||||
"sidebar.topBar.profile": "Perfil",
|
||||
"sidebar.topBar.settings": "Configurações",
|
||||
"sidebar.session.newSession": "Nova Sessão",
|
||||
"sidebar.session.newSession.tooltip": "Inicie uma nova conversa mantendo a sessão atual intacta.",
|
||||
"sidebar.session.newWorktree": "Novo Worktree",
|
||||
|
||||
+8
@@ -315,6 +315,14 @@ export const dict = {
|
||||
"session.messages.loadEarlier": "Učitaj ranije poruke",
|
||||
"session.messages.loading": "Učitavanje poruka...",
|
||||
|
||||
"sidebar.topBar.label": "Navigacija Kilo Code",
|
||||
"sidebar.topBar.newTask": "Novi Zadatak",
|
||||
"sidebar.topBar.history": "Historija",
|
||||
"sidebar.topBar.agentManager": "Agent Manager",
|
||||
"sidebar.topBar.kiloClaw": "KiloClaw",
|
||||
"sidebar.topBar.marketplace": "Marketplace",
|
||||
"sidebar.topBar.profile": "Profil",
|
||||
"sidebar.topBar.settings": "Postavke",
|
||||
"sidebar.session.newSession": "Nova Sesija",
|
||||
"sidebar.session.newSession.tooltip": "Započnite novi razgovor držeći trenutnu sesiju netaknutom.",
|
||||
"sidebar.session.newWorktree": "Novi Worktree",
|
||||
|
||||
+8
@@ -313,6 +313,14 @@ export const dict = {
|
||||
"session.messages.loadEarlier": "Indlæs tidligere beskeder",
|
||||
"session.messages.loading": "Indlæser beskeder...",
|
||||
|
||||
"sidebar.topBar.label": "Kilo Code-navigation",
|
||||
"sidebar.topBar.newTask": "Ny Opgave",
|
||||
"sidebar.topBar.history": "Historik",
|
||||
"sidebar.topBar.agentManager": "Agent Manager",
|
||||
"sidebar.topBar.kiloClaw": "KiloClaw",
|
||||
"sidebar.topBar.marketplace": "Markedsplads",
|
||||
"sidebar.topBar.profile": "Profil",
|
||||
"sidebar.topBar.settings": "Indstillinger",
|
||||
"sidebar.session.newSession": "Ny Session",
|
||||
"sidebar.session.newSession.tooltip": "Start en frisk samtale, mens den nuværende session holdes intakt.",
|
||||
"sidebar.session.newWorktree": "Nyt Worktree",
|
||||
|
||||
@@ -322,6 +322,14 @@ export const dict = {
|
||||
"session.messages.loadEarlier": "Frühere Nachrichten laden",
|
||||
"session.messages.loading": "Lade Nachrichten...",
|
||||
|
||||
"sidebar.topBar.label": "Kilo Code Navigation",
|
||||
"sidebar.topBar.newTask": "Neue Aufgabe",
|
||||
"sidebar.topBar.history": "Verlauf",
|
||||
"sidebar.topBar.agentManager": "Agent Manager",
|
||||
"sidebar.topBar.kiloClaw": "KiloClaw",
|
||||
"sidebar.topBar.marketplace": "Marktplatz",
|
||||
"sidebar.topBar.profile": "Profil",
|
||||
"sidebar.topBar.settings": "Einstellungen",
|
||||
"sidebar.session.newSession": "Neue Sitzung",
|
||||
"sidebar.session.newSession.tooltip": "Starte eine neue Unterhaltung, während die aktuelle Sitzung intakt bleibt.",
|
||||
"sidebar.session.newWorktree": "Neuer Worktree",
|
||||
|
||||
@@ -708,6 +708,14 @@ export const dict = {
|
||||
"session.costAlert.question":
|
||||
"This session just went above your {{limit}} per-session alert threshold and cost {{cost}}. Keep going?",
|
||||
"session.costAlert.stop": "Stop",
|
||||
"sidebar.topBar.label": "Kilo Code navigation",
|
||||
"sidebar.topBar.newTask": "New Task",
|
||||
"sidebar.topBar.history": "History",
|
||||
"sidebar.topBar.agentManager": "Agent Manager",
|
||||
"sidebar.topBar.kiloClaw": "KiloClaw",
|
||||
"sidebar.topBar.marketplace": "Marketplace",
|
||||
"sidebar.topBar.profile": "Profile",
|
||||
"sidebar.topBar.settings": "Settings",
|
||||
"sidebar.session.newSession": "New Session",
|
||||
"sidebar.session.newSession.tooltip": "Start a fresh conversation while keeping the current session intact.",
|
||||
"sidebar.session.newWorktree": "New Worktree",
|
||||
|
||||
+8
@@ -317,6 +317,14 @@ export const dict = {
|
||||
"session.messages.loadEarlier": "Cargar mensajes anteriores",
|
||||
"session.messages.loading": "Cargando mensajes...",
|
||||
|
||||
"sidebar.topBar.label": "Navegación de Kilo Code",
|
||||
"sidebar.topBar.newTask": "Nueva Tarea",
|
||||
"sidebar.topBar.history": "Historial",
|
||||
"sidebar.topBar.agentManager": "Agent Manager",
|
||||
"sidebar.topBar.kiloClaw": "KiloClaw",
|
||||
"sidebar.topBar.marketplace": "Marketplace",
|
||||
"sidebar.topBar.profile": "Perfil",
|
||||
"sidebar.topBar.settings": "Configuración",
|
||||
"sidebar.session.newSession": "Nueva Sesión",
|
||||
"sidebar.session.newSession.tooltip": "Inicia una nueva conversación manteniendo intacta la sesión actual.",
|
||||
"sidebar.session.newWorktree": "Nuevo Worktree",
|
||||
|
||||
+8
@@ -712,6 +712,14 @@ export const dict = {
|
||||
"session.costAlert.continue": "ادامه",
|
||||
"session.costAlert.question": "هزینه این جلسه از آستانه هشدار {{limit}} شما فراتر رفت و {{cost}} شد. ادامه میدهید؟",
|
||||
"session.costAlert.stop": "توقف",
|
||||
"sidebar.topBar.label": "پیمایش Kilo Code",
|
||||
"sidebar.topBar.newTask": "وظیفه جدید",
|
||||
"sidebar.topBar.history": "تاریخچه",
|
||||
"sidebar.topBar.agentManager": "مدیر عاملها",
|
||||
"sidebar.topBar.kiloClaw": "KiloClaw",
|
||||
"sidebar.topBar.marketplace": "بازارچه",
|
||||
"sidebar.topBar.profile": "پروفایل",
|
||||
"sidebar.topBar.settings": "تنظیمات",
|
||||
"sidebar.session.newSession": "جلسه جدید",
|
||||
"sidebar.session.newSession.tooltip": "یک مکالمه تازه شروع کنید و جلسه فعلی را دستنخورده نگه دارید.",
|
||||
"sidebar.session.newWorktree": "Worktree جدید",
|
||||
|
||||
+8
@@ -316,6 +316,14 @@ export const dict = {
|
||||
"session.messages.loadEarlier": "Charger les messages précédents",
|
||||
"session.messages.loading": "Chargement des messages...",
|
||||
|
||||
"sidebar.topBar.label": "Navigation Kilo Code",
|
||||
"sidebar.topBar.newTask": "Nouvelle Tâche",
|
||||
"sidebar.topBar.history": "Historique",
|
||||
"sidebar.topBar.agentManager": "Agent Manager",
|
||||
"sidebar.topBar.kiloClaw": "KiloClaw",
|
||||
"sidebar.topBar.marketplace": "Marketplace",
|
||||
"sidebar.topBar.profile": "Profil",
|
||||
"sidebar.topBar.settings": "Paramètres",
|
||||
"sidebar.session.newSession": "Nouvelle Session",
|
||||
"sidebar.session.newSession.tooltip":
|
||||
"Démarrer une nouvelle conversation tout en gardant la session actuelle intacte.",
|
||||
|
||||
+8
@@ -591,6 +591,14 @@ export const dict = {
|
||||
"session.status.retry": "Riprovo...",
|
||||
"session.status.working": "Al lavoro...",
|
||||
"session.status.offline": "Rete disconnessa - riconnessione...",
|
||||
"sidebar.topBar.label": "Navigazione Kilo Code",
|
||||
"sidebar.topBar.newTask": "Nuova Attività",
|
||||
"sidebar.topBar.history": "Cronologia",
|
||||
"sidebar.topBar.agentManager": "Agent Manager",
|
||||
"sidebar.topBar.kiloClaw": "KiloClaw",
|
||||
"sidebar.topBar.marketplace": "Marketplace",
|
||||
"sidebar.topBar.profile": "Profilo",
|
||||
"sidebar.topBar.settings": "Impostazioni",
|
||||
"sidebar.session.newSession": "Nuova sessione",
|
||||
"sidebar.session.newSession.tooltip": "Avvia una nuova conversazione mantenendo intatta la sessione corrente.",
|
||||
"sidebar.session.newWorktree": "Nuovo worktree",
|
||||
|
||||
+8
@@ -313,6 +313,14 @@ export const dict = {
|
||||
"session.messages.loadEarlier": "以前のメッセージを読み込む",
|
||||
"session.messages.loading": "メッセージを読み込み中...",
|
||||
|
||||
"sidebar.topBar.label": "Kilo Code ナビゲーション",
|
||||
"sidebar.topBar.newTask": "新規タスク",
|
||||
"sidebar.topBar.history": "履歴",
|
||||
"sidebar.topBar.agentManager": "エージェントマネージャー",
|
||||
"sidebar.topBar.kiloClaw": "KiloClaw",
|
||||
"sidebar.topBar.marketplace": "マーケットプレイス",
|
||||
"sidebar.topBar.profile": "プロフィール",
|
||||
"sidebar.topBar.settings": "設定",
|
||||
"sidebar.session.newSession": "新規セッション",
|
||||
"sidebar.session.newSession.tooltip": "現在のセッションを維持したまま、新しい会話を開始します。",
|
||||
"sidebar.session.newWorktree": "新規 Worktree",
|
||||
|
||||
+8
@@ -314,6 +314,14 @@ export const dict = {
|
||||
"session.messages.loadEarlier": "이전 메시지 로드",
|
||||
"session.messages.loading": "메시지 로드 중...",
|
||||
|
||||
"sidebar.topBar.label": "Kilo Code 탐색",
|
||||
"sidebar.topBar.newTask": "새 작업",
|
||||
"sidebar.topBar.history": "기록",
|
||||
"sidebar.topBar.agentManager": "에이전트 관리자",
|
||||
"sidebar.topBar.kiloClaw": "KiloClaw",
|
||||
"sidebar.topBar.marketplace": "마켓플레이스",
|
||||
"sidebar.topBar.profile": "프로필",
|
||||
"sidebar.topBar.settings": "설정",
|
||||
"sidebar.session.newSession": "새 세션",
|
||||
"sidebar.session.newSession.tooltip": "현재 세션을 그대로 유지하면서 새로운 대화를 시작합니다.",
|
||||
"sidebar.session.newWorktree": "새 Worktree",
|
||||
|
||||
+8
@@ -318,6 +318,14 @@ export const dict = {
|
||||
"session.messages.loadEarlier": "Eerdere berichten laden",
|
||||
"session.messages.loading": "Berichten laden...",
|
||||
|
||||
"sidebar.topBar.label": "Kilo Code-navigatie",
|
||||
"sidebar.topBar.newTask": "Nieuwe Taak",
|
||||
"sidebar.topBar.history": "Geschiedenis",
|
||||
"sidebar.topBar.agentManager": "Agent Manager",
|
||||
"sidebar.topBar.kiloClaw": "KiloClaw",
|
||||
"sidebar.topBar.marketplace": "Marketplace",
|
||||
"sidebar.topBar.profile": "Profiel",
|
||||
"sidebar.topBar.settings": "Instellingen",
|
||||
"sidebar.session.newSession": "Nieuwe Sessie",
|
||||
"sidebar.session.newSession.tooltip": "Start een nieuw gesprek terwijl je de huidige sessie intact houdt.",
|
||||
"sidebar.session.newWorktree": "Nieuwe Worktree",
|
||||
|
||||
+8
@@ -320,6 +320,14 @@ export const dict = {
|
||||
"session.messages.loadEarlier": "Last inn tidligere meldinger",
|
||||
"session.messages.loading": "Laster meldinger...",
|
||||
|
||||
"sidebar.topBar.label": "Kilo Code-navigasjon",
|
||||
"sidebar.topBar.newTask": "Ny Oppgave",
|
||||
"sidebar.topBar.history": "Historikk",
|
||||
"sidebar.topBar.agentManager": "Agent Manager",
|
||||
"sidebar.topBar.kiloClaw": "KiloClaw",
|
||||
"sidebar.topBar.marketplace": "Markedsplass",
|
||||
"sidebar.topBar.profile": "Profil",
|
||||
"sidebar.topBar.settings": "Innstillinger",
|
||||
"sidebar.session.newSession": "Ny Økt",
|
||||
"sidebar.session.newSession.tooltip": "Start en ny samtale mens den nåværende økten holdes intakt.",
|
||||
"sidebar.session.newWorktree": "Nytt Worktree",
|
||||
|
||||
+8
@@ -314,6 +314,14 @@ export const dict = {
|
||||
"session.messages.loadEarlier": "Załaduj wcześniejsze wiadomości",
|
||||
"session.messages.loading": "Ładowanie wiadomości...",
|
||||
|
||||
"sidebar.topBar.label": "Nawigacja Kilo Code",
|
||||
"sidebar.topBar.newTask": "Nowe Zadanie",
|
||||
"sidebar.topBar.history": "Historia",
|
||||
"sidebar.topBar.agentManager": "Agent Manager",
|
||||
"sidebar.topBar.kiloClaw": "KiloClaw",
|
||||
"sidebar.topBar.marketplace": "Marketplace",
|
||||
"sidebar.topBar.profile": "Profil",
|
||||
"sidebar.topBar.settings": "Ustawienia",
|
||||
"sidebar.session.newSession": "Nowa Sesja",
|
||||
"sidebar.session.newSession.tooltip": "Rozpocznij nową konwersację, zachowując obecną sesję bez zmian.",
|
||||
"sidebar.session.newWorktree": "Nowe Worktree",
|
||||
|
||||
+8
@@ -312,6 +312,14 @@ export const dict = {
|
||||
"session.messages.loadEarlier": "Загрузить предыдущие сообщения",
|
||||
"session.messages.loading": "Загрузка сообщений...",
|
||||
|
||||
"sidebar.topBar.label": "Навигация Kilo Code",
|
||||
"sidebar.topBar.newTask": "Новая задача",
|
||||
"sidebar.topBar.history": "История",
|
||||
"sidebar.topBar.agentManager": "Agent Manager",
|
||||
"sidebar.topBar.kiloClaw": "KiloClaw",
|
||||
"sidebar.topBar.marketplace": "Маркетплейс",
|
||||
"sidebar.topBar.profile": "Профиль",
|
||||
"sidebar.topBar.settings": "Настройки",
|
||||
"sidebar.session.newSession": "Новая сессия",
|
||||
"sidebar.session.newSession.tooltip": "Начать новую беседу, сохранив текущую сессию нетронутой.",
|
||||
"sidebar.session.newWorktree": "Новое Worktree",
|
||||
|
||||
+8
@@ -312,6 +312,14 @@ export const dict = {
|
||||
"session.messages.loadEarlier": "โหลดข้อความก่อนหน้า",
|
||||
"session.messages.loading": "กำลังโหลดข้อความ...",
|
||||
|
||||
"sidebar.topBar.label": "การนำทาง Kilo Code",
|
||||
"sidebar.topBar.newTask": "งานใหม่",
|
||||
"sidebar.topBar.history": "ประวัติ",
|
||||
"sidebar.topBar.agentManager": "ตัวจัดการเอเจนต์",
|
||||
"sidebar.topBar.kiloClaw": "KiloClaw",
|
||||
"sidebar.topBar.marketplace": "มาร์เก็ตเพลส",
|
||||
"sidebar.topBar.profile": "โปรไฟล์",
|
||||
"sidebar.topBar.settings": "การตั้งค่า",
|
||||
"sidebar.session.newSession": "เซสชันใหม่",
|
||||
"sidebar.session.newSession.tooltip": "เริ่มการสนทนาใหม่โดยที่ยังคงเซสชันปัจจุบันไว้",
|
||||
"sidebar.session.newWorktree": "Worktree ใหม่",
|
||||
|
||||
+8
@@ -313,6 +313,14 @@ export const dict = {
|
||||
"session.messages.loadEarlier": "Önceki mesajları yükle",
|
||||
"session.messages.loading": "Mesajlar yükleniyor...",
|
||||
|
||||
"sidebar.topBar.label": "Kilo Code Gezinme",
|
||||
"sidebar.topBar.newTask": "Yeni Görev",
|
||||
"sidebar.topBar.history": "Geçmiş",
|
||||
"sidebar.topBar.agentManager": "Agent Manager",
|
||||
"sidebar.topBar.kiloClaw": "KiloClaw",
|
||||
"sidebar.topBar.marketplace": "Marketplace",
|
||||
"sidebar.topBar.profile": "Profil",
|
||||
"sidebar.topBar.settings": "Ayarlar",
|
||||
"sidebar.session.newSession": "Yeni Oturum",
|
||||
"sidebar.session.newSession.tooltip": "Mevcut oturumu koruyarak yeni bir konuşma başlatın.",
|
||||
"sidebar.session.newWorktree": "Yeni Worktree",
|
||||
|
||||
+8
@@ -317,6 +317,14 @@ export const dict = {
|
||||
"session.messages.loadEarlier": "Завантажити попередні повідомлення",
|
||||
"session.messages.loading": "Завантаження повідомлень...",
|
||||
|
||||
"sidebar.topBar.label": "Навігація Kilo Code",
|
||||
"sidebar.topBar.newTask": "Нове завдання",
|
||||
"sidebar.topBar.history": "Історія",
|
||||
"sidebar.topBar.agentManager": "Agent Manager",
|
||||
"sidebar.topBar.kiloClaw": "KiloClaw",
|
||||
"sidebar.topBar.marketplace": "Маркетплейс",
|
||||
"sidebar.topBar.profile": "Профіль",
|
||||
"sidebar.topBar.settings": "Налаштування",
|
||||
"sidebar.session.newSession": "Нова сесія",
|
||||
"sidebar.session.newSession.tooltip": "Почати нову розмову, зберігши поточну сесію незмінною.",
|
||||
"sidebar.session.newWorktree": "Нове Worktree",
|
||||
|
||||
+8
@@ -301,6 +301,14 @@ export const dict = {
|
||||
"session.messages.loadEarlier": "加载更早的消息",
|
||||
"session.messages.loading": "正在加载消息...",
|
||||
|
||||
"sidebar.topBar.label": "Kilo Code 导航",
|
||||
"sidebar.topBar.newTask": "新建任务",
|
||||
"sidebar.topBar.history": "历史记录",
|
||||
"sidebar.topBar.agentManager": "代理管理器",
|
||||
"sidebar.topBar.kiloClaw": "KiloClaw",
|
||||
"sidebar.topBar.marketplace": "市场",
|
||||
"sidebar.topBar.profile": "个人资料",
|
||||
"sidebar.topBar.settings": "设置",
|
||||
"sidebar.session.newSession": "新会话",
|
||||
"sidebar.session.newSession.tooltip": "在保持当前会话完整的同时开始全新的对话。",
|
||||
"sidebar.session.newWorktree": "新 Worktree",
|
||||
|
||||
+8
@@ -299,6 +299,14 @@ export const dict = {
|
||||
"session.messages.loadEarlier": "載入更早的訊息",
|
||||
"session.messages.loading": "正在載入訊息...",
|
||||
|
||||
"sidebar.topBar.label": "Kilo Code 導覽",
|
||||
"sidebar.topBar.newTask": "新建任務",
|
||||
"sidebar.topBar.history": "歷史記錄",
|
||||
"sidebar.topBar.agentManager": "代理管理器",
|
||||
"sidebar.topBar.kiloClaw": "KiloClaw",
|
||||
"sidebar.topBar.marketplace": "市集",
|
||||
"sidebar.topBar.profile": "個人資料",
|
||||
"sidebar.topBar.settings": "設定",
|
||||
"sidebar.session.newSession": "新會話",
|
||||
"sidebar.session.newSession.tooltip": "在保持當前會話完整的同時開始全新的對話。",
|
||||
"sidebar.session.newWorktree": "新 Worktree",
|
||||
|
||||
@@ -962,6 +962,9 @@ export const SideTerminalPanelEmpty: Story = {
|
||||
state={state}
|
||||
contextKey={() => LOCAL}
|
||||
visible={() => true}
|
||||
nextKeybind="⌘⇧]"
|
||||
closeKeybind="⌘W"
|
||||
onFocusPrompt={() => undefined}
|
||||
onSelect={() => undefined}
|
||||
onClose={() => undefined}
|
||||
onCloseOthers={() => undefined}
|
||||
@@ -1004,6 +1007,9 @@ export const SideTerminalPanelTabs: Story = {
|
||||
state={state}
|
||||
contextKey={() => LOCAL}
|
||||
visible={() => true}
|
||||
nextKeybind="⌘⇧]"
|
||||
closeKeybind="⌘W"
|
||||
onFocusPrompt={() => undefined}
|
||||
onSelect={(id) => state.setSideActive(LOCAL, id)}
|
||||
onClose={() => undefined}
|
||||
onCloseOthers={() => undefined}
|
||||
|
||||
@@ -19,6 +19,7 @@ import { QuestionDock } from "../components/chat/QuestionDock"
|
||||
import { SuggestBar } from "../components/chat/SuggestBar"
|
||||
import { MessageList } from "../components/chat/MessageList"
|
||||
import { VscodeUserMessage } from "../components/chat/VscodeUserMessage"
|
||||
import { SidebarTopBar } from "../components/chat/SidebarTopBar"
|
||||
import { TurnOutcome } from "../components/shared/TurnOutcome"
|
||||
import { SessionContext } from "../context/session"
|
||||
import { ProviderContext } from "../context/provider"
|
||||
@@ -1265,6 +1266,21 @@ const mockServer = {
|
||||
gitInstalled: () => true,
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// SidebarTopBar — in-webview replacement for the native view/title toolbar
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const SidebarTopBarDefault: Story = {
|
||||
name: "SidebarTopBar — default actions",
|
||||
render: () => (
|
||||
<StoryProviders sessionID={SESSION_ID} status="idle" noPadding>
|
||||
<div style={{ width: "340px" }}>
|
||||
<SidebarTopBar onNewTask={() => undefined} onHistory={() => undefined} surface="sidebar_title" />
|
||||
</div>
|
||||
</StoryProviders>
|
||||
),
|
||||
}
|
||||
|
||||
export const WelcomeWithSwitcherAndNotification: Story = {
|
||||
name: "Welcome — account switcher + notification",
|
||||
render: () => (
|
||||
|
||||
@@ -23,6 +23,36 @@
|
||||
--chat-scrollbar-width: 10px;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
Sidebar Top Bar
|
||||
============================================ */
|
||||
|
||||
/* Always-visible in-webview equivalent of the native `view/title` toolbar, which
|
||||
disappears in the Secondary Side Bar with no way to detect or work around
|
||||
that. border-top/-bottom rule it off from the native pane title above (which
|
||||
a webview can't render into) and the chat content below. */
|
||||
.sidebar-top-bar {
|
||||
--top-bar-border: 1px solid var(--vscode-sideBar-border, var(--vscode-widget-border));
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
flex-shrink: 0;
|
||||
padding: 4px 6px;
|
||||
border-top: var(--top-bar-border);
|
||||
border-bottom: var(--top-bar-border);
|
||||
overflow-x: auto;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.sidebar-top-bar::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sidebar-top-bar-spacer {
|
||||
flex: 1;
|
||||
min-width: 4px;
|
||||
}
|
||||
|
||||
.chat-messages-wrapper {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
|
||||
@@ -202,6 +202,10 @@ export interface OpenSettingsPanelRequest {
|
||||
tab?: string
|
||||
}
|
||||
|
||||
export interface OpenProfilePanelRequest {
|
||||
type: "openProfilePanel"
|
||||
}
|
||||
|
||||
export interface OpenVSCodeSettingsRequest {
|
||||
type: "openVSCodeSettings"
|
||||
query: string
|
||||
@@ -1388,6 +1392,7 @@ export type WebviewMessage =
|
||||
| RefreshProfileRequest
|
||||
| OpenExternalRequest
|
||||
| OpenSettingsPanelRequest
|
||||
| OpenProfilePanelRequest
|
||||
| OpenVSCodeSettingsRequest
|
||||
| OpenConfigFileRequest
|
||||
| OpenMarketplacePanelRequest
|
||||
|
||||
@@ -834,11 +834,7 @@ export class RunFooter implements FooterApi {
|
||||
return
|
||||
}
|
||||
|
||||
const previous = this.currentModel()
|
||||
this.setCurrentModel(model)
|
||||
if (!previous || previous.providerID !== model.providerID || previous.modelID !== model.modelID) {
|
||||
this.setCurrentVariant(undefined)
|
||||
}
|
||||
void Promise.resolve()
|
||||
.then(() => this.options.onModelSelect?.(model))
|
||||
.then((result) => {
|
||||
|
||||
@@ -21,6 +21,8 @@ import { resolveModelInfo, resolveRunTuiConfig, resolveSessionInfo } from "./run
|
||||
import { createRuntimeLifecycle } from "./runtime.lifecycle"
|
||||
import { trace } from "./trace"
|
||||
import { cycleVariant, formatModelLabel, resolveSavedVariant, resolveVariant, saveVariant } from "./variant.shared"
|
||||
// kilocode_change - preserve compatible variants when switching models
|
||||
import { resolvePreservedVariant } from "@/kilocode/cli/cmd/run/variant" // kilocode_change
|
||||
import type { LocalReplayAnchor, LocalReplayRow, RunInput, RunPrompt, RunProvider, StreamCommit } from "./types"
|
||||
|
||||
/** @internal Exported for testing */
|
||||
@@ -294,8 +296,9 @@ async function runInteractiveRuntime(input: RunRuntimeInput, deps: RunRuntimeDep
|
||||
return
|
||||
}
|
||||
|
||||
// kilocode_change start - preserve the active effort across model switches
|
||||
const previous = state.activeVariant
|
||||
state.model = model
|
||||
state.activeVariant = undefined
|
||||
state.variants = variantsFor(state.providers, model)
|
||||
const switching = resolveSavedVariant(model).then((saved) => {
|
||||
const current = state.model
|
||||
@@ -303,8 +306,12 @@ async function runInteractiveRuntime(input: RunRuntimeInput, deps: RunRuntimeDep
|
||||
return
|
||||
}
|
||||
|
||||
state.activeVariant = resolveVariant(ctx.variant, undefined, saved, state.variants)
|
||||
// kilocode_change - prefer the active effort over a model-specific saved preference
|
||||
state.activeVariant =
|
||||
resolvePreservedVariant(ctx.variant, previous, state.variants) ??
|
||||
resolveVariant(ctx.variant, undefined, saved, state.variants)
|
||||
})
|
||||
// kilocode_change end
|
||||
state.switching = switching
|
||||
await switching
|
||||
if (state.switching === switching) {
|
||||
@@ -440,10 +447,14 @@ async function runInteractiveRuntime(input: RunRuntimeInput, deps: RunRuntimeDep
|
||||
state.variants = variantsFor(state.providers, state.model)
|
||||
state.limits = info.limits
|
||||
|
||||
const next = resolveVariant(ctx.variant, session.variant, savedVariant, state.variants)
|
||||
// kilocode_change start - preserve the active effort when the model catalog arrives asynchronously
|
||||
const next =
|
||||
resolvePreservedVariant(ctx.variant, state.activeVariant, state.variants) ??
|
||||
resolveVariant(ctx.variant, session.variant, savedVariant, state.variants)
|
||||
if (next !== state.activeVariant) {
|
||||
state.activeVariant = next
|
||||
}
|
||||
// kilocode_change end
|
||||
|
||||
if (footer.isClosed) {
|
||||
return
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
const effort = ["none", "minimal", "low", "medium", "high", "xhigh", "max"]
|
||||
|
||||
/** Keep an explicit CLI variant verbatim; only infer a fallback for automatic selections. */
|
||||
export function resolvePreservedVariant(
|
||||
input: string | undefined,
|
||||
current: string | undefined,
|
||||
variants: string[],
|
||||
): string | undefined {
|
||||
return input ?? preserveVariant(current, variants)
|
||||
}
|
||||
|
||||
/** Keep the selected effort when possible, falling back to the nearest known effort. */
|
||||
export function preserveVariant(current: string | undefined, variants: string[]): string | undefined {
|
||||
if (!current || variants.length === 0) return undefined
|
||||
if (variants.includes(current)) return current
|
||||
|
||||
const rank = effort.indexOf(current)
|
||||
if (rank === -1) return undefined
|
||||
|
||||
return variants
|
||||
.map((value, index) => ({
|
||||
value,
|
||||
index,
|
||||
rank: effort.indexOf(value),
|
||||
distance: Math.abs(effort.indexOf(value) - rank),
|
||||
}))
|
||||
.filter((item) => effort.includes(item.value))
|
||||
.sort((a, b) => a.distance - b.distance || b.rank - a.rank || a.index - b.index)[0]?.value
|
||||
}
|
||||
@@ -117,7 +117,7 @@ const WireParams = Schema.Struct({
|
||||
),
|
||||
filter: Schema.optional(ListParams.fields.filter),
|
||||
sessionID: Schema.optional(
|
||||
SessionID.annotate({ description: "For move, use a session ID returned by action=list." }),
|
||||
Schema.String.annotate({ description: "For move, use a session ID returned by action=list (IDs start with ses_)." }),
|
||||
),
|
||||
prompt: Schema.optional(PromptParams.fields.prompt),
|
||||
sectionID: Schema.optional(MoveParams.fields.sectionID),
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { Effect, Layer, ManagedRuntime, Queue } from "effect"
|
||||
import { Effect, Layer, ManagedRuntime, Queue, Schema } from "effect"
|
||||
import { MessageID, SessionID } from "../../src/session/schema"
|
||||
import { provideTmpdirInstance } from "../fixture/fixture"
|
||||
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
|
||||
import { AgentManagerTool } from "../../src/kilocode/tool/agent-manager"
|
||||
import { AgentManagerTool, Params } from "../../src/kilocode/tool/agent-manager"
|
||||
import { AgentManagerEvent, type AgentManagerStart } from "../../src/kilocode/agent-manager/event"
|
||||
import { AgentManager } from "../../src/kilocode/agent-manager/service"
|
||||
import { Bus } from "../../src/bus"
|
||||
@@ -164,8 +164,9 @@ describe("agent_manager tool", () => {
|
||||
expect(action && typeof action === "object" ? action.description : undefined).toContain("Use list first")
|
||||
expect(action && typeof action === "object" ? action.description : undefined).toContain("Never edit")
|
||||
expect(schema.properties?.sessionID).toEqual(
|
||||
expect.objectContaining({ description: expect.stringContaining("returned by action=list") }),
|
||||
expect.objectContaining({ description: expect.stringContaining("IDs start with ses_") }),
|
||||
)
|
||||
expect(schema.properties?.sessionID).not.toHaveProperty("pattern")
|
||||
expect(schema.properties?.sectionID).toEqual(
|
||||
expect.objectContaining({ description: expect.stringContaining("Use null to unassign") }),
|
||||
)
|
||||
@@ -186,6 +187,11 @@ describe("agent_manager tool", () => {
|
||||
])
|
||||
})
|
||||
|
||||
test("keeps session ID validation local", () => {
|
||||
expect(Schema.is(Params)({ action: "stop", sessionID: "ses_target" })).toBe(true)
|
||||
expect(Schema.is(Params)({ action: "stop", sessionID: "invalid" })).toBe(false)
|
||||
})
|
||||
|
||||
test("asks for agent_manager permission", async () => {
|
||||
const tool = await init()
|
||||
const calls: unknown[] = []
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user