diff --git a/packages/kilo-vscode/src/agent-manager/AgentManagerProvider.ts b/packages/kilo-vscode/src/agent-manager/AgentManagerProvider.ts index 6b78625095..5437543cb1 100644 --- a/packages/kilo-vscode/src/agent-manager/AgentManagerProvider.ts +++ b/packages/kilo-vscode/src/agent-manager/AgentManagerProvider.ts @@ -1543,6 +1543,16 @@ export class AgentManagerProvider implements Disposable { await this.onCreateWorktree(baseBranch, branchName) } + public async openAdvancedWorktree(): Promise { + this.openPanel() + const panel = this.panel + if (!panel) return + await panel.waitForActive() + await panel.waitForReady() + await this.waitForStateReady("openAdvancedWorktree") + queueMicrotask(() => this.postToWebview({ type: "action", action: "advancedWorktree" })) + } + private handleSection(m: AgentManagerInMessage): boolean { return handleSection(this.state, m, () => this.pushState()) } diff --git a/packages/kilo-vscode/src/agent-manager/host.ts b/packages/kilo-vscode/src/agent-manager/host.ts index 1f86425194..92970ecac7 100644 --- a/packages/kilo-vscode/src/agent-manager/host.ts +++ b/packages/kilo-vscode/src/agent-manager/host.ts @@ -56,6 +56,12 @@ export interface PanelContext { /** Send a message to the webview. */ postMessage(msg: unknown): void + /** Resolve once the panel webview is ready to receive messages. */ + waitForReady(): Promise + + /** Resolve once the panel is the active editor tab. */ + waitForActive(): Promise + /** Reveal the panel. */ reveal(preserveFocus?: boolean): void diff --git a/packages/kilo-vscode/src/agent-manager/vscode-host.ts b/packages/kilo-vscode/src/agent-manager/vscode-host.ts index 06370eae2f..8b5ebfdc72 100644 --- a/packages/kilo-vscode/src/agent-manager/vscode-host.ts +++ b/packages/kilo-vscode/src/agent-manager/vscode-host.ts @@ -110,6 +110,19 @@ export class VscodeHost implements Host { postMessage(msg) { void panel.webview.postMessage(msg) }, + waitForReady() { + return provider.waitForReady() + }, + waitForActive() { + if (panel.active) return Promise.resolve() + return new Promise((resolve) => { + const sub = panel.onDidChangeViewState((e) => { + if (!e.webviewPanel.active) return + sub.dispose() + resolve() + }) + }) + }, reveal(preserveFocus) { panel.reveal(vscode.ViewColumn.One, preserveFocus ?? false) }, diff --git a/packages/kilo-vscode/src/extension.ts b/packages/kilo-vscode/src/extension.ts index 2b6f5e8ba3..a6f4905c31 100644 --- a/packages/kilo-vscode/src/extension.ts +++ b/packages/kilo-vscode/src/extension.ts @@ -338,9 +338,9 @@ export function activate(context: vscode.ExtensionContext) { vscode.commands.registerCommand("kilo-code.new.agentManager.closeWorktree", () => { agentManagerProvider.postMessage({ type: "action", action: "closeWorktree" }) }), - vscode.commands.registerCommand("kilo-code.new.agentManager.advancedWorktree", () => { - agentManagerProvider.postMessage({ type: "action", action: "advancedWorktree" }) - }), + vscode.commands.registerCommand("kilo-code.new.agentManager.advancedWorktree", () => + agentManagerProvider.openAdvancedWorktree(), + ), ...Array.from({ length: 9 }, (_, i) => vscode.commands.registerCommand(`kilo-code.new.agentManager.jumpTo${i + 1}`, () => { agentManagerProvider.postMessage({ type: "action", action: `jumpTo${i + 1}` }) diff --git a/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx b/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx index 401be6510a..4239f4e735 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx @@ -1,7 +1,8 @@ // Agent Manager root component +/** @jsxImportSource solid-js */ + import { - Component, For, Show, createSignal, @@ -11,6 +12,7 @@ import { onMount, onCleanup, type Accessor, + type Component, } from "solid-js" import type { ExtensionMessage, @@ -2285,7 +2287,7 @@ const AgentManagerContent: Component = () => { - {t("agentManager.dialog.advanced")} + {t("agentManager.dialog.configureWorktree")} {parseBindingTokens(kb().advancedWorktree ?? "").map((token) => ( {token} diff --git a/packages/kilo-vscode/webview-ui/agent-manager/NewWorktreeDialog.tsx b/packages/kilo-vscode/webview-ui/agent-manager/NewWorktreeDialog.tsx index 13f5692bf6..3e54451f4e 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/NewWorktreeDialog.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/NewWorktreeDialog.tsx @@ -1,6 +1,8 @@ // New Worktree dialog — prompt, versions, model, mode, import tab -import { Component, For, Show, createSignal, createEffect, createMemo, onMount, onCleanup } from "solid-js" +/** @jsxImportSource solid-js */ + +import { type Component, For, Show, createSignal, createEffect, createMemo, onMount, onCleanup } from "solid-js" import type { AgentManagerBranchesMessage, AgentManagerImportResultMessage, BranchInfo } from "../src/types/messages" import { Dialog } from "@kilocode/kilo-ui/dialog" import { showToast } from "@kilocode/kilo-ui/toast" @@ -171,6 +173,17 @@ export const NewWorktreeDialog: Component<{ onClose: () => void; defaultBaseBran vscode.postMessage({ type: "agentManager.requestBranches" }) // Resize textarea if restoring a cached prompt if (prompt()) adjustHeight() + const focus = () => { + textareaRef?.focus({ preventScroll: true }) + const end = textareaRef?.value.length ?? 0 + textareaRef?.setSelectionRange(end, end) + } + requestAnimationFrame(() => { + focus() + requestAnimationFrame(focus) + setTimeout(focus, 0) + setTimeout(focus, 50) + }) }) const effectiveBaseBranch = () => baseBranch() ?? defaultBranch() diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/en.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/en.ts index bde300bd16..e564072f3a 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/en.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/en.ts @@ -77,7 +77,7 @@ export const dict = { "agentManager.shortcuts.nextItem": "Next item", "agentManager.shortcuts.newWorktree": "New worktree", "agentManager.shortcuts.openWorktree": "Open worktree", - "agentManager.shortcuts.advancedWorktree": "Advanced worktree", + "agentManager.shortcuts.advancedWorktree": "Configure new worktree", "agentManager.shortcuts.deleteWorktree": "Delete worktree", "agentManager.shortcuts.previousTab": "Previous tab", "agentManager.shortcuts.nextTab": "Next tab", @@ -122,6 +122,7 @@ export const dict = { "agentManager.dialog.createWorktree": "Create Worktree", "agentManager.dialog.removeImage": "Remove image", "agentManager.dialog.advanced": "Advanced...", + "agentManager.dialog.configureWorktree": "Configure New Worktree...", "agentManager.diff.toggle": "Toggle diff", "agentManager.diff.openFile": "Open file", diff --git a/packages/kilo-vscode/webview-ui/src/components/chat/ChatView.tsx b/packages/kilo-vscode/webview-ui/src/components/chat/ChatView.tsx index dbf4cea2a9..819b95eaa3 100644 --- a/packages/kilo-vscode/webview-ui/src/components/chat/ChatView.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/chat/ChatView.tsx @@ -54,6 +54,7 @@ export const ChatView: Component = (props) => { const [transferring, setTransferring] = createSignal(false) const [transferDetail, setTransferDetail] = createSignal("") const [repoBranch, setRepoBranch] = createSignal() + let worktreeRef: HTMLDivElement | undefined // Permissions and questions scoped to this session's family (self + subagents). // Each ChatView only sees its own session tree — no cross-session leakage. @@ -146,7 +147,10 @@ export const ChatView: Component = (props) => { const openAgentManager = () => vscode.postMessage({ type: "openAgentManager" }) + const openChanges = () => vscode.postMessage({ type: "openChanges" }) + const moveToWorktree = () => { + if (transferring()) return const sid = id() if (!sid) return setTransferring(true) @@ -159,6 +163,19 @@ export const ChatView: Component = (props) => { const advancedTooltip = "Open the Agent Manager worktree dialog to configure a new worktree before creating it." + const moveTooltip = () => { + const stats = session.worktreeStats() + if (!stats?.files) + return "Move this conversation and your current local changes into a dedicated worktree for isolated follow-up work." + return `Move this conversation and ${stats.files} changed file${stats.files > 1 ? "s" : ""} into a dedicated worktree for isolated follow-up work.` + } + + const changesTooltip = () => { + const stats = session.worktreeStats() + if (!stats?.files) return "Open the changes view to inspect the current working tree." + return `${stats.files} file${stats.files > 1 ? "s" : ""} changed · +${stats.additions} -${stats.deletions}. Open the changes view.` + } + const showAdvancedWorktree = () => vscode.postMessage({ type: "openAdvancedWorktree" }) createEffect(() => { @@ -169,13 +186,27 @@ export const ChatView: Component = (props) => { const renderActions = (hasChat: boolean) => (
- - -
+
- + worktreeRef?.getBoundingClientRect()}> @@ -207,7 +238,9 @@ export const ChatView: Component = (props) => { - Configure New Worktree... + + {language.t("agentManager.dialog.configureWorktree")} + @@ -216,42 +249,42 @@ export const ChatView: Component = (props) => { - 1 ? "s" : ""} into a dedicated worktree for isolated follow-up work.` - : "Move this conversation and your current local changes into a dedicated worktree for isolated follow-up work." - } - placement="top" - > - - +
+ + + + + + +
= (props) => { variant="ghost" size="small" onClick={() => void handleSend()} - disabled={!canSend()} + aria-disabled={!canSend()} aria-label={language.t("prompt.action.send")} > diff --git a/packages/kilo-vscode/webview-ui/src/styles/session-actions.css b/packages/kilo-vscode/webview-ui/src/styles/session-actions.css index a816c6914e..901b8c04fa 100644 --- a/packages/kilo-vscode/webview-ui/src/styles/session-actions.css +++ b/packages/kilo-vscode/webview-ui/src/styles/session-actions.css @@ -63,6 +63,10 @@ color: var(--vscode-foreground); } +.session-actions-row > [data-component="tooltip-trigger"] > [data-component="button"].session-new-button:disabled { + cursor: default; +} + .session-worktree-split { display: flex; flex: 0 0 auto; @@ -177,22 +181,57 @@ font-weight: 600; } -/* Diff wrapper also stretches evenly (same flex as buttons) */ -.session-move-button { +.session-move-group { + display: flex; + flex: 0 0 auto; + align-items: center; + gap: 2px; + border-radius: var(--session-action-radius); + color: var(--vscode-descriptionForeground); + transition: + background-color 150ms ease, + color 150ms ease; +} + +.session-move-group > [data-component="tooltip-trigger"] { + display: flex; + flex: 0 0 auto; +} + +.session-move-group:hover, +.session-move-group:has(.session-move-action:focus-visible), +.session-move-group:has(.session-move-changes:focus-visible) { + color: var(--vscode-foreground); +} + +.session-move-group:hover .session-move-action, +.session-move-group:hover .session-move-changes, +.session-move-group:has(.session-move-action:focus-visible) .session-move-action, +.session-move-group:has(.session-move-action:focus-visible) .session-move-changes, +.session-move-group:has(.session-move-changes:focus-visible) .session-move-action, +.session-move-group:has(.session-move-changes:focus-visible) .session-move-changes { + background: var(--surface-base-hover); +} + +.session-move-action { display: flex; flex: 0 0 auto; align-items: center; justify-content: center; gap: 5px; - border-radius: var(--session-action-radius); - color: var(--vscode-descriptionForeground); + border-radius: var(--session-action-radius) 0 0 var(--session-action-radius); + color: inherit; padding: 4px 6px !important; + background: transparent; + box-shadow: none; } -.session-move-button:hover:not(:disabled), -.session-move-button:focus-visible:not(:disabled), -.session-move-button:active:not(:disabled) { - color: var(--vscode-foreground); +.session-move-action:hover:not(:disabled), +.session-move-action:focus-visible:not(:disabled), +.session-move-action:active:not(:disabled) { + color: inherit; + background: inherit; + box-shadow: none; } .session-move-label { @@ -202,31 +241,41 @@ white-space: nowrap; } -.session-move-tail { - display: inline-flex; - align-items: center; - gap: 5px; - flex-shrink: 0; - padding-left: 5px; -} - -.session-move-divider { - width: 1px; - height: 14px; - background: color-mix(in srgb, currentColor 25%, transparent); +.session-move-action [data-component="icon"], +.session-move-changes [data-component="icon"] { opacity: 0.7; } -.session-move-stats { +.session-move-changes { display: inline-flex; align-items: center; gap: 3px; - flex-shrink: 0; + flex: 0 0 auto; + border-radius: 0 var(--session-action-radius) var(--session-action-radius) 0; + color: inherit; + padding: 4px 6px !important; + background: transparent; + box-shadow: none; +} + +.session-move-changes:hover:not(:disabled), +.session-move-changes:focus-visible:not(:disabled), +.session-move-changes:active:not(:disabled) { + color: inherit; + background: inherit; + box-shadow: none; +} + +.session-move-changes { font-family: var(--font-family-sans); font-size: 12px; font-variant-numeric: tabular-nums; } +.session-move-changes--empty { + opacity: 0.7; +} + .session-agent-manager-slot { flex: 0 0 auto; margin-left: auto; @@ -254,10 +303,6 @@ color: var(--vscode-foreground); } -.session-move-stats [data-component="icon"] { - opacity: 0.7; -} - /* Diff stats badge styles retained for the inline move button stats */ .session-diff-badge { display: flex; @@ -315,21 +360,16 @@ margin-left: 0; } - .session-move-divider, .session-diff-add, .session-diff-del { display: none; } - .session-move-tail { - padding-left: 4px; - } - - .session-move-stats { + .session-move-changes { position: relative; } - .session-move-button--has-changes .session-move-stats [data-component="icon"]::after { + .session-move-changes--has-changes [data-component="icon"]::after { content: ""; position: absolute; top: -2px;