From b894206b6306c42bc1bddf8eccfca4e3689d49e7 Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Tue, 4 Aug 2026 10:21:31 +0200 Subject: [PATCH 1/7] fix(vscode): fix multi-project navigation shortcuts --- .changeset/fix-multi-project-navigation.md | 5 ++ .../kilo-vscode/tests/unit/navigate.test.ts | 27 ++++++++++- .../agent-manager/AgentManagerApp.tsx | 8 +++- .../webview-ui/agent-manager/ProjectList.tsx | 2 + .../agent-manager/ProjectSidebarBody.tsx | 12 +++++ .../webview-ui/agent-manager/navigate.ts | 34 +++++++++----- .../webview-ui/agent-manager/project-nav.ts | 46 +++++++++++-------- .../agent-manager/section-helpers.ts | 2 +- 8 files changed, 102 insertions(+), 34 deletions(-) create mode 100644 .changeset/fix-multi-project-navigation.md diff --git a/.changeset/fix-multi-project-navigation.md b/.changeset/fix-multi-project-navigation.md new file mode 100644 index 00000000000..4eddfc5eb17 --- /dev/null +++ b/.changeset/fix-multi-project-navigation.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Fix multi-project Agent Manager keyboard navigation and Cmd/Ctrl shortcut selection when worktrees are grouped in sections. diff --git a/packages/kilo-vscode/tests/unit/navigate.test.ts b/packages/kilo-vscode/tests/unit/navigate.test.ts index be6e5d816e1..e8ce22735e8 100644 --- a/packages/kilo-vscode/tests/unit/navigate.test.ts +++ b/packages/kilo-vscode/tests/unit/navigate.test.ts @@ -401,7 +401,7 @@ describe("buildProjectNavOrder", () => { expect(order.map((e) => e.id)).toEqual([localNavId("A"), worktreeNavId("A", "aw2")]) }) - it("renders section members before ungrouped worktrees (matching the project body)", () => { + it("renders ungrouped worktrees before section members (matching the project body)", () => { const order = buildProjectNavOrder([ project({ id: "A", @@ -411,7 +411,30 @@ describe("buildProjectNavOrder", () => { unassigned: [], }), ]) - expect(order.map((e) => e.id)).toEqual([localNavId("A"), worktreeNavId("A", "aw1"), worktreeNavId("A", "aw2")]) + expect(order.map((e) => e.id)).toEqual([localNavId("A"), worktreeNavId("A", "aw2"), worktreeNavId("A", "aw1")]) + }) + + it("follows persisted top-level section order and worktree order", () => { + const order = buildProjectNavOrder([ + project({ + id: "A", + expanded: true, + worktrees: [{ id: "aw1", sectionId: "s1" }, { id: "aw2" }, { id: "aw3", sectionId: "s2" }], + worktreeOrder: ["aw2", "s2", "s1", "aw3", "aw1"], + sections: [ + { id: "s1", collapsed: false }, + { id: "s2", collapsed: false }, + ], + unassigned: [], + }), + ]) + + expect(order.map((e) => e.id)).toEqual([ + localNavId("A"), + worktreeNavId("A", "aw2"), + worktreeNavId("A", "aw3"), + worktreeNavId("A", "aw1"), + ]) }) it("excludes unassigned sessions when the sessions section is collapsed", () => { diff --git a/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx b/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx index 939a8e505be..602b16b2fe3 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx @@ -103,7 +103,7 @@ import { focusChatSearch, LOCAL, } from "./navigate" -import { createProjectNav } from "./project-nav" +import { buildProjectNavEntries, createProjectNav } from "./project-nav" import { addPendingTab as addLocalPendingTab, nextTabAfterClose, @@ -882,6 +882,11 @@ const AgentManagerContent: Component = () => { ) /** Map from sidebar item id → 1-based shortcut number (⌘1 for LOCAL, ⌘2 for first worktree, etc.) */ const shortcutMap = createMemo(() => buildShortcutMap(sidebarOrder())) + const projectShortcutMap = createMemo(() => + buildShortcutMap( + buildProjectNavEntries(projectList(), projectStates(), projectLive.sessions()).map((entry) => ({ id: entry.id })), + ) + ) const moveToSection = (ids: string[], sec: string | null) => vscode.postMessage({ type: "agentManager.moveToSection", worktreeIds: ids, sectionId: sec }) @@ -2347,6 +2352,7 @@ const AgentManagerContent: Component = () => { t={t} onSearchRef={(ref) => (sidebarSearchMenu = ref)} onShortcuts={handleShowKeyboardShortcuts} + shortcutMap={projectShortcutMap} /> diff --git a/packages/kilo-vscode/webview-ui/agent-manager/ProjectList.tsx b/packages/kilo-vscode/webview-ui/agent-manager/ProjectList.tsx index 3fb1220c166..a07fbab208c 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/ProjectList.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/ProjectList.tsx @@ -39,6 +39,7 @@ interface Props { t: LanguageContextValue["t"] onSearchRef: (ref: SidebarSearchMenuRef) => void onShortcuts: () => void + shortcutMap?: () => Map } export const ProjectList: Component = (props) => { @@ -224,6 +225,7 @@ export const ProjectList: Component = (props) => { onSelectSession={(projectId, sessionId) => select({ projectId, kind: "session", sessionId })} onNewWorktree={newWorktree} onDefaultBranch={defaultBranch} + shortcutMap={props.shortcutMap} /> )} /> diff --git a/packages/kilo-vscode/webview-ui/agent-manager/ProjectSidebarBody.tsx b/packages/kilo-vscode/webview-ui/agent-manager/ProjectSidebarBody.tsx index 40b3cd82cb4..1b67b39216f 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/ProjectSidebarBody.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/ProjectSidebarBody.tsx @@ -31,6 +31,8 @@ import { ConstrainDragXAxis } from "./constrain-drag-x" import { createProjectStore, type ProjectStore } from "./project/store" import { randomColor } from "./section-colors" +const isMac = typeof navigator !== "undefined" && /Mac|iPhone|iPad/.test(navigator.userAgent) + interface Props { project: AgentProjectSnapshot state?: AgentManagerStateMessage @@ -49,6 +51,7 @@ interface Props { onSelectSession: (projectId: string, sessionId: string) => void onNewWorktree: (projectId: string) => void onDefaultBranch: (projectId: string, selected?: string, detected?: string) => void + shortcutMap?: () => Map } /** Permanent real sidebar body for one expanded project. */ @@ -214,6 +217,7 @@ export const ProjectSidebarBody: Component = (props) => { = (props) => { + + {(shortcut) => ( + + {isMac ? "⌘" : "Ctrl+"} + {shortcut()} + + )} +
{props.t("agentManager.local")} diff --git a/packages/kilo-vscode/webview-ui/agent-manager/navigate.ts b/packages/kilo-vscode/webview-ui/agent-manager/navigate.ts index 6fad2144f44..f2655cf4ef7 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/navigate.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/navigate.ts @@ -161,6 +161,8 @@ export interface ProjectNavInput { id: string expanded: boolean worktrees: { id: string; sectionId?: string }[] + /** Worktree IDs in the exact visual order produced by the project body. */ + worktreeOrder?: string[] sections: { id: string; collapsed: boolean }[] sessionsCollapsed: boolean /** Visible unassigned (root, no worktree) sessions in render order. */ @@ -174,10 +176,10 @@ export const sessionNavId = (projectId: string, sessionId: string) => `${project /** * Build one global visual order across expanded projects. * - * For each expanded project (in input order): Local, then worktrees in the - * order the multi-project body renders them (each non-collapsed section's - * members, then ungrouped), then visible unassigned sessions (when the - * sessions section is not collapsed). Collapsed projects contribute nothing. + * For each expanded project (in input order): Local, then ungrouped worktrees, + * then members of each non-collapsed section in top-level order, then visible + * unassigned sessions. This matches `buildTopLevelItems` and the project body. + * Collapsed projects contribute nothing. */ export function buildProjectNavOrder(projects: ProjectNavInput[]): NavEntry[] { const order: NavEntry[] = [] @@ -185,19 +187,29 @@ export function buildProjectNavOrder(projects: ProjectNavInput[]): NavEntry[] { if (!p.expanded) continue const pid = p.id order.push({ id: localNavId(pid), target: { projectId: pid, kind: "local" } }) - for (const sec of p.sections) { + const worktrees = p.worktreeOrder + ? [...p.worktreeOrder, ...p.worktrees.map((w) => w.id)] + .filter((id, index, all) => all.indexOf(id) === index) + .map((id) => p.worktrees.find((w) => w.id === id)) + .filter((w): w is NonNullable => !!w) + : p.worktrees + const rank = new Map((p.worktreeOrder ?? []).map((id, index) => [id, index] as const)) + const secs = [...p.sections].sort( + (a, b) => (rank.get(a.id) ?? Number.MAX_SAFE_INTEGER) - (rank.get(b.id) ?? Number.MAX_SAFE_INTEGER), + ) + for (const w of worktrees) { + if (!w.sectionId) { + order.push({ id: worktreeNavId(pid, w.id), target: { projectId: pid, kind: "worktree", worktreeId: w.id } }) + } + } + for (const sec of secs) { if (sec.collapsed) continue - for (const w of p.worktrees) { + for (const w of worktrees) { if (w.sectionId === sec.id) { order.push({ id: worktreeNavId(pid, w.id), target: { projectId: pid, kind: "worktree", worktreeId: w.id } }) } } } - for (const w of p.worktrees) { - if (!w.sectionId) { - order.push({ id: worktreeNavId(pid, w.id), target: { projectId: pid, kind: "worktree", worktreeId: w.id } }) - } - } if (!p.sessionsCollapsed) { for (const s of p.unassigned) { order.push({ id: sessionNavId(pid, s.id), target: { projectId: pid, kind: "session", sessionId: s.id } }) diff --git a/packages/kilo-vscode/webview-ui/agent-manager/project-nav.ts b/packages/kilo-vscode/webview-ui/agent-manager/project-nav.ts index beae37660cc..ce15c7fcdce 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/project-nav.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/project-nav.ts @@ -19,7 +19,7 @@ import type { AgentManagerStateMessage, AgentProjectSnapshot, ProjectSessionInfo * * Handles previous/next (⌘⌥↑/↓) and numeric-shortcut (⌘1-9) navigation across * the sidebar. In multi-project mode it builds one global visual order across - * every expanded project — Local, worktrees (sections first, then ungrouped), + * every expanded project — Local, ungrouped worktrees, then section members, * and visible unassigned sessions — using stable project-qualified composite * ids, and activates each target with a single atomic * `agentManager.activateSelection` dispatch. In single-project mode it keeps @@ -48,6 +48,31 @@ export interface ProjectNav { jump: (index: number) => void } +/** Build the same global order used by keyboard navigation and shortcut badges. */ +export function buildProjectNavEntries( + projects: AgentProjectSnapshot[], + states: Record, + sessions: Record, +): NavEntry[] { + return buildProjectNavOrder( + projects.map((p) => { + const st = states[p.id] + if (!st) { + return { id: p.id, expanded: false, worktrees: [], sections: [], sessionsCollapsed: false, unassigned: [] } + } + return { + id: p.id, + expanded: p.expanded, + worktrees: (st.worktrees ?? []).map((w) => ({ id: w.id, sectionId: w.sectionId })), + worktreeOrder: st.worktreeOrder, + sections: (st.sections ?? []).map((s) => ({ id: s.id, collapsed: s.collapsed })), + sessionsCollapsed: st.sessionsCollapsed === true, + unassigned: (sessions[p.id] ?? []).filter((s) => s.worktreeId === null).map((s) => ({ id: s.id })), + } + }), + ) +} + /** DOM selector for the sidebar element backing a nav target. */ export const navSelector = (target: NavTarget): string => { if (target.kind === "local") return `[data-sidebar-id="${target.projectId}:local"]` @@ -74,24 +99,7 @@ export function createProjectNav( if (!deps.multiProject()) return [] const states = deps.states() const live = deps.sessions() - return buildProjectNavOrder( - deps.projects().map((p) => { - const st = states[p.id] - // Until a project's state payload arrives its body shows a spinner - // (no Local/worktrees/sessions) — exclude it from the nav order. - if (!st) { - return { id: p.id, expanded: false, worktrees: [], sections: [], sessionsCollapsed: false, unassigned: [] } - } - return { - id: p.id, - expanded: p.expanded, - worktrees: (st.worktrees ?? []).map((w) => ({ id: w.id, sectionId: w.sectionId })), - sections: (st.sections ?? []).map((s) => ({ id: s.id, collapsed: s.collapsed })), - sessionsCollapsed: st.sessionsCollapsed === true, - unassigned: (live[p.id] ?? []).filter((s) => s.worktreeId === null).map((s) => ({ id: s.id })), - } - }), - ) + return buildProjectNavEntries(deps.projects(), states, live) }) const currentId = createMemo((): string | undefined => { diff --git a/packages/kilo-vscode/webview-ui/agent-manager/section-helpers.ts b/packages/kilo-vscode/webview-ui/agent-manager/section-helpers.ts index c79eb3c5378..e9d8d169b24 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/section-helpers.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/section-helpers.ts @@ -145,7 +145,7 @@ export function buildSidebarOrder( } /** Build a map from sidebar item id → 1-based shortcut number (1 for LOCAL, 2+ for worktrees). */ -export function buildShortcutMap(order: SidebarItem[]): Map { +export function buildShortcutMap(order: { id: string }[]): Map { const map = new Map() for (let i = 0; i < order.length && i < 9; i++) { map.set(order[i]!.id, i + 1) From 24f463c23263456d77c51cf8a9c765e8520c7bcc Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Tue, 4 Aug 2026 10:30:54 +0200 Subject: [PATCH 2/7] chore(vscode): format agent manager navigation --- .../kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx b/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx index 602b16b2fe3..2362d705b1e 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx @@ -885,7 +885,7 @@ const AgentManagerContent: Component = () => { const projectShortcutMap = createMemo(() => buildShortcutMap( buildProjectNavEntries(projectList(), projectStates(), projectLive.sessions()).map((entry) => ({ id: entry.id })), - ) + ), ) const moveToSection = (ids: string[], sec: string | null) => From d3dca39a5558dac07dc3f50c48cc0a09d43689bb Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Tue, 4 Aug 2026 10:48:12 +0200 Subject: [PATCH 3/7] fix(vscode): align grouped worktree navigation --- .../kilo-vscode/tests/unit/navigate.test.ts | 20 +++++++++++++++++++ .../webview-ui/agent-manager/navigate.ts | 2 ++ .../webview-ui/agent-manager/project-nav.ts | 2 +- .../agent-manager/section-helpers.ts | 6 +++--- 4 files changed, 26 insertions(+), 4 deletions(-) diff --git a/packages/kilo-vscode/tests/unit/navigate.test.ts b/packages/kilo-vscode/tests/unit/navigate.test.ts index e8ce22735e8..c7410cc3422 100644 --- a/packages/kilo-vscode/tests/unit/navigate.test.ts +++ b/packages/kilo-vscode/tests/unit/navigate.test.ts @@ -437,6 +437,26 @@ describe("buildProjectNavOrder", () => { ]) }) + it("keeps multi-version worktrees adjacent", () => { + const order = buildProjectNavOrder([ + project({ + id: "A", + expanded: true, + worktrees: [{ id: "aw1", groupId: "g" }, { id: "aw2" }, { id: "aw3", groupId: "g" }], + worktreeOrder: ["aw2", "aw3", "aw1"], + sections: [], + unassigned: [], + }), + ]) + + expect(order.map((e) => e.id)).toEqual([ + localNavId("A"), + worktreeNavId("A", "aw2"), + worktreeNavId("A", "aw3"), + worktreeNavId("A", "aw1"), + ]) + }) + it("excludes unassigned sessions when the sessions section is collapsed", () => { const order = buildProjectNavOrder([ project({ diff --git a/packages/kilo-vscode/webview-ui/agent-manager/navigate.ts b/packages/kilo-vscode/webview-ui/agent-manager/navigate.ts index f2655cf4ef7..76c5b792077 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/navigate.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/navigate.ts @@ -7,6 +7,8 @@ * Returns the action to take: select a session by ID, go to local, or do nothing. */ +import { sortWorktrees } from "./section-helpers" + /** Sentinel value for the local repo selection. */ export const LOCAL = "local" as const diff --git a/packages/kilo-vscode/webview-ui/agent-manager/project-nav.ts b/packages/kilo-vscode/webview-ui/agent-manager/project-nav.ts index ce15c7fcdce..2ff2fdbe84f 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/project-nav.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/project-nav.ts @@ -63,7 +63,7 @@ export function buildProjectNavEntries( return { id: p.id, expanded: p.expanded, - worktrees: (st.worktrees ?? []).map((w) => ({ id: w.id, sectionId: w.sectionId })), + worktrees: (st.worktrees ?? []).map((w) => ({ id: w.id, sectionId: w.sectionId, groupId: w.groupId })), worktreeOrder: st.worktreeOrder, sections: (st.sections ?? []).map((s) => ({ id: s.id, collapsed: s.collapsed })), sessionsCollapsed: st.sessionsCollapsed === true, diff --git a/packages/kilo-vscode/webview-ui/agent-manager/section-helpers.ts b/packages/kilo-vscode/webview-ui/agent-manager/section-helpers.ts index e9d8d169b24..fba922c053d 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/section-helpers.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/section-helpers.ts @@ -10,11 +10,11 @@ export type TopLevelItem = { kind: "section"; section: SectionState } | { kind: export type SidebarItem = { type: "local" | "wt" | "session"; id: string } /** Apply persisted order while keeping multi-version worktrees adjacent. */ -export function sortWorktrees(all: WorktreeState[], order: string[]): WorktreeState[] { +export function sortWorktrees(all: T[], order: string[]): T[] { const ordered = applyTabOrder(all, order) if (ordered.length === 0) return [] - const groups = new Map() + const groups = new Map() for (const wt of ordered) { if (!wt.groupId) continue const group = groups.get(wt.groupId) ?? [] @@ -22,7 +22,7 @@ export function sortWorktrees(all: WorktreeState[], order: string[]): WorktreeSt groups.set(wt.groupId, group) } - const result: WorktreeState[] = [] + const result: T[] = [] const placed = new Set() for (const wt of ordered) { if (placed.has(wt.id)) continue From d9d3568f3ff8c1c6e469b56d4640f9933e77eb62 Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Tue, 4 Aug 2026 10:57:31 +0200 Subject: [PATCH 4/7] fix(vscode): apply grouped worktree order --- packages/kilo-vscode/tests/unit/navigate.test.ts | 6 +++--- .../webview-ui/agent-manager/navigate.ts | 15 +++++---------- 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/packages/kilo-vscode/tests/unit/navigate.test.ts b/packages/kilo-vscode/tests/unit/navigate.test.ts index c7410cc3422..2a3e8df437c 100644 --- a/packages/kilo-vscode/tests/unit/navigate.test.ts +++ b/packages/kilo-vscode/tests/unit/navigate.test.ts @@ -443,7 +443,7 @@ describe("buildProjectNavOrder", () => { id: "A", expanded: true, worktrees: [{ id: "aw1", groupId: "g" }, { id: "aw2" }, { id: "aw3", groupId: "g" }], - worktreeOrder: ["aw2", "aw3", "aw1"], + worktreeOrder: ["aw1", "aw2", "aw3"], sections: [], unassigned: [], }), @@ -451,9 +451,9 @@ describe("buildProjectNavOrder", () => { expect(order.map((e) => e.id)).toEqual([ localNavId("A"), - worktreeNavId("A", "aw2"), - worktreeNavId("A", "aw3"), worktreeNavId("A", "aw1"), + worktreeNavId("A", "aw3"), + worktreeNavId("A", "aw2"), ]) }) diff --git a/packages/kilo-vscode/webview-ui/agent-manager/navigate.ts b/packages/kilo-vscode/webview-ui/agent-manager/navigate.ts index 76c5b792077..bcae1690821 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/navigate.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/navigate.ts @@ -139,8 +139,8 @@ export function focusChatSearch(reset: { history(v: boolean): void; review(v: bo * Multi-project navigation. * * In multi-project mode the sidebar shows an accordion of projects; each - * expanded project renders its own Local item, worktrees (sections first, - * then ungrouped), and an unassigned-sessions list. Keyboard previous/next + * expanded project renders its own Local item, ungrouped worktrees, section + * members, and an unassigned-sessions list. Keyboard previous/next * and numeric shortcuts must traverse every expanded project in visual * order, not just the active one. * @@ -162,8 +162,8 @@ export interface NavEntry { export interface ProjectNavInput { id: string expanded: boolean - worktrees: { id: string; sectionId?: string }[] - /** Worktree IDs in the exact visual order produced by the project body. */ + worktrees: { id: string; sectionId?: string; groupId?: string }[] + /** Persisted top-level order containing worktree and section IDs. */ worktreeOrder?: string[] sections: { id: string; collapsed: boolean }[] sessionsCollapsed: boolean @@ -189,12 +189,7 @@ export function buildProjectNavOrder(projects: ProjectNavInput[]): NavEntry[] { if (!p.expanded) continue const pid = p.id order.push({ id: localNavId(pid), target: { projectId: pid, kind: "local" } }) - const worktrees = p.worktreeOrder - ? [...p.worktreeOrder, ...p.worktrees.map((w) => w.id)] - .filter((id, index, all) => all.indexOf(id) === index) - .map((id) => p.worktrees.find((w) => w.id === id)) - .filter((w): w is NonNullable => !!w) - : p.worktrees + const worktrees = sortWorktrees(p.worktrees, p.worktreeOrder ?? []) const rank = new Map((p.worktreeOrder ?? []).map((id, index) => [id, index] as const)) const secs = [...p.sections].sort( (a, b) => (rank.get(a.id) ?? Number.MAX_SAFE_INTEGER) - (rank.get(b.id) ?? Number.MAX_SAFE_INTEGER), From c8f86d8f91acd010a2fdc073f2cd3310dc144bf5 Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Tue, 4 Aug 2026 11:07:15 +0200 Subject: [PATCH 5/7] fix(vscode): match section navigation order --- .../kilo-vscode/tests/unit/navigate.test.ts | 26 +++++++++++++++++++ .../webview-ui/agent-manager/navigate.ts | 12 ++++++--- 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/packages/kilo-vscode/tests/unit/navigate.test.ts b/packages/kilo-vscode/tests/unit/navigate.test.ts index 2a3e8df437c..3f23206ec69 100644 --- a/packages/kilo-vscode/tests/unit/navigate.test.ts +++ b/packages/kilo-vscode/tests/unit/navigate.test.ts @@ -457,6 +457,32 @@ describe("buildProjectNavOrder", () => { ]) }) + it("matches raw ungrouped order when sections are present", () => { + const order = buildProjectNavOrder([ + project({ + id: "A", + expanded: true, + worktrees: [ + { id: "aw1", groupId: "g" }, + { id: "aw2" }, + { id: "aw3", groupId: "g" }, + { id: "aw4", sectionId: "s1" }, + ], + worktreeOrder: ["aw1", "aw2", "aw3", "s1", "aw4"], + sections: [{ id: "s1", collapsed: false }], + unassigned: [], + }), + ]) + + expect(order.map((e) => e.id)).toEqual([ + localNavId("A"), + worktreeNavId("A", "aw1"), + worktreeNavId("A", "aw2"), + worktreeNavId("A", "aw3"), + worktreeNavId("A", "aw4"), + ]) + }) + it("excludes unassigned sessions when the sessions section is collapsed", () => { const order = buildProjectNavOrder([ project({ diff --git a/packages/kilo-vscode/webview-ui/agent-manager/navigate.ts b/packages/kilo-vscode/webview-ui/agent-manager/navigate.ts index bcae1690821..8596666cf91 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/navigate.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/navigate.ts @@ -191,13 +191,17 @@ export function buildProjectNavOrder(projects: ProjectNavInput[]): NavEntry[] { order.push({ id: localNavId(pid), target: { projectId: pid, kind: "local" } }) const worktrees = sortWorktrees(p.worktrees, p.worktreeOrder ?? []) const rank = new Map((p.worktreeOrder ?? []).map((id, index) => [id, index] as const)) + const ungrouped = worktrees.filter((w) => !w.sectionId) + if (p.sections.length > 0) { + ungrouped.sort( + (a, b) => (rank.get(a.id) ?? Number.MAX_SAFE_INTEGER) - (rank.get(b.id) ?? Number.MAX_SAFE_INTEGER), + ) + } const secs = [...p.sections].sort( (a, b) => (rank.get(a.id) ?? Number.MAX_SAFE_INTEGER) - (rank.get(b.id) ?? Number.MAX_SAFE_INTEGER), ) - for (const w of worktrees) { - if (!w.sectionId) { - order.push({ id: worktreeNavId(pid, w.id), target: { projectId: pid, kind: "worktree", worktreeId: w.id } }) - } + for (const w of ungrouped) { + order.push({ id: worktreeNavId(pid, w.id), target: { projectId: pid, kind: "worktree", worktreeId: w.id } }) } for (const sec of secs) { if (sec.collapsed) continue From 8fe53293076e410e9428112e1ed1d0681b3b0792 Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Tue, 4 Aug 2026 11:16:04 +0200 Subject: [PATCH 6/7] fix(vscode): add project-local navigation hints --- .changeset/project-local-navigation-hints.md | 5 ++++ .../unit/project-local-navigation.test.ts | 25 +++++++++++++++++++ .../webview-ui/agent-manager/ProjectList.tsx | 1 + .../agent-manager/ProjectSidebarBody.tsx | 15 +++++++++++ .../agent-manager/project-local-navigation.ts | 19 ++++++++++++++ 5 files changed, 65 insertions(+) create mode 100644 .changeset/project-local-navigation-hints.md create mode 100644 packages/kilo-vscode/tests/unit/project-local-navigation.test.ts create mode 100644 packages/kilo-vscode/webview-ui/agent-manager/project-local-navigation.ts diff --git a/.changeset/project-local-navigation-hints.md b/.changeset/project-local-navigation-hints.md new file mode 100644 index 00000000000..f6acca80ecd --- /dev/null +++ b/.changeset/project-local-navigation-hints.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Show previous and next navigation hints using each project's own Agent Manager sidebar order. diff --git a/packages/kilo-vscode/tests/unit/project-local-navigation.test.ts b/packages/kilo-vscode/tests/unit/project-local-navigation.test.ts new file mode 100644 index 00000000000..b20bbc5f30a --- /dev/null +++ b/packages/kilo-vscode/tests/unit/project-local-navigation.test.ts @@ -0,0 +1,25 @@ +import { describe, expect, it } from "bun:test" +import { projectAdjacentHint } from "../../webview-ui/agent-manager/project-local-navigation" + +describe("projectAdjacentHint", () => { + it("does not leak a hint to another project with the same raw ID", () => { + expect(projectAdjacentHint("project-a", "project-a", "shared", "local", ["local", "shared"], "prev", "next")).toBe( + "next", + ) + expect(projectAdjacentHint("project-b", "project-a", "shared", "local", ["local", "shared"], "prev", "next")).toBe( + "", + ) + }) + + it("uses the active project's local sidebar order", () => { + expect(projectAdjacentHint("project-a", "project-a", "shared", "local", ["local", "shared"], "prev", "next")).toBe( + "next", + ) + expect(projectAdjacentHint("project-a", "project-a", "local", "shared", ["local", "shared"], "prev", "next")).toBe( + "prev", + ) + expect( + projectAdjacentHint("project-b", "project-b", "shared", "local", ["local", "other", "shared"], "prev", "next"), + ).toBe("") + }) +}) diff --git a/packages/kilo-vscode/webview-ui/agent-manager/ProjectList.tsx b/packages/kilo-vscode/webview-ui/agent-manager/ProjectList.tsx index a07fbab208c..8fe0c65bbdc 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/ProjectList.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/ProjectList.tsx @@ -218,6 +218,7 @@ export const ProjectList: Component = (props) => { sessions={props.sessions[project.id]} selectedProject={props.selectedProject} selection={props.selection} + currentSessionID={props.currentSessionID} bindings={props.bindings} t={props.t} onSelectLocal={(projectId) => select({ projectId, kind: "local" })} diff --git a/packages/kilo-vscode/webview-ui/agent-manager/ProjectSidebarBody.tsx b/packages/kilo-vscode/webview-ui/agent-manager/ProjectSidebarBody.tsx index 1b67b39216f..a0fd9a1a814 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/ProjectSidebarBody.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/ProjectSidebarBody.tsx @@ -20,6 +20,7 @@ import type { } from "../src/types/messages" import type { LanguageContextValue } from "../src/context/language" import { useVSCode } from "../src/context/vscode" +import { projectAdjacentHint, projectSidebarOrder } from "./project-local-navigation" import SectionHeader from "./SectionHeader" import { WorktreeItem } from "./WorktreeItem" import { UnassignedSessionsSection } from "./UnassignedSessionsSection" @@ -44,6 +45,7 @@ interface Props { sessions?: ProjectSessionInfo[] selectedProject?: string selection?: string + currentSessionID?: () => string | undefined bindings: Record t: LanguageContextValue["t"] onSelectLocal: (projectId: string) => void @@ -100,9 +102,21 @@ export const ProjectSidebarBody: Component = (props) => { const members = (sectionId: string) => sorted().filter((wt) => wt.sectionId === sectionId) const ungrouped = createMemo(() => sorted().filter((wt) => !wt.sectionId)) const top = createMemo(() => buildTopLevelItems(sections(), ungrouped(), sorted(), order())) + const sidebarOrder = createMemo(() => projectSidebarOrder(top(), sorted(), sections(), members, localSessions())) const post = (message: Record) => vscode.postMessage({ ...message, projectId: props.project.id } as never) + const navHint = (id: string) => + projectAdjacentHint( + props.project.id, + props.selectedProject, + id, + props.selection ?? props.currentSessionID?.(), + sidebarOrder(), + props.bindings.previousSession ?? "", + props.bindings.nextSession ?? "", + ) + const scope = (kind: "section" | "worktree", id: string) => `${props.project.id}:${kind}:${id}` const parse = (kind: "section" | "worktree", value: unknown) => { if (typeof value !== "string") return @@ -226,6 +240,7 @@ export const ProjectSidebarBody: Component = (props) => { working={runs()[worktree.id]?.state === "running"} stale={state()?.staleWorktreeIds?.includes(worktree.id) === true} stats={props.stats?.[worktree.id]} + navHint={navHint(worktree.id)} sessions={sessions(worktree.id).length} grouped={isGrouped(worktree)} groupStart={isGroupStart(worktree, idx(), list)} diff --git a/packages/kilo-vscode/webview-ui/agent-manager/project-local-navigation.ts b/packages/kilo-vscode/webview-ui/agent-manager/project-local-navigation.ts new file mode 100644 index 00000000000..ede97a8e129 --- /dev/null +++ b/packages/kilo-vscode/webview-ui/agent-manager/project-local-navigation.ts @@ -0,0 +1,19 @@ +import { adjacentHint } from "./navigate" +import { buildSidebarOrder } from "./section-helpers" + +export function projectSidebarOrder(...args: Parameters): string[] { + return buildSidebarOrder(...args).map((item) => item.id) +} + +export function projectAdjacentHint( + projectId: string, + activeProjectId: string | undefined, + itemId: string, + activeId: string | undefined, + flatIds: string[], + prev: string, + next: string, +): string { + if (projectId !== activeProjectId) return "" + return adjacentHint(itemId, activeId, flatIds, prev, next) +} From 43ffa2bd137c0dbab58ec642e68ede78070421ca Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Tue, 4 Aug 2026 11:39:48 +0200 Subject: [PATCH 7/7] fix(vscode): align hints with collapsed sessions --- .../webview-ui/agent-manager/ProjectSidebarBody.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/kilo-vscode/webview-ui/agent-manager/ProjectSidebarBody.tsx b/packages/kilo-vscode/webview-ui/agent-manager/ProjectSidebarBody.tsx index a0fd9a1a814..0c97381b395 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/ProjectSidebarBody.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/ProjectSidebarBody.tsx @@ -102,7 +102,9 @@ export const ProjectSidebarBody: Component = (props) => { const members = (sectionId: string) => sorted().filter((wt) => wt.sectionId === sectionId) const ungrouped = createMemo(() => sorted().filter((wt) => !wt.sectionId)) const top = createMemo(() => buildTopLevelItems(sections(), ungrouped(), sorted(), order())) - const sidebarOrder = createMemo(() => projectSidebarOrder(top(), sorted(), sections(), members, localSessions())) + const sidebarOrder = createMemo(() => + projectSidebarOrder(top(), sorted(), sections(), members, state()?.sessionsCollapsed ? [] : localSessions()), + ) const post = (message: Record) => vscode.postMessage({ ...message, projectId: props.project.id } as never)