Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/search-agent-manager-sidebar.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"kilo-code": minor
---

Search and switch between local sessions, worktrees, and their sessions from the Agent Manager sidebar.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
11 changes: 11 additions & 0 deletions packages/kilo-vscode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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+/",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
3 changes: 3 additions & 0 deletions packages/kilo-vscode/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
58 changes: 58 additions & 0 deletions packages/kilo-vscode/tests/accessibility.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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()
})
})
3 changes: 2 additions & 1 deletion packages/kilo-vscode/tests/unit/agent-manager-arch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down
11 changes: 11 additions & 0 deletions packages/kilo-vscode/tests/unit/extension-arch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
})
})
})

// ---------------------------------------------------------------------------
Expand Down
10 changes: 9 additions & 1 deletion packages/kilo-vscode/tests/unit/format-keybinding.test.ts
Original file line number Diff line number Diff line change
@@ -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", () => {
Expand Down Expand Up @@ -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")
})
})
154 changes: 154 additions & 0 deletions packages/kilo-vscode/tests/unit/sidebar-search.test.ts
Original file line number Diff line number Diff line change
@@ -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<Parameters<typeof buildSidebarSearch>[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",
})
})
})
Loading
Loading