diff --git a/.changeset/search-agent-manager-sidebar.md b/.changeset/search-agent-manager-sidebar.md new file mode 100644 index 00000000000..e47bae5fc41 --- /dev/null +++ b/.changeset/search-agent-manager-sidebar.md @@ -0,0 +1,5 @@ +--- +"kilo-code": minor +--- + +Search and switch between local sessions, worktrees, and their sessions from the Agent Manager sidebar. diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/sidebar-search-open-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/sidebar-search-open-chromium-linux.png new file mode 100644 index 00000000000..3a21dccf87c --- /dev/null +++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/sidebar-search-open-chromium-linux.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:29bb5361de5ac4b7acf247b8f9976c715c4e300448f6e504ec791566eca91219 +size 25750 diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/tab-bar-multiple-tabs-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/tab-bar-multiple-tabs-chromium-linux.png index 6ab79b4689a..23c66f21c9f 100644 --- a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/tab-bar-multiple-tabs-chromium-linux.png +++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/tab-bar-multiple-tabs-chromium-linux.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b7ec87f1e046cf1a44688b952c0d994899119c6663bf627026688bf337b56127 -size 2411 +oid sha256:1370f2dcf5c50258971e0665ea151f2c522f566619745d58d6a10f3914949e6c +size 2521 diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/tab-bar-single-tab-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/tab-bar-single-tab-chromium-linux.png index 905e28d6b85..8e3f4a2492e 100644 --- a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/tab-bar-single-tab-chromium-linux.png +++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/tab-bar-single-tab-chromium-linux.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7ee0856c65e445a230cf3e94bfe592c98306b7502e44a2602b5fac2f6d69937a -size 4601 +oid sha256:b8af37e0c169811092437b71110c63571ae43c4d35e12f6890be8295a3cb67da +size 4569 diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/tab-bar-with-review-tab-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/tab-bar-with-review-tab-chromium-linux.png index 83fda13fbb4..3eb073991f9 100644 --- a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/tab-bar-with-review-tab-chromium-linux.png +++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/tab-bar-with-review-tab-chromium-linux.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f1ee2d880a055b03664d594598a6e8764ab27d443a578572fd01b37451cb0193 -size 3193 +oid sha256:16fdb6361199c58a8e99ad2661dc47791b0f4737ada16760151a26e7ebdd8bd1 +size 3044 diff --git a/packages/kilo-vscode/package.json b/packages/kilo-vscode/package.json index 876b6f9e9f4..a065675d6ce 100644 --- a/packages/kilo-vscode/package.json +++ b/packages/kilo-vscode/package.json @@ -235,6 +235,11 @@ "title": "Agent Manager: Next Tab", "category": "Kilo Code" }, + { + "command": "kilo-code.new.agentManager.search", + "title": "Agent Manager: Search Worktrees and Sessions", + "category": "Kilo Code" + }, { "command": "kilo-code.new.agentManager.showTerminal", "title": "Agent Manager: Focus Terminal", @@ -629,6 +634,12 @@ "mac": "cmd+alt+right", "when": "activeWebviewPanelId == 'kilo-code.new.AgentManagerPanel'" }, + { + "command": "kilo-code.new.agentManager.search", + "key": "ctrl+f", + "mac": "cmd+f", + "when": "activeWebviewPanelId == 'kilo-code.new.AgentManagerPanel' && !terminalFocus" + }, { "command": "kilo-code.new.agentManager.showTerminal", "key": "ctrl+/", diff --git a/packages/kilo-vscode/src/agent-manager/format-keybinding.ts b/packages/kilo-vscode/src/agent-manager/format-keybinding.ts index daa15a3af59..c6a4b05f150 100644 --- a/packages/kilo-vscode/src/agent-manager/format-keybinding.ts +++ b/packages/kilo-vscode/src/agent-manager/format-keybinding.ts @@ -67,6 +67,7 @@ export function buildKeybindingMap( // Ensure fallback bindings are always present (may be missing from // cached packageJSON if the extension hasn't been fully reloaded) + if (!bindings.search) bindings.search = formatKeybinding(mac ? "cmd+f" : "ctrl+f", mac) 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) diff --git a/packages/kilo-vscode/src/extension.ts b/packages/kilo-vscode/src/extension.ts index 35dc3073d2b..815124b225a 100644 --- a/packages/kilo-vscode/src/extension.ts +++ b/packages/kilo-vscode/src/extension.ts @@ -435,6 +435,9 @@ 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.search", () => { + agentManagerProvider.postMessage({ type: "action", action: "search" }) + }), vscode.commands.registerCommand("kilo-code.new.agentManager.showTerminal", () => { // Route through the webview so it can reach into the active session // state and open the VS Code integrated terminal for it. diff --git a/packages/kilo-vscode/tests/accessibility.spec.ts b/packages/kilo-vscode/tests/accessibility.spec.ts index 4600a9f0cd6..2f1565ab4df 100644 --- a/packages/kilo-vscode/tests/accessibility.spec.ts +++ b/packages/kilo-vscode/tests/accessibility.spec.ts @@ -13,6 +13,7 @@ const STORIES = [ { id: "settings--providers-configure", name: "Settings / providers empty state" }, { id: "marketplace--skills-tab-empty", name: "Marketplace / skills empty state" }, { id: "marketplace--agents-tab-empty", name: "Marketplace / agents empty state" }, + { id: "agentmanager--sidebar-search-open", name: "Agent Manager / sidebar search" }, ] function url(id: string) { @@ -61,4 +62,61 @@ test.describe("webview accessibility ratchet", () => { await page.keyboard.press("Enter") await expect(login).toHaveAttribute("data-keyboard-activated", "true") }) + + test("Agent Manager sidebar search filters and selects with the keyboard", async ({ page }) => { + await open(page, "agentmanager--sidebar-search-open") + + const trigger = page.getByRole("button", { name: "Search worktrees and sessions" }) + const input = page.getByPlaceholder("Search worktrees and sessions", { exact: true }) + const prompt = page.getByRole("textbox", { name: "Story prompt" }) + await expect(input).toBeFocused() + await expect(page.locator('[data-slot="list-header"]')).toHaveText(["SESSIONS", "LOCAL & WORKTREES"]) + + await input.fill("Render") + await expect(page.locator('[data-slot="sidebar-search-result"]').first()).toContainText( + "Render images in diff viewer", + ) + await input.fill("rndr img") + await expect(page.locator('[data-slot="sidebar-search-result"]').first()).toContainText( + "Render images in diff viewer", + ) + + await input.fill("main") + await expect(page.locator('[data-slot="sidebar-search-result"]').first()).toContainText("local") + await page.keyboard.press("Enter") + await expect(page.locator('[data-slot="sidebar-search-selection"]')).toHaveText("local") + await expect(prompt).toBeFocused() + + await trigger.click() + await input.fill("Build grouped") + await expect(page.getByText("Build grouped worktree search", { exact: true })).toBeVisible() + + await page.keyboard.press("Enter") + await expect(page.locator('[data-slot="sidebar-search-selection"]')).toHaveText("session:session-build") + await expect(input).toBeHidden() + await expect(prompt).toBeFocused() + + await trigger.click() + await input.fill("local indexing") + await expect(page.getByText("Investigate local indexing", { exact: true })).toBeVisible() + await page.keyboard.press("Enter") + await expect(page.locator('[data-slot="sidebar-search-selection"]')).toHaveText("session:session-local") + + await trigger.click() + await input.fill("does not exist") + await expect(page.locator('[data-slot="list-empty-state"]')).toBeVisible() + await page.keyboard.press("Escape") + await expect(prompt).toBeFocused() + + await trigger.click() + await expect(input).toBeFocused() + await page.locator(".am-section-label").click() + await expect(prompt).toBeFocused() + + await trigger.hover() + await expect( + page.getByText("Searches the local workspace, local sessions, worktrees, and their sessions", { exact: true }), + ).toBeVisible() + await expect(page.getByText("⌘F", { exact: true })).toBeVisible() + }) }) diff --git a/packages/kilo-vscode/tests/unit/agent-manager-arch.test.ts b/packages/kilo-vscode/tests/unit/agent-manager-arch.test.ts index d45c3873e2e..90d1d1cc17d 100644 --- a/packages/kilo-vscode/tests/unit/agent-manager-arch.test.ts +++ b/packages/kilo-vscode/tests/unit/agent-manager-arch.test.ts @@ -35,8 +35,9 @@ const TSX_FILES = [ path.join(ROOT, "webview-ui/agent-manager/ApplyDialog.tsx"), path.join(ROOT, "webview-ui/agent-manager/WorktreeItem.tsx"), path.join(ROOT, "webview-ui/agent-manager/SectionHeader.tsx"), - path.join(ROOT, "webview-ui/agent-manager/CurrentTabsMenu.tsx"), + path.join(ROOT, "webview-ui/agent-manager/SidebarSearchMenu.tsx"), path.join(ROOT, "webview-ui/agent-manager/SidebarToggleButton.tsx"), + path.join(ROOT, "webview-ui/agent-manager/WorktreeSectionActions.tsx"), path.join(ROOT, "webview-ui/agent-manager/tab-rendering.tsx"), path.join(ROOT, "webview-ui/agent-manager/terminal/TerminalTab.tsx"), path.join(ROOT, "webview-ui/agent-manager/terminal/SortableTerminalTab.tsx"), diff --git a/packages/kilo-vscode/tests/unit/extension-arch.test.ts b/packages/kilo-vscode/tests/unit/extension-arch.test.ts index 174989a2611..f2d07811517 100644 --- a/packages/kilo-vscode/tests/unit/extension-arch.test.ts +++ b/packages/kilo-vscode/tests/unit/extension-arch.test.ts @@ -103,6 +103,17 @@ describe("Extension — package.json command sync", () => { `Commands without "kilo-code.new." prefix — use the namespaced form:\n` + bad.map((b) => ` - ${b}`).join("\n"), ).toEqual([]) }) + + it("scopes Agent Manager search to the panel and leaves the integrated terminal alone", () => { + const binding = pkg.contributes?.keybindings?.find( + (item: { command: string }) => item.command === "kilo-code.new.agentManager.search", + ) + expect(binding).toMatchObject({ + key: "ctrl+f", + mac: "cmd+f", + when: "activeWebviewPanelId == 'kilo-code.new.AgentManagerPanel' && !terminalFocus", + }) + }) }) // --------------------------------------------------------------------------- diff --git a/packages/kilo-vscode/tests/unit/format-keybinding.test.ts b/packages/kilo-vscode/tests/unit/format-keybinding.test.ts index 564b8a9ef3a..8320b94cd08 100644 --- a/packages/kilo-vscode/tests/unit/format-keybinding.test.ts +++ b/packages/kilo-vscode/tests/unit/format-keybinding.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from "bun:test" -import { formatKeybinding } from "../../src/agent-manager/format-keybinding" +import { buildKeybindingMap, formatKeybinding } from "../../src/agent-manager/format-keybinding" describe("formatKeybinding", () => { describe("mac", () => { @@ -70,3 +70,11 @@ describe("formatKeybinding", () => { }) }) }) + +describe("buildKeybindingMap", () => { + it("maps the configurable Agent Manager search shortcut", () => { + const bindings = [{ command: "kilo-code.new.agentManager.search", key: "ctrl+f", mac: "cmd+f" }] + expect(buildKeybindingMap(bindings, true).search).toBe("⌘F") + expect(buildKeybindingMap(bindings, false).search).toBe("Ctrl+F") + }) +}) diff --git a/packages/kilo-vscode/tests/unit/sidebar-search.test.ts b/packages/kilo-vscode/tests/unit/sidebar-search.test.ts new file mode 100644 index 00000000000..9eeccde33b0 --- /dev/null +++ b/packages/kilo-vscode/tests/unit/sidebar-search.test.ts @@ -0,0 +1,154 @@ +import { describe, expect, it } from "bun:test" +import type { SectionState, SessionInfo, WorktreeState } from "../../webview-ui/src/types/messages" +import { buildSidebarSearch } from "../../webview-ui/agent-manager/sidebar-search" +import { buildShortcutCategories } from "../../webview-ui/agent-manager/shortcuts" + +const session = (id: string, title: string, updatedAt: string, parentID?: string): SessionInfo => ({ + id, + title, + parentID, + createdAt: "2026-06-01T00:00:00.000Z", + updatedAt, +}) + +const worktree = (id: string, branch: string, sectionId?: string): WorktreeState => ({ + id, + branch, + path: `/tmp/${id}`, + parentBranch: "main", + createdAt: "2026-06-01T00:00:00.000Z", + sectionId, +}) + +const section: SectionState = { + id: "section-polish", + name: "Polish", + color: "Blue", + order: 0, + collapsed: true, +} + +const build = (overrides?: Partial[0]>) => + buildSidebarSearch({ + worktrees: [ + { + worktree: worktree("wt-search", "feat/sidebar-search", section.id), + label: "Agent Manager search", + sessions: [ + session("busy-session", "Build grouped search", "2026-06-02T00:00:00.000Z"), + session("recent-session", "Review search UI", "2026-06-03T00:00:00.000Z"), + ], + }, + { + worktree: worktree("wt-other", "fix/other"), + label: "Other worktree", + sessions: [session("other-session", "Other session", "2026-06-04T00:00:00.000Z")], + }, + ], + sections: [section], + local: [session("local-session", "Local investigation", "2026-06-05T00:00:00.000Z")], + localLabel: "local", + localBranch: "main", + untitled: "Untitled", + pending: (id) => id.startsWith("pending:"), + status: (id) => (id === "busy-session" ? "busy" : "idle"), + busy: () => false, + localBusy: false, + ...overrides, + }) + +describe("buildSidebarSearch", () => { + it("indexes worktree sessions with section context and excludes non-root tabs", () => { + const items = build({ + worktrees: [ + { + worktree: worktree("wt-search", "feat/sidebar-search", section.id), + label: "Agent Manager search", + sessions: [ + session("session", "Build grouped search", "2026-06-02T00:00:00.000Z"), + session("pending:1", "New Session", "2026-06-03T00:00:00.000Z"), + session("child", "Subagent", "2026-06-04T00:00:00.000Z", "session"), + ], + }, + ], + local: [], + }) + + expect(items).toHaveLength(3) + expect(items[0]).toMatchObject({ + kind: "session", + sessionId: "session", + worktreeId: "wt-search", + meta: ["Polish", "Agent Manager search", "feat/sidebar-search"], + section: { id: section.id, color: "Blue", collapsed: true }, + }) + expect(items[0]?.search).toContain("Build grouped search Agent Manager search feat/sidebar-search Polish") + expect(items[1]).toMatchObject({ kind: "local", title: "local", meta: ["main"], count: 0 }) + expect(items[2]).toMatchObject({ kind: "worktree", worktreeId: "wt-search", count: 1, visible: false }) + }) + + it("ranks attention and progress before recency within each result group", () => { + const items = build() + + expect(items.map((item) => item.key)).toEqual([ + "session:busy-session", + "session:local-session", + "session:other-session", + "session:recent-session", + "worktree:wt-search", + "local", + "worktree:wt-other", + ]) + expect(items[0]).toMatchObject({ state: "busy", updatedAt: "2026-06-02T00:00:00.000Z" }) + expect(items[1]).toMatchObject({ location: "local", meta: ["local"] }) + expect(items[4]).toMatchObject({ state: "busy", updatedAt: "2026-06-03T00:00:00.000Z" }) + }) + + it("uses expanded sidebar visibility before recency as a tie-breaker", () => { + const items = build({ + worktrees: [ + { + worktree: worktree("wt-hidden", "feat/hidden", section.id), + label: "Same task hidden", + sessions: [session("hidden", "Same task", "2026-06-05T00:00:00.000Z")], + }, + { + worktree: worktree("wt-visible", "feat/visible"), + label: "Same task visible", + sessions: [session("visible", "Same task", "2026-06-01T00:00:00.000Z")], + }, + ], + local: [], + status: () => "idle", + }) + + expect(items.filter((item) => item.kind === "session").map((item) => item.sessionId)).toEqual(["visible", "hidden"]) + expect(items.find((item) => item.key === "session:hidden")).toMatchObject({ visible: false }) + expect(items.find((item) => item.key === "session:visible")).toMatchObject({ visible: true }) + }) + + it("avoids repeating a worktree label that matches the session title", () => { + const items = build({ + worktrees: [ + { + worktree: worktree("wt-owned", "feat/owned", section.id), + label: "Owned context", + sessions: [session("owned", "Owned context", "2026-06-02T00:00:00.000Z")], + }, + ], + local: [], + }) + + expect(items[0]).toMatchObject({ kind: "session", meta: ["Polish", "feat/owned"] }) + }) +}) + +describe("sidebar search shortcut", () => { + it("appears in the quick-switch keyboard shortcuts section", () => { + const categories = buildShortcutCategories({ search: "⌘F", jumpTo1: "⌘1" }, (key) => key) + expect(categories[0]?.shortcuts[0]).toEqual({ + label: "agentManager.sidebarSearch.label", + binding: "⌘F", + }) + }) +}) diff --git a/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx b/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx index 7f9ee074816..a1a37b1314e 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx @@ -101,7 +101,7 @@ import { reorderTabs, applyTabOrder, firstOrderedTitle } from "./tab-order" import { createTabOrderSync } from "./tab-order-sync" import { ConstrainDragYAxis } from "./sortable-tab" import { isTerminalTabId, createTerminalState, createTerminalHandlers, createTerminalMessageHandler } from "./terminal" -import { renderTab, renderTerminalLayer, renderNewTabButton } from "./tab-rendering" +import { focusCurrentTab, renderTab, renderTerminalLayer, renderNewTabButton } from "./tab-rendering" import { useTabScroll } from "./tab-scroll" import { DiffPanel } from "./DiffPanel" import { createRevertFile } from "./revert-file" @@ -110,7 +110,9 @@ import { ApplyDialog } from "./ApplyDialog" import { groupApplyConflicts } from "./apply-conflicts" import type { ReviewComment } from "../diff-viewer/review-comments" import { clearReviewComposer, createReviewComposer } from "../diff-viewer/review-annotations" -import { CurrentTabsMenu, createCurrentTabItems, focusCurrentTab } from "./CurrentTabsMenu" +import type { SidebarSearchMenuRef } from "./SidebarSearchMenu" +import { createSidebarSearch, type SidebarSearchItem } from "./sidebar-search" +import { WorktreeSectionActions } from "./WorktreeSectionActions" import { BranchSelect } from "../src/components/shared/BranchSelect" import { WorktreeItem } from "./WorktreeItem" import SectionHeader from "./SectionHeader" @@ -171,6 +173,7 @@ const defaultBindings: Record = { nextSession: isMac ? "⌘⌥↓" : "Ctrl+Alt+↓", previousTab: isMac ? "⌘⌥←" : "Ctrl+Alt+←", nextTab: isMac ? "⌘⌥→" : "Ctrl+Alt+→", + search: isMac ? "⌘F" : "Ctrl+F", showTerminal: isMac ? "⌘/" : "Ctrl+/", newTerminal: isMac ? "⌘⇧T" : "Ctrl+Shift+T", runScript: isMac ? "⌘E" : "Ctrl+E", @@ -197,6 +200,7 @@ const AgentManagerContent: Component = () => { const session = useSession() const vscode = useVSCode() const dialog = useDialog() + let sidebarSearchMenu: SidebarSearchMenuRef | undefined const [kb, setKb] = createSignal>(defaultBindings) @@ -964,6 +968,37 @@ const AgentManagerContent: Component = () => { return true } + const sidebarSearch = createSidebarSearch({ + worktrees: sortedWorktrees, + sections, + local: localSessions, + localBranch: repoBranch, + selection, + sessionId: session.currentSessionID, + statuses: session.allStatusMap, + permissions: session.permissions, + questions: session.questions, + label: worktreeLabel, + sessions: sessionsForWorktree, + pending: isPending, + busy: (id) => busyWorktrees().has(id) || (runStatuses()[id]?.state ?? "idle") !== "idle", + localBusy: isLocalBusy, + t, + }) + const focusSidebarSearchItem = (item: SidebarSearchItem) => { + if (item.section?.collapsed) + vscode.postMessage({ type: "agentManager.toggleSectionCollapsed", sectionId: item.section.id }) + setHistory(false) + if (item.kind === "local") return selectLocal() + if (item.kind === "worktree") return selectWorktree(item.worktreeId) + if (item.location === "local") selectLocal() + if (item.location === "worktree" && item.worktreeId) selectWorktree(item.worktreeId) + terms.setActiveId(undefined) + setReviewActive(false) + setActivePendingId(undefined) + session.selectSession(item.sessionId) + } + const cycleAgent = (direction: 1 | -1) => { const available = session.agents().filter((a) => a.mode !== "subagent" && !a.hidden) if (available.length <= 1) return @@ -990,7 +1025,13 @@ const AgentManagerContent: Component = () => { else if (msg.action === "sessionNext") navigate("down") else if (msg.action === "tabPrevious") navigateTab("left") else if (msg.action === "tabNext") navigateTab("right") - else if (msg.action === "showTerminal") { + else if (msg.action === "search") { + if (!sidebarCollapsed()) sidebarSearchMenu?.open() + else { + expandSidebar() + requestAnimationFrame(() => sidebarSearchMenu?.open()) + } + } else if (msg.action === "showTerminal") { // Cmd+/ opens the legacy VS Code integrated terminal for the // active session (or local). The new xterm tab affordance has // its own keybind (Cmd+Shift+T) so both coexist. @@ -2027,20 +2068,6 @@ const AgentManagerContent: Component = () => { activateTerminal: termHandlers.activate, }) - const tabMenuItems = createCurrentTabItems({ - tabIds, - tabLookup, - statusMap: session.allStatusMap, - permissions: session.permissions, - questions: session.questions, - visibleTabId, - terms, - reviewId: REVIEW_TAB_ID, - isTerminal: isTerminalTabId, - isPending, - t, - }) - // Close the currently active tab via keyboard shortcut. // If no tabs remain, fall through to close the selected worktree. const closeActiveTab = () => { @@ -2203,97 +2230,23 @@ const AgentManagerContent: Component = () => {
- -
-
- - - - - - - - - - - - {t("agentManager.dialog.configureWorktree")} - - {parseBindingTokens(kb().advancedWorktree ?? "").map((token) => ( - {token} - ))} - - - - newSection()}> - - {t("agentManager.worktree.newSection")} - - - - -
- - - - - - - - - {t("agentManager.worktree.setupScript")} - - - - - {t("agentManager.worktree.defaultBaseBranch")}: {repoDefaultBranch()} - - - - - -
-
+ (sidebarSearchMenu = value)} + onSelect={focusSidebarSearchItem} + onCreate={createWorktree} + onAdvanced={showAdvancedWorktreeDialog} + onSection={newSection} + onShortcuts={metrics.click("keyboard_shortcuts", "worktrees_header", handleShowKeyboardShortcuts)} + onSetup={setupScript} + onBranch={handleChangeDefaultBaseBranch} + />
{
-
diff --git a/packages/kilo-vscode/webview-ui/agent-manager/CurrentTabsMenu.tsx b/packages/kilo-vscode/webview-ui/agent-manager/CurrentTabsMenu.tsx deleted file mode 100644 index b1369f75318..00000000000 --- a/packages/kilo-vscode/webview-ui/agent-manager/CurrentTabsMenu.tsx +++ /dev/null @@ -1,288 +0,0 @@ -/** @jsxImportSource solid-js */ - -import { For, Show, createEffect, createMemo, createSignal } from "solid-js" -import type { Accessor, Component } from "solid-js" -import { Popover } from "@kilocode/kilo-ui/popover" -import { Spinner } from "@kilocode/kilo-ui/spinner" -import { Tooltip } from "@kilocode/kilo-ui/tooltip" -import type { PermissionRequest, QuestionRequest, SessionInfo, SessionStatusInfo } from "../src/types/messages" -import type { TerminalStateControls } from "./terminal" - -interface CurrentTabItem { - id: string - title: string - status?: string - working: boolean - tone: "active" | "busy" | "waiting" | "idle" -} - -interface CurrentTabItemsDeps { - tabIds: Accessor - tabLookup: Accessor> - statusMap: Accessor> - permissions: Accessor - questions: Accessor - visibleTabId: Accessor - terms: TerminalStateControls - reviewId: string - isTerminal: (id: string) => boolean - isPending: (id: string) => boolean - t: (key: string) => string -} - -interface FocusTabDeps { - id: string - terms: TerminalStateControls - isTerminal: (id: string) => boolean - isPending: (id: string) => boolean - reviewId: string - reviewOpen: Accessor - setReviewOpen: (open: boolean) => void - setReviewActive: (active: boolean) => void - tabLookup: Accessor> - setActivePendingId: (id: string | undefined) => void - clearSession: () => void - selectSession: (id: string) => void - activateTerminal: (id: string) => void -} - -export function focusCurrentTab(deps: FocusTabDeps) { - if (deps.isTerminal(deps.id)) { - deps.activateTerminal(deps.id) - return - } - deps.terms.setActiveId(undefined) - if (deps.id === deps.reviewId) { - if (!deps.reviewOpen()) deps.setReviewOpen(true) - deps.setReviewActive(true) - return - } - const target = deps.tabLookup().get(deps.id) - if (!target) return - deps.setReviewActive(false) - if (deps.isPending(target.id)) { - deps.setActivePendingId(target.id) - deps.clearSession() - return - } - deps.setActivePendingId(undefined) - deps.selectSession(target.id) -} - -export const createCurrentTabItems = (deps: CurrentTabItemsDeps): Accessor => - createMemo(() => { - const statuses = deps.statusMap() - const perms = deps.permissions() - const qs = deps.questions() - return deps - .tabIds() - .map((id) => buildItem(id, statuses, perms, qs, deps)) - .filter((item): item is CurrentTabItem => item !== undefined) - }) - -const basicItem = (id: string, title: string, deps: CurrentTabItemsDeps): CurrentTabItem => ({ - id, - title, - working: false, - tone: id === deps.visibleTabId() ? "active" : "idle", -}) - -function buildReviewItem(id: string, deps: CurrentTabItemsDeps) { - return basicItem(id, deps.t("session.tab.review"), deps) -} - -function buildTerminalItem(id: string, deps: CurrentTabItemsDeps) { - const term = deps.terms.lookup().get(id) - if (!term) return undefined - return basicItem(id, term.title, deps) -} - -function buildPendingItem(tab: SessionInfo, deps: CurrentTabItemsDeps) { - return basicItem(tab.id, tab.title || deps.t("agentManager.session.newSession"), deps) -} - -function buildSessionItem( - tab: SessionInfo, - status: SessionStatusInfo | undefined, - blocked: boolean, - deps: CurrentTabItemsDeps, -) { - const working = !blocked && (status?.type === "busy" || status?.type === "retry") - return { - id: tab.id, - title: tab.title || deps.t("agentManager.session.untitled"), - status: statusLabel(tab.id, blocked, status, deps), - working, - tone: statusTone(tab.id, blocked, working, deps), - } satisfies CurrentTabItem -} - -function buildItem( - id: string, - statuses: Record, - perms: PermissionRequest[], - qs: QuestionRequest[], - deps: CurrentTabItemsDeps, -): CurrentTabItem | undefined { - if (id === deps.reviewId) return buildReviewItem(id, deps) - if (deps.isTerminal(id)) return buildTerminalItem(id, deps) - const tab = deps.tabLookup().get(id) - if (!tab) return undefined - if (deps.isPending(id)) return buildPendingItem(tab, deps) - const blocked = perms.some((p) => p.sessionID === id) || qs.some((q) => q.sessionID === id) - return buildSessionItem(tab, statuses[id], blocked, deps) -} - -function statusLabel(id: string, blocked: boolean, status: SessionStatusInfo | undefined, deps: CurrentTabItemsDeps) { - if (blocked) return deps.t("agentManager.tabsMenu.status.waiting") - if (status?.type === "retry") return deps.t("agentManager.tabsMenu.status.retry") - return undefined -} - -function statusTone(id: string, blocked: boolean, working: boolean, deps: CurrentTabItemsDeps): CurrentTabItem["tone"] { - if (id === deps.visibleTabId()) return "active" - if (blocked) return "waiting" - if (working) return "busy" - return "idle" -} - -interface CurrentTabsMenuProps { - items: Accessor - label: string - searchLabel: string - emptyLabel: string - activeId: Accessor - onSelect: (id: string) => void -} - -function SearchIcon() { - return ( - - ) -} - -export const CurrentTabsMenu: Component = (props) => { - const [search, setSearch] = createSignal("") - const [open, setOpen] = createSignal(false) - const [mark, setMark] = createSignal(0) - let input: HTMLInputElement | undefined - - const focus = () => { - input?.focus({ preventScroll: true }) - } - - createEffect(() => { - if (!open()) return - setSearch("") - queueMicrotask(focus) - }) - - const rows = createMemo(() => { - const q = search().trim().toLowerCase() - if (!q) return props.items() - return props.items().filter((item) => item.title.toLowerCase().includes(q)) - }) - - createEffect(() => { - if (!open()) return - search() - setMark(0) - }) - - const select = (id: string) => { - props.onSelect(id) - setOpen(false) - } - - const move = (dir: 1 | -1) => { - const len = rows().length - if (len === 0) return - setMark((prev) => (prev + dir + len) % len) - } - - const onKeyDown = (e: KeyboardEvent) => { - e.stopPropagation() - if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return - if (e.key === "ArrowDown") { - e.preventDefault() - move(1) - return - } - if (e.key === "ArrowUp") { - e.preventDefault() - move(-1) - return - } - if (e.key === "Enter") { - e.preventDefault() - const item = rows()[mark()] - if (item) select(item.id) - return - } - if (e.key === "Escape") { - e.preventDefault() - setOpen(false) - } - } - - return ( - - - - } - > - -
- 0} fallback={
{props.emptyLabel}
}> - - {(item) => ( - - )} - -
-
-
- ) -} diff --git a/packages/kilo-vscode/webview-ui/agent-manager/SidebarSearchMenu.tsx b/packages/kilo-vscode/webview-ui/agent-manager/SidebarSearchMenu.tsx new file mode 100644 index 00000000000..7da9c9cb79f --- /dev/null +++ b/packages/kilo-vscode/webview-ui/agent-manager/SidebarSearchMenu.tsx @@ -0,0 +1,165 @@ +/** @jsxImportSource solid-js */ + +import { Show, createEffect, createSignal } from "solid-js" +import type { Accessor, Component } from "solid-js" +import { Icon } from "@kilocode/kilo-ui/icon" +import { List } from "@kilocode/kilo-ui/list" +import type { ListRef } from "@kilocode/kilo-ui/list" +import { Popover } from "@kilocode/kilo-ui/popover" +import { Spinner } from "@kilocode/kilo-ui/spinner" +import { TooltipKeybind } from "@kilocode/kilo-ui/tooltip" +import { formatRelativeDate } from "../src/utils/date" +import { colorCss } from "./section-colors" +import type { SidebarSearchItem } from "./sidebar-search" + +export interface SidebarSearchMenuRef { + open: () => void +} + +interface SidebarSearchMenuProps { + items: Accessor + current: Accessor + labels: { search: string; scope: string; contexts: string; sessions: string; waiting: string; retry: string } + keybind: string + ref?: (value: SidebarSearchMenuRef) => void + onSelect: (item: SidebarSearchItem) => void + defaultOpen?: boolean + portal?: boolean +} + +export const SidebarSearchMenu: Component = (props) => { + const [open, setOpen] = createSignal(props.defaultOpen ?? false) + const [query, setQuery] = createSignal("") + const [notice, setNotice] = createSignal("") + let list: ListRef | undefined + let root: HTMLDivElement | undefined + + const focus = () => + queueMicrotask(() => { + list?.setFilter("") + root?.querySelector("input")?.focus({ preventScroll: true }) + }) + + createEffect(() => { + if (open()) focus() + }) + + const reveal = () => { + setOpen(true) + focus() + } + props.ref?.({ open: reveal }) + + const refocus = () => + requestAnimationFrame(() => window.dispatchEvent(new CustomEvent("focusPrompt", { detail: { restore: true } }))) + const close = (next: boolean) => { + setOpen(next) + if (!next) refocus() + } + + const select = (item: SidebarSearchItem) => { + setOpen(false) + props.onSelect(item) + refocus() + } + + return ( + + } + > + + + + ) +} diff --git a/packages/kilo-vscode/webview-ui/agent-manager/WorktreeSectionActions.tsx b/packages/kilo-vscode/webview-ui/agent-manager/WorktreeSectionActions.tsx new file mode 100644 index 00000000000..0980bb060f2 --- /dev/null +++ b/packages/kilo-vscode/webview-ui/agent-manager/WorktreeSectionActions.tsx @@ -0,0 +1,139 @@ +/** @jsxImportSource solid-js */ + +import type { Accessor, Component } from "solid-js" +import { Show } from "solid-js" +import { DropdownMenu } from "@kilocode/kilo-ui/dropdown-menu" +import { Icon } from "@kilocode/kilo-ui/icon" +import { IconButton } from "@kilocode/kilo-ui/icon-button" +import { TooltipKeybind } from "@kilocode/kilo-ui/tooltip" +import type { LanguageContextValue } from "../src/context/language" +import { parseBindingTokens } from "./keybind-tokens" +import { SidebarSearchMenu, type SidebarSearchMenuRef } from "./SidebarSearchMenu" +import type { SidebarSearchItem } from "./sidebar-search" + +interface WorktreeSectionActionsProps { + items: Accessor + current: Accessor + bindings: Record + branch: string + git: boolean + loaded: boolean + t: LanguageContextValue["t"] + onRef: (ref: SidebarSearchMenuRef) => void + onSelect: (item: SidebarSearchItem) => void + onCreate: () => void + onAdvanced: () => void + onSection: () => void + onShortcuts: () => void + onSetup: () => void + onBranch: () => void +} + +export const WorktreeSectionActions: Component = (props) => ( +
+ + +
+ + + + + + + + + + + + {props.t("agentManager.dialog.configureWorktree")} + + {parseBindingTokens(props.bindings.advancedWorktree ?? "").map((token) => ( + {token} + ))} + + + + + + {props.t("agentManager.worktree.newSection")} + + + + +
+ + + + + + + + + {props.t("agentManager.worktree.setupScript")} + + + + + {props.t("agentManager.worktree.defaultBaseBranch")}: {props.branch} + + + + + +
+
+) diff --git a/packages/kilo-vscode/webview-ui/agent-manager/agent-manager.css b/packages/kilo-vscode/webview-ui/agent-manager/agent-manager.css index 7eea66cc458..9403d9c7a54 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/agent-manager.css +++ b/packages/kilo-vscode/webview-ui/agent-manager/agent-manager.css @@ -1371,165 +1371,167 @@ body.am-wt-dragging-active * { padding-right: 4px; border-left: 1px solid var(--border-weak-base); } - -.am-tabs-menu-trigger { +.am-sidebar-search-trigger { display: inline-flex; - align-items: center; - justify-content: center; width: 24px; height: 24px; + flex-shrink: 0; + align-items: center; + justify-content: center; padding: 0; border: none; border-radius: var(--radius-sm); background: transparent; color: var(--text-weak); cursor: pointer; - flex-shrink: 0; } -.am-tabs-menu-trigger:hover, -[data-slot="popover-trigger"][data-expanded] .am-tabs-menu-trigger { +.am-sidebar-search-trigger:hover, +.am-sidebar-search-trigger[data-expanded] { background: var(--surface-inset-base-hover); color: var(--text-base); } -.am-tabs-search-icon { - width: 16px; - height: 16px; - fill: none; - stroke: currentColor; - stroke-width: 1.35; - stroke-linecap: round; - stroke-linejoin: round; -} - -.am-tabs-menu[data-component="popover-content"] { - width: 300px; - max-height: min(360px, calc(100vh - 64px)); +.am-sidebar-search-popover[data-component="popover-content"] { + width: min(360px, calc(100vw - 24px)); + max-height: min(480px, calc(100vh - 64px)); padding: 0; overflow: hidden; } -.am-tabs-menu [data-slot="popover-body"] { +.am-sidebar-search-popover [data-slot="popover-body"] { padding: 0; } -.am-tabs-menu-search { - display: flex; - align-items: center; - padding: 8px 12px; - border-bottom: 1px solid var(--border-base); - color: var(--text-weaker); +.am-sidebar-search [data-component="list"] { + max-height: min(440px, calc(100vh - 88px)); + gap: 0; + padding: 8px 0 0; } -.am-tabs-menu-search-input { - flex: 1; - min-width: 0; - border: none; - background: transparent; - color: var(--text-base); - font-family: inherit; - font-size: var(--kilo-font-size-12); - outline: none; - user-select: text; +.am-sidebar-search [data-slot="list-search-wrapper"] { + margin: 0 0 4px; + padding: 0 8px; } -.am-tabs-menu-search-input::placeholder { - color: var(--vscode-input-placeholderForeground, var(--text-weaker)); +.am-sidebar-search [data-slot="list-search"] { + background: var(--surface-inset-base); } -.am-tabs-menu-list { - max-height: 300px; - overflow-y: auto; - padding: 4px; +.am-sidebar-search [data-slot="list-scroll"] { + max-height: 360px; + gap: 2px; + padding: 0 4px; } -.am-tabs-menu-item { - display: flex; - align-items: center; - gap: 8px; - width: 100%; - min-height: 34px; - padding: 6px 10px; - border: none; - border-radius: var(--radius-sm); - background: transparent; - color: var(--text-base); - cursor: pointer; - font-family: inherit; - font-size: var(--kilo-font-size-12); - text-align: left; +.am-sidebar-search [data-slot="list-group"]:last-child { + padding-bottom: 4px; } -.am-tabs-menu-item:hover { - background: var(--surface-inset-base-hover); +.am-sidebar-search [data-slot="list-header"] { + padding: 7px 8px 4px; + color: var(--text-weaker); + font-size: var(--kilo-font-size-10); + font-weight: 600; + letter-spacing: 0.06em; } -.am-tabs-menu-item-marked:not(.am-tabs-menu-item-active) { - background: var(--surface-inset-base-hover); +.am-sidebar-search [data-slot="list-item"] { + min-height: 46px; + padding: 6px 8px; } -.am-tabs-menu-item-active, -.am-tabs-menu-item-active:hover { - background: var(--surface-interactive-base); - color: var(--text-on-interactive-base); +.am-sidebar-search [data-slot="list-item"][data-active="true"], +.am-sidebar-search [data-slot="list-item"][data-selected="true"] { + background: color-mix(in srgb, var(--text-base) 10%, transparent); } -.am-tabs-menu-indicator { +.am-sidebar-search-result { + display: flex; + width: 100%; + min-width: 0; + align-items: center; + gap: 8px; +} + +.am-sidebar-search-icon { display: inline-flex; + width: 18px; + height: 18px; + flex-shrink: 0; align-items: center; justify-content: center; - width: 16px; - height: 16px; - flex-shrink: 0; + color: var(--text-weak); } -.am-tabs-menu-spinner { +.am-sidebar-search-spinner { width: 14px; height: 14px; } -.am-tabs-menu-status { - display: inline-flex; - align-items: center; - justify-content: flex-end; - flex-shrink: 0; - margin-left: auto; - color: var(--text-weaker); - font-size: var(--kilo-font-size-10); - line-height: 1; - text-transform: uppercase; +.am-sidebar-search-copy { + display: flex; + min-width: 0; + flex: 1; + flex-direction: column; + gap: 1px; + text-align: left; } -.am-tabs-menu-status[data-tone="active"] { - color: var(--text-weak); +.am-sidebar-search-title, +.am-sidebar-search-meta { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } -.am-tabs-menu-status[data-tone="busy"] { - color: var(--vscode-testing-iconPassed, #34d399); +.am-sidebar-search-title { + color: var(--text-base); + font-size: var(--kilo-font-size-12); + font-weight: 500; + line-height: 1.35; } -.am-tabs-menu-status[data-tone="waiting"] { - color: #fbbf24; +.am-sidebar-search-meta { + display: flex; + align-items: center; + gap: 5px; + color: var(--text-base); + font-size: var(--kilo-font-size-10); + font-weight: 400; + line-height: 1.35; } -.am-tabs-menu-item-active .am-tabs-menu-status { - color: color-mix(in srgb, var(--text-on-interactive-base) 70%, transparent); +.am-sidebar-search-swatch { + width: 6px; + height: 6px; + flex-shrink: 0; + border-radius: 50%; } -.am-tabs-menu-title { - flex: 1; - min-width: 0; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; +.am-sidebar-search-status, +.am-sidebar-search-count, +.am-sidebar-search-time { + flex-shrink: 0; + margin-left: auto; + color: var(--text-base); + font-size: var(--kilo-font-size-10); + font-weight: 400; } -.am-tabs-menu-empty { - padding: 14px 10px; - color: var(--text-weaker); - font-size: var(--kilo-font-size-12); - text-align: center; +.am-sidebar-search-status { + color: #fbbf24; + text-transform: uppercase; +} + +.am-sidebar-search-count { + display: inline-flex; + min-width: 18px; + height: 18px; + align-items: center; + justify-content: center; + border-radius: 9px; + background: var(--surface-inset-base); } /* Empty worktree state */ diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/ar.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/ar.ts index cd5f714e31c..986066f84b0 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/ar.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/ar.ts @@ -52,11 +52,11 @@ export const dict = { "agentManager.tab.terminal": "الطرفية", "agentManager.tab.openTerminal": "فتح الطرفية", "agentManager.tab.newOptions": "المزيد من خيارات علامات التبويب الجديدة", - "agentManager.tabsMenu.label": "البحث في علامات التبويب المفتوحة", - "agentManager.tabsMenu.search": "البحث في علامات التبويب...", - "agentManager.tabsMenu.empty": "لا توجد علامات تبويب مطابقة", "agentManager.tabsMenu.status.waiting": "انتظار", "agentManager.tabsMenu.status.retry": "إعادة", + "agentManager.sidebarSearch.label": "البحث في Worktrees والجلسات", + "agentManager.sidebarSearch.scope": "يبحث في مساحة العمل المحلية والجلسات المحلية وWorktrees وجلساتها", + "agentManager.sidebarSearch.contexts": "محلي & WORKTREES", "agentManager.terminal.new": "علامة تبويب جديدة للمحطة الطرفية", "agentManager.terminal.ended": "انتهت المحطة الطرفية — أغلق علامة التبويب للإخفاء", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/br.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/br.ts index 43508c5489e..786bdeeb7d5 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/br.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/br.ts @@ -52,11 +52,12 @@ export const dict = { "agentManager.tab.terminal": "Terminal", "agentManager.tab.openTerminal": "Abrir Terminal", "agentManager.tab.newOptions": "Mais opções de nova aba", - "agentManager.tabsMenu.label": "Pesquisar abas abertas", - "agentManager.tabsMenu.search": "Pesquisar abas...", - "agentManager.tabsMenu.empty": "Nenhuma aba correspondente", "agentManager.tabsMenu.status.waiting": "Espera", "agentManager.tabsMenu.status.retry": "Repetir", + "agentManager.sidebarSearch.label": "Pesquisar Worktrees e sessões", + "agentManager.sidebarSearch.scope": + "Pesquisa o espaço de trabalho local, as sessões locais, os Worktrees e suas sessões", + "agentManager.sidebarSearch.contexts": "LOCAL & WORKTREES", "agentManager.terminal.new": "Nova aba de terminal", "agentManager.terminal.ended": "terminal encerrado — feche a aba para dispensar", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/bs.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/bs.ts index 02cd467db9e..7116a0687eb 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/bs.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/bs.ts @@ -52,11 +52,11 @@ export const dict = { "agentManager.tab.terminal": "Terminal", "agentManager.tab.openTerminal": "Otvori Terminal", "agentManager.tab.newOptions": "Više opcija nove kartice", - "agentManager.tabsMenu.label": "Pretraži otvorene kartice", - "agentManager.tabsMenu.search": "Pretraži kartice...", - "agentManager.tabsMenu.empty": "Nema podudarnih kartica", "agentManager.tabsMenu.status.waiting": "Čeka", "agentManager.tabsMenu.status.retry": "Pokušaj", + "agentManager.sidebarSearch.label": "Pretraži Worktree-ove i sesije", + "agentManager.sidebarSearch.scope": "Pretražuje lokalni radni prostor, lokalne sesije, Worktree-ove i njihove sesije", + "agentManager.sidebarSearch.contexts": "LOKALNO & WORKTREES", "agentManager.terminal.new": "Nova kartica terminala", "agentManager.terminal.ended": "terminal je završen — zatvorite karticu da biste odbacili", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/da.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/da.ts index 086d759380a..83d54a5a503 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/da.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/da.ts @@ -52,11 +52,13 @@ export const dict = { "agentManager.tab.terminal": "Terminal", "agentManager.tab.openTerminal": "Åbn Terminal", "agentManager.tab.newOptions": "Flere nye faneindstillinger", - "agentManager.tabsMenu.label": "Søg i åbne faner", - "agentManager.tabsMenu.search": "Søg faner...", - "agentManager.tabsMenu.empty": "Ingen matchende faner", + "agentManager.tabsMenu.status.waiting": "Venter", "agentManager.tabsMenu.status.retry": "Igen", + "agentManager.sidebarSearch.label": "Søg i Worktrees og sessioner", + "agentManager.sidebarSearch.scope": + "Søger i det lokale arbejdsområde, lokale sessioner, Worktrees og deres sessioner", + "agentManager.sidebarSearch.contexts": "LOKAL & WORKTREES", "agentManager.terminal.new": "Ny terminalfane", "agentManager.terminal.ended": "terminal afsluttet — luk fanen for at fjerne", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/de.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/de.ts index 872f5d43ff4..db4dea7a3ff 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/de.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/de.ts @@ -52,11 +52,12 @@ export const dict = { "agentManager.tab.terminal": "Terminal", "agentManager.tab.openTerminal": "Terminal öffnen", "agentManager.tab.newOptions": "Weitere Tab-Optionen", - "agentManager.tabsMenu.label": "Offene Tabs suchen", - "agentManager.tabsMenu.search": "Tabs suchen...", - "agentManager.tabsMenu.empty": "Keine passenden Tabs", "agentManager.tabsMenu.status.waiting": "Warten", "agentManager.tabsMenu.status.retry": "Erneut", + "agentManager.sidebarSearch.label": "Worktrees und Sitzungen durchsuchen", + "agentManager.sidebarSearch.scope": + "Durchsucht den lokalen Arbeitsbereich, lokale Sitzungen, Worktrees und deren Sitzungen", + "agentManager.sidebarSearch.contexts": "LOKAL & WORKTREES", "agentManager.terminal.new": "Neuer Terminal-Tab", "agentManager.terminal.ended": "Terminal beendet — Tab schließen zum Verwerfen", 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 3249785b6ae..30f6c101433 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/en.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/en.ts @@ -56,11 +56,11 @@ export const dict = { "agentManager.tab.terminal": "Terminal", "agentManager.tab.openTerminal": "Open Terminal", "agentManager.tab.newOptions": "More new-tab options", - "agentManager.tabsMenu.label": "Search and switch tabs", - "agentManager.tabsMenu.search": "Search tabs...", - "agentManager.tabsMenu.empty": "No matching tabs", "agentManager.tabsMenu.status.waiting": "Wait", "agentManager.tabsMenu.status.retry": "Retry", + "agentManager.sidebarSearch.label": "Search worktrees and sessions", + "agentManager.sidebarSearch.scope": "Searches the local workspace, local sessions, worktrees, and their sessions", + "agentManager.sidebarSearch.contexts": "LOCAL & WORKTREES", "agentManager.terminal.new": "New Terminal Tab", "agentManager.terminal.ended": "terminal ended — close tab to dismiss", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/es.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/es.ts index e36b18c1622..5917ecf226c 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/es.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/es.ts @@ -52,11 +52,12 @@ export const dict = { "agentManager.tab.terminal": "Terminal", "agentManager.tab.openTerminal": "Abrir Terminal", "agentManager.tab.newOptions": "Más opciones de nueva pestaña", - "agentManager.tabsMenu.label": "Buscar pestañas abiertas", - "agentManager.tabsMenu.search": "Buscar pestañas...", - "agentManager.tabsMenu.empty": "No hay pestañas coincidentes", "agentManager.tabsMenu.status.waiting": "Espera", "agentManager.tabsMenu.status.retry": "Reintento", + "agentManager.sidebarSearch.label": "Buscar Worktrees y sesiones", + "agentManager.sidebarSearch.scope": + "Busca en el espacio de trabajo local, las sesiones locales, los Worktrees y sus sesiones", + "agentManager.sidebarSearch.contexts": "LOCAL & WORKTREES", "agentManager.terminal.new": "Nueva pestaña de terminal", "agentManager.terminal.ended": "terminal finalizado — cierra la pestaña para descartar", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/fr.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/fr.ts index 6781bcf4c05..c4d2ca062cb 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/fr.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/fr.ts @@ -52,11 +52,12 @@ export const dict = { "agentManager.tab.terminal": "Terminal", "agentManager.tab.openTerminal": "Ouvrir le Terminal", "agentManager.tab.newOptions": "Plus d'options de nouvel onglet", - "agentManager.tabsMenu.label": "Rechercher les onglets ouverts", - "agentManager.tabsMenu.search": "Rechercher des onglets...", - "agentManager.tabsMenu.empty": "Aucun onglet correspondant", "agentManager.tabsMenu.status.waiting": "Attente", "agentManager.tabsMenu.status.retry": "Réessai", + "agentManager.sidebarSearch.label": "Rechercher des Worktrees et des sessions", + "agentManager.sidebarSearch.scope": + "Recherche dans l'espace de travail local, les sessions locales, les Worktrees et leurs sessions", + "agentManager.sidebarSearch.contexts": "LOCAL & WORKTREES", "agentManager.terminal.new": "Nouvel onglet de terminal", "agentManager.terminal.ended": "terminal terminé — fermez l'onglet pour ignorer", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/it.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/it.ts index b2a02e4e4a5..b1f725d78de 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/it.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/it.ts @@ -56,11 +56,12 @@ export const dict = { "agentManager.tab.terminal": "Terminale", "agentManager.tab.openTerminal": "Apri terminale", "agentManager.tab.newOptions": "Altre opzioni nuova scheda", - "agentManager.tabsMenu.label": "Cerca e cambia scheda", - "agentManager.tabsMenu.search": "Cerca schede...", - "agentManager.tabsMenu.empty": "Nessuna scheda corrispondente", "agentManager.tabsMenu.status.waiting": "Attendi", "agentManager.tabsMenu.status.retry": "Riprova", + "agentManager.sidebarSearch.label": "Cerca Worktree e sessioni", + "agentManager.sidebarSearch.scope": + "Cerca nell'area di lavoro locale, nelle sessioni locali, nei Worktree e nelle relative sessioni", + "agentManager.sidebarSearch.contexts": "LOCALE & WORKTREE", "agentManager.terminal.new": "Nuova scheda terminale", "agentManager.terminal.ended": "terminale terminato - chiudi la scheda per nasconderlo", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/ja.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/ja.ts index cff88a4bb15..6ea7b2a79eb 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/ja.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/ja.ts @@ -52,11 +52,12 @@ export const dict = { "agentManager.tab.terminal": "ターミナル", "agentManager.tab.openTerminal": "ターミナルを開く", "agentManager.tab.newOptions": "新しいタブのその他のオプション", - "agentManager.tabsMenu.label": "開いているタブを検索", - "agentManager.tabsMenu.search": "タブを検索...", - "agentManager.tabsMenu.empty": "一致するタブがありません", "agentManager.tabsMenu.status.waiting": "待機", "agentManager.tabsMenu.status.retry": "再試行", + "agentManager.sidebarSearch.label": "Worktreeとセッションを検索", + "agentManager.sidebarSearch.scope": + "ローカルワークスペース、ローカルセッション、Worktree、および各Worktreeのセッションを検索", + "agentManager.sidebarSearch.contexts": "ローカル & WORKTREES", "agentManager.terminal.new": "新しいターミナルタブ", "agentManager.terminal.ended": "ターミナルが終了しました — タブを閉じて破棄", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/ko.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/ko.ts index f25dc1818e9..632286896b6 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/ko.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/ko.ts @@ -52,11 +52,11 @@ export const dict = { "agentManager.tab.terminal": "터미널", "agentManager.tab.openTerminal": "터미널 열기", "agentManager.tab.newOptions": "더 많은 새 탭 옵션", - "agentManager.tabsMenu.label": "열린 탭 검색", - "agentManager.tabsMenu.search": "탭 검색...", - "agentManager.tabsMenu.empty": "일치하는 탭 없음", "agentManager.tabsMenu.status.waiting": "대기", "agentManager.tabsMenu.status.retry": "재시도", + "agentManager.sidebarSearch.label": "Worktree 및 세션 검색", + "agentManager.sidebarSearch.scope": "로컬 워크스페이스, 로컬 세션, Worktree 및 각 Worktree의 세션 검색", + "agentManager.sidebarSearch.contexts": "로컬 & WORKTREES", "agentManager.terminal.new": "새 터미널 탭", "agentManager.terminal.ended": "터미널 종료됨 — 탭을 닫아 해제", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/nl.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/nl.ts index e351a332782..bf4f10f4864 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/nl.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/nl.ts @@ -56,11 +56,11 @@ export const dict = { "agentManager.tab.terminal": "Terminal", "agentManager.tab.openTerminal": "Terminal openen", "agentManager.tab.newOptions": "Meer opties voor nieuwe tabblad", - "agentManager.tabsMenu.label": "Open tabbladen zoeken", - "agentManager.tabsMenu.search": "Tabbladen zoeken...", - "agentManager.tabsMenu.empty": "Geen overeenkomende tabbladen", "agentManager.tabsMenu.status.waiting": "Wacht", "agentManager.tabsMenu.status.retry": "Opnieuw", + "agentManager.sidebarSearch.label": "Worktrees en sessies doorzoeken", + "agentManager.sidebarSearch.scope": "Doorzoekt de lokale werkruimte, lokale sessies, Worktrees en hun sessies", + "agentManager.sidebarSearch.contexts": "LOKAAL & WORKTREES", "agentManager.terminal.new": "Nieuw terminaltabblad", "agentManager.terminal.ended": "terminal beëindigd — sluit tabblad om te negeren", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/no.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/no.ts index 4ae4cf01c44..33266e5649f 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/no.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/no.ts @@ -52,11 +52,11 @@ export const dict = { "agentManager.tab.terminal": "Terminal", "agentManager.tab.openTerminal": "Åpne Terminal", "agentManager.tab.newOptions": "Flere alternativer for ny fane", - "agentManager.tabsMenu.label": "Søk i åpne faner", - "agentManager.tabsMenu.search": "Søk i faner...", - "agentManager.tabsMenu.empty": "Ingen treffende faner", "agentManager.tabsMenu.status.waiting": "Venter", "agentManager.tabsMenu.status.retry": "Igjen", + "agentManager.sidebarSearch.label": "Søk i Worktrees og økter", + "agentManager.sidebarSearch.scope": "Søker i det lokale arbeidsområdet, lokale økter, Worktrees og øktene deres", + "agentManager.sidebarSearch.contexts": "LOKAL & WORKTREES", "agentManager.terminal.new": "Ny terminalfane", "agentManager.terminal.ended": "terminal avsluttet — lukk fanen for å avvise", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/pl.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/pl.ts index 342a3f4e1d7..bec2721aefa 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/pl.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/pl.ts @@ -52,11 +52,12 @@ export const dict = { "agentManager.tab.terminal": "Terminal", "agentManager.tab.openTerminal": "Otwórz Terminal", "agentManager.tab.newOptions": "Więcej opcji nowej karty", - "agentManager.tabsMenu.label": "Szukaj otwartych kart", - "agentManager.tabsMenu.search": "Szukaj kart...", - "agentManager.tabsMenu.empty": "Brak pasujących kart", "agentManager.tabsMenu.status.waiting": "Czeka", "agentManager.tabsMenu.status.retry": "Ponów", + "agentManager.sidebarSearch.label": "Wyszukaj Worktree i sesje", + "agentManager.sidebarSearch.scope": + "Przeszukuje lokalny obszar roboczy, lokalne sesje, Worktree i przypisane do nich sesje", + "agentManager.sidebarSearch.contexts": "LOKALNE & WORKTREES", "agentManager.terminal.new": "Nowa karta terminala", "agentManager.terminal.ended": "terminal zakończony — zamknij kartę, aby zamknąć", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/ru.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/ru.ts index 8954a0e01b7..d303c685492 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/ru.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/ru.ts @@ -52,11 +52,12 @@ export const dict = { "agentManager.tab.terminal": "Терминал", "agentManager.tab.openTerminal": "Открыть терминал", "agentManager.tab.newOptions": "Другие параметры новой вкладки", - "agentManager.tabsMenu.label": "Искать открытые вкладки", - "agentManager.tabsMenu.search": "Поиск вкладок...", - "agentManager.tabsMenu.empty": "Нет подходящих вкладок", "agentManager.tabsMenu.status.waiting": "Ожидание", "agentManager.tabsMenu.status.retry": "Повтор", + "agentManager.sidebarSearch.label": "Поиск по Worktree и сессиям", + "agentManager.sidebarSearch.scope": + "Поиск локального рабочего пространства, локальных сессий, Worktree и связанных с ними сессий", + "agentManager.sidebarSearch.contexts": "ЛОКАЛЬНЫЙ & WORKTREES", "agentManager.terminal.new": "Новая вкладка терминала", "agentManager.terminal.ended": "терминал завершен — закройте вкладку, чтобы скрыть", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/th.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/th.ts index 3badc096bad..0a54fbda504 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/th.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/th.ts @@ -52,11 +52,11 @@ export const dict = { "agentManager.tab.terminal": "เทอร์มินัล", "agentManager.tab.openTerminal": "เปิดเทอร์มินัล", "agentManager.tab.newOptions": "ตัวเลือกแท็บใหม่เพิ่มเติม", - "agentManager.tabsMenu.label": "ค้นหาแท็บที่เปิดอยู่", - "agentManager.tabsMenu.search": "ค้นหาแท็บ...", - "agentManager.tabsMenu.empty": "ไม่มีแท็บที่ตรงกัน", "agentManager.tabsMenu.status.waiting": "รอ", "agentManager.tabsMenu.status.retry": "ลองใหม่", + "agentManager.sidebarSearch.label": "ค้นหา Worktree และเซสชัน", + "agentManager.sidebarSearch.scope": "ค้นหาพื้นที่ทำงานในเครื่อง เซสชันในเครื่อง Worktree และเซสชันของ Worktree", + "agentManager.sidebarSearch.contexts": "ในเครื่อง & WORKTREES", "agentManager.terminal.new": "แท็บเทอร์มินัลใหม่", "agentManager.terminal.ended": "เทอร์มินัลสิ้นสุด — ปิดแท็บเพื่อยกเลิก", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/tr.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/tr.ts index 24d5e641149..da962287b23 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/tr.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/tr.ts @@ -56,11 +56,12 @@ export const dict = { "agentManager.tab.terminal": "Terminal", "agentManager.tab.openTerminal": "Terminali Aç", "agentManager.tab.newOptions": "Daha fazla yeni sekme seçeneği", - "agentManager.tabsMenu.label": "Açık sekmeleri ara", - "agentManager.tabsMenu.search": "Sekmelerde ara...", - "agentManager.tabsMenu.empty": "Eşleşen sekme yok", "agentManager.tabsMenu.status.waiting": "Bekliyor", "agentManager.tabsMenu.status.retry": "Yeniden", + "agentManager.sidebarSearch.label": "Worktree'leri ve oturumları ara", + "agentManager.sidebarSearch.scope": + "Yerel çalışma alanını, yerel oturumları, Worktree'leri ve bunların oturumlarını arar", + "agentManager.sidebarSearch.contexts": "YEREL & WORKTREE'LER", "agentManager.terminal.new": "Yeni Terminal Sekmesi", "agentManager.terminal.ended": "terminal sona erdi — kapatmak için sekmeyi kapatın", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/uk.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/uk.ts index 5a853526233..90179cf9e5a 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/uk.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/uk.ts @@ -56,11 +56,12 @@ export const dict = { "agentManager.tab.terminal": "Термінал", "agentManager.tab.openTerminal": "Відкрити термінал", "agentManager.tab.newOptions": "Інші параметри нової вкладки", - "agentManager.tabsMenu.label": "Шукати відкриті вкладки", - "agentManager.tabsMenu.search": "Шукати вкладки...", - "agentManager.tabsMenu.empty": "Немає відповідних вкладок", "agentManager.tabsMenu.status.waiting": "Очікує", "agentManager.tabsMenu.status.retry": "Повтор", + "agentManager.sidebarSearch.label": "Пошук робочих дерев і сесій", + "agentManager.sidebarSearch.scope": + "Пошук локального робочого простору, локальних сесій, робочих дерев і призначених їм сесій", + "agentManager.sidebarSearch.contexts": "ЛОКАЛЬНИЙ & РОБОЧІ ДЕРЕВА", "agentManager.terminal.new": "Нова вкладка термінала", "agentManager.terminal.ended": "термінал завершено — закрийте вкладку, щоб відхилити", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/zh.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/zh.ts index 70801afdcc3..e682d428122 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/zh.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/zh.ts @@ -52,11 +52,11 @@ export const dict = { "agentManager.tab.terminal": "终端", "agentManager.tab.openTerminal": "打开终端", "agentManager.tab.newOptions": "更多新建标签页选项", - "agentManager.tabsMenu.label": "搜索打开的标签页", - "agentManager.tabsMenu.search": "搜索标签页...", - "agentManager.tabsMenu.empty": "无匹配标签页", "agentManager.tabsMenu.status.waiting": "等待", "agentManager.tabsMenu.status.retry": "重试", + "agentManager.sidebarSearch.label": "搜索 Worktree 和会话", + "agentManager.sidebarSearch.scope": "搜索本地工作区、本地会话、Worktree 及其会话", + "agentManager.sidebarSearch.contexts": "本地 & WORKTREES", "agentManager.terminal.new": "新建终端标签页", "agentManager.terminal.ended": "终端已结束 — 关闭标签页以消除", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/zht.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/zht.ts index 3435eb35dbe..618efb0a118 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/zht.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/zht.ts @@ -52,11 +52,11 @@ export const dict = { "agentManager.tab.terminal": "終端機", "agentManager.tab.openTerminal": "開啟終端機", "agentManager.tab.newOptions": "更多新增分頁選項", - "agentManager.tabsMenu.label": "搜尋開啟的分頁", - "agentManager.tabsMenu.search": "搜尋分頁...", - "agentManager.tabsMenu.empty": "沒有相符的分頁", "agentManager.tabsMenu.status.waiting": "等待", "agentManager.tabsMenu.status.retry": "重試", + "agentManager.sidebarSearch.label": "搜尋 Worktree 與工作階段", + "agentManager.sidebarSearch.scope": "搜尋本機工作區、本機工作階段、Worktree 及其工作階段", + "agentManager.sidebarSearch.contexts": "本機 & WORKTREES", "agentManager.terminal.new": "新增終端分頁", "agentManager.terminal.ended": "終端已結束 — 關閉分頁以消除", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/shortcuts.ts b/packages/kilo-vscode/webview-ui/agent-manager/shortcuts.ts index 8f21d698a11..e265a524d14 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/shortcuts.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/shortcuts.ts @@ -16,6 +16,7 @@ export function buildShortcutCategories( { title: t("agentManager.shortcuts.category.quickSwitch"), shortcuts: [ + { label: t("agentManager.sidebarSearch.label"), binding: bindings.search ?? "" }, { label: t("agentManager.shortcuts.jumpToItem"), binding: (() => { diff --git a/packages/kilo-vscode/webview-ui/agent-manager/sidebar-search.ts b/packages/kilo-vscode/webview-ui/agent-manager/sidebar-search.ts new file mode 100644 index 00000000000..7be92e0b37e --- /dev/null +++ b/packages/kilo-vscode/webview-ui/agent-manager/sidebar-search.ts @@ -0,0 +1,225 @@ +import { createMemo } from "solid-js" +import type { Accessor } from "solid-js" +import type { + PermissionRequest, + QuestionRequest, + SectionState, + SessionInfo, + SessionStatusInfo, + WorktreeState, +} from "../src/types/messages" +import { LOCAL } from "./navigate" + +export type SidebarSearchState = "idle" | "busy" | "retry" | "waiting" + +type SearchItem = { + key: string + title: string + meta: string[] + search: string + updatedAt: string + state: SidebarSearchState + visible: boolean + section?: SectionState +} + +export type SidebarSearchItem = + | (SearchItem & { + kind: "local" + group: "contexts" + count: number + }) + | (SearchItem & { + kind: "worktree" + group: "contexts" + worktreeId: string + count: number + }) + | (SearchItem & { + kind: "session" + group: "sessions" + sessionId: string + location: "local" | "worktree" + worktreeId?: string + }) + +export interface SidebarSearchWorktree { + worktree: WorktreeState + label: string + sessions: SessionInfo[] +} + +interface SidebarSearchInput { + worktrees: SidebarSearchWorktree[] + sections: SectionState[] + local: SessionInfo[] + localLabel: string + localBranch?: string + untitled: string + pending: (id: string) => boolean + status: (id: string) => SidebarSearchState + busy: (id: string) => boolean + localBusy: boolean +} + +const root = (item: SessionInfo) => !item.parentID +const same = (a: string, b: string) => a.trim().toLowerCase() === b.trim().toLowerCase() +const score = (state: SidebarSearchState) => (state === "waiting" ? 3 : state === "idle" ? 0 : 2) +const newest = (items: SessionInfo[], fallback: string) => + items.reduce((latest, item) => (item.updatedAt > latest ? item.updatedAt : latest), fallback) + +export function sortSidebarSearch(a: SidebarSearchItem, b: SidebarSearchItem) { + return ( + score(b.state) - score(a.state) || + Number(b.visible) - Number(a.visible) || + b.updatedAt.localeCompare(a.updatedAt) || + a.title.localeCompare(b.title) + ) +} + +export function buildSidebarSearch(input: SidebarSearchInput): SidebarSearchItem[] { + const sections = new Map(input.sections.map((item) => [item.id, item])) + const owned = new Set(input.worktrees.flatMap((item) => item.sessions.map((session) => session.id))) + const local = input.local.filter((session) => root(session) && !input.pending(session.id) && !owned.has(session.id)) + const sessions: SidebarSearchItem[] = local.map((session) => ({ + key: `session:${session.id}`, + kind: "session" as const, + group: "sessions" as const, + title: session.title || input.untitled, + meta: [input.localLabel], + search: [session.title || input.untitled, input.localLabel, session.id].join(" "), + sessionId: session.id, + location: "local" as const, + updatedAt: session.updatedAt, + state: input.status(session.id), + visible: true, + })) + const localState = local.map((session) => input.status(session.id)).sort((a, b) => score(b) - score(a))[0] ?? "idle" + const contexts: SidebarSearchItem[] = [ + { + key: LOCAL, + kind: "local", + group: "contexts", + title: input.localLabel, + meta: input.localBranch ? [input.localBranch] : [], + search: [input.localLabel, input.localBranch].filter(Boolean).join(" "), + updatedAt: newest(local, ""), + state: input.localBusy && localState === "idle" ? "busy" : localState, + visible: true, + count: local.length, + }, + ] + + for (const item of input.worktrees) { + const wt = item.worktree + const section = wt.sectionId ? sections.get(wt.sectionId) : undefined + const roots = item.sessions.filter((session) => root(session) && !input.pending(session.id)) + + for (const session of roots) { + const title = session.title || input.untitled + const meta = [section?.name, !same(item.label, title) ? item.label : undefined, wt.branch].filter( + (value): value is string => !!value, + ) + sessions.push({ + key: `session:${session.id}`, + kind: "session", + group: "sessions", + title, + meta, + search: [title, item.label, wt.branch, section?.name, session.id].filter(Boolean).join(" "), + sessionId: session.id, + location: "worktree", + worktreeId: wt.id, + updatedAt: session.updatedAt, + state: input.status(session.id), + visible: !section?.collapsed, + section, + }) + } + + const state = roots.map((session) => input.status(session.id)).sort((a, b) => score(b) - score(a))[0] ?? "idle" + contexts.push({ + key: `worktree:${wt.id}`, + kind: "worktree", + group: "contexts", + title: item.label, + meta: [section?.name, wt.branch].filter((value): value is string => !!value), + search: [item.label, wt.branch, section?.name, wt.prNumber ? `#${wt.prNumber}` : undefined, wt.id] + .filter(Boolean) + .join(" "), + worktreeId: wt.id, + updatedAt: newest(roots, wt.createdAt), + state: input.busy(wt.id) && state === "idle" ? "busy" : state, + visible: !section?.collapsed, + section, + count: roots.length, + }) + } + + // The List keeps this order for an empty query and as the tie-break order for equally relevant fuzzy matches. + return [...sessions.sort(sortSidebarSearch), ...contexts.sort(sortSidebarSearch)] +} + +interface SidebarSearchDeps { + worktrees: Accessor + sections: Accessor + local: Accessor + localBranch: Accessor + selection: Accessor + sessionId: Accessor + statuses: Accessor> + permissions: Accessor + questions: Accessor + label: (worktree: WorktreeState) => string + sessions: (id: string) => SessionInfo[] + pending: (id: string) => boolean + busy: (id: string) => boolean + localBusy: Accessor + t: (key: string) => string +} + +export function createSidebarSearch(deps: SidebarSearchDeps) { + const items = createMemo(() => { + const statuses = deps.statuses() + const blocked = new Set([ + ...deps.permissions().map((item) => item.sessionID), + ...deps.questions().map((item) => item.sessionID), + ]) + return buildSidebarSearch({ + worktrees: deps.worktrees().map((worktree) => ({ + worktree, + label: deps.label(worktree), + sessions: deps.sessions(worktree.id), + })), + sections: deps.sections(), + local: deps.local(), + localLabel: deps.t("agentManager.local"), + localBranch: deps.localBranch(), + untitled: deps.t("agentManager.session.untitled"), + pending: deps.pending, + status: (id) => { + if (blocked.has(id)) return "waiting" + const status = statuses[id]?.type + return status === "busy" || status === "retry" ? status : "idle" + }, + busy: deps.busy, + localBusy: deps.localBusy(), + }) + }) + + const current = createMemo(() => { + const id = deps.sessionId() + const selection = deps.selection() + const active = items().find( + (item) => + item.kind === "session" && + item.sessionId === id && + ((item.location === "local" && selection === LOCAL) || item.worktreeId === selection), + ) + if (active || !selection) return active + if (selection === LOCAL) return items().find((item) => item.kind === "local") + return items().find((item) => item.kind === "worktree" && item.worktreeId === selection) + }) + + return { items, current } +} diff --git a/packages/kilo-vscode/webview-ui/agent-manager/tab-rendering.tsx b/packages/kilo-vscode/webview-ui/agent-manager/tab-rendering.tsx index 04f5f6b219e..7175cccdb5b 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/tab-rendering.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/tab-rendering.tsx @@ -8,7 +8,7 @@ */ import { Show } from "solid-js" -import type { JSX } from "solid-js" +import type { Accessor, JSX } from "solid-js" import { IconButton } from "@kilocode/kilo-ui/icon-button" import { DropdownMenu } from "@kilocode/kilo-ui/dropdown-menu" import { Icon } from "@kilocode/kilo-ui/icon" @@ -19,6 +19,45 @@ import { isTerminalTabId, renderTerminalTab } from "./terminal" import type { SessionInfo } from "../src/types/messages" import { parseBindingTokens } from "./keybind-tokens" +interface FocusTabDeps { + id: string + terms: TerminalStateControls + isTerminal: (id: string) => boolean + isPending: (id: string) => boolean + reviewId: string + reviewOpen: Accessor + setReviewOpen: (open: boolean) => void + setReviewActive: (active: boolean) => void + tabLookup: Accessor> + setActivePendingId: (id: string | undefined) => void + clearSession: () => void + selectSession: (id: string) => void + activateTerminal: (id: string) => void +} + +export function focusCurrentTab(deps: FocusTabDeps) { + if (deps.isTerminal(deps.id)) { + deps.activateTerminal(deps.id) + return + } + deps.terms.setActiveId(undefined) + if (deps.id === deps.reviewId) { + if (!deps.reviewOpen()) deps.setReviewOpen(true) + deps.setReviewActive(true) + return + } + const target = deps.tabLookup().get(deps.id) + if (!target) return + deps.setReviewActive(false) + if (deps.isPending(target.id)) { + deps.setActivePendingId(target.id) + deps.clearSession() + return + } + deps.setActivePendingId(undefined) + deps.selectSession(target.id) +} + export interface TabRenderDeps { terms: TerminalStateControls REVIEW_TAB_ID: string diff --git a/packages/kilo-vscode/webview-ui/agent-manager/terminal/TerminalTab.tsx b/packages/kilo-vscode/webview-ui/agent-manager/terminal/TerminalTab.tsx index 622fa520705..7b43afc7752 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/terminal/TerminalTab.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/terminal/TerminalTab.tsx @@ -109,7 +109,7 @@ function isAgentManagerShortcut(e: KeyboardEvent): boolean { if (!e.metaKey && !e.ctrlKey) return false const key = e.key.toLowerCase() if (e.altKey && ["arrowleft", "arrowright", "arrowup", "arrowdown"].includes(key)) return true - if (["t", "w", "n", "d", "e"].includes(key)) return true + if (["t", "w", "n", "d", "e", "f"].includes(key)) return true if (e.shiftKey && ["w", "n", "o", "m", "/", "?"].includes(key)) return true if (/^[1-9]$/.test(key)) return true if (key === "/") return true diff --git a/packages/kilo-vscode/webview-ui/src/stories/agent-manager.stories.tsx b/packages/kilo-vscode/webview-ui/src/stories/agent-manager.stories.tsx index 8b387cd3cde..fbad72deba7 100644 --- a/packages/kilo-vscode/webview-ui/src/stories/agent-manager.stories.tsx +++ b/packages/kilo-vscode/webview-ui/src/stories/agent-manager.stories.tsx @@ -15,12 +15,15 @@ import { registerVscodeToolOverrides } from "../components/chat/VscodeToolOverri import { SessionContext } from "../context/session" import { ServerContext } from "../context/server" import { WorktreeModeProvider } from "../context/worktree-mode" +import { SidebarSearchMenu } from "../../agent-manager/SidebarSearchMenu" +import { SidebarToggleButton } from "../../agent-manager/SidebarToggleButton" +import type { SidebarSearchItem } from "../../agent-manager/sidebar-search" import { Button } from "@kilocode/kilo-ui/button" import { IconButton } from "@kilocode/kilo-ui/icon-button" import { Icon } from "@kilocode/kilo-ui/icon" import { TooltipKeybind } from "@kilocode/kilo-ui/tooltip" import { ContextMenu } from "@kilocode/kilo-ui/context-menu" -import { createSignal, type JSX } from "solid-js" +import { createSignal, onCleanup, onMount, type JSX } from "solid-js" import type { WorktreeFileDiff, WorktreeState, WorktreeGitStats, PRStatus } from "../types/messages" import type { ReviewComment } from "../../diff-viewer/review-comments" import "../../agent-manager/agent-manager.css" @@ -823,18 +826,9 @@ const MockReviewTab = (props: { active?: boolean }) => (
) -const MockTabsSearchButton = () => ( - -) - const MockTabLeading = () => (
- + {}} />
) @@ -934,3 +928,121 @@ export const TabBarSingleTab: Story = { ), } + +const searchSection = { id: "polish", name: "Polish", color: "Blue", order: 0, collapsed: false } +const slackedSection = { id: "slacked", name: "SLACKED", color: "Yellow", order: 1, collapsed: false } +const sidebarSearchItems: SidebarSearchItem[] = [ + { + key: "session:session-build", + kind: "session", + group: "sessions", + title: "Build grouped worktree search", + meta: ["Polish", "Agent Manager search", "feat/sidebar-search"], + search: "Build grouped worktree search Agent Manager search feat/sidebar-search Polish", + sessionId: "session-build", + location: "worktree", + worktreeId: "wt-search", + updatedAt: new Date(Date.now() - 3 * 60_000).toISOString(), + state: "busy", + visible: true, + section: searchSection, + }, + { + key: "session:session-local", + kind: "session", + group: "sessions", + title: "Investigate local indexing", + meta: ["local"], + search: "Investigate local indexing local", + sessionId: "session-local", + location: "local", + updatedAt: new Date(Date.now() - 8 * 60_000).toISOString(), + state: "idle", + visible: true, + }, + { + key: "session:session-render", + kind: "session", + group: "sessions", + title: "Render images in diff viewer", + meta: ["SLACKED", "images diff viewer", "utopian-approval"], + search: "Render images in diff viewer SLACKED images diff viewer utopian-approval", + sessionId: "session-render", + location: "worktree", + worktreeId: "wt-render", + updatedAt: new Date(Date.now() - 60 * 60_000).toISOString(), + state: "idle", + visible: true, + section: slackedSection, + }, + { + key: "local", + kind: "local", + group: "contexts", + title: "local", + meta: ["main"], + search: "local main", + updatedAt: new Date(Date.now() - 3 * 60_000).toISOString(), + state: "idle", + visible: true, + count: 2, + }, + { + key: "worktree:wt-search", + kind: "worktree", + group: "contexts", + title: "Agent Manager search", + meta: ["Polish", "feat/sidebar-search"], + search: "Agent Manager search Polish feat/sidebar-search", + worktreeId: "wt-search", + updatedAt: new Date(Date.now() - 3 * 60_000).toISOString(), + state: "busy", + visible: true, + section: searchSection, + count: 2, + }, +] + +export const SidebarSearchOpen: Story = { + name: "Sidebar search — worktrees and sessions", + render: () => { + const [selected, setSelected] = createSignal("worktree:wt-search") + let prompt!: HTMLTextAreaElement + const refocus = () => requestAnimationFrame(() => prompt.focus()) + onMount(() => { + window.addEventListener("focusPrompt", refocus) + onCleanup(() => window.removeEventListener("focusPrompt", refocus)) + }) + return ( + +
+
+ +
+ sidebarSearchItems} + keybind="⌘F" + current={() => sidebarSearchItems.find((item) => item.key === selected())} + labels={{ + search: "Search worktrees and sessions", + scope: "Searches the local workspace, local sessions, worktrees, and their sessions", + contexts: "LOCAL & WORKTREES", + sessions: "SESSIONS", + waiting: "Wait", + retry: "Retry", + }} + onSelect={(item) => setSelected(item.key)} + defaultOpen + portal={false} + /> +
+
+ + {selected()} + +