Files
Kilo-Org_kilo/packages/kilo-vscode/webview-ui/agent-manager/navigate.ts
T
Marius cb18296f88 feat(agent-manager): add keyboard shortcut tooltips and worktree hover cards (#516)
* feat(agent-manager): add keyboard shortcut tooltips and worktree hover cards

- Add TooltipKeybind to all agent manager buttons (tabs, new session,
  close tab, terminal, new worktree, delete worktree, promote to worktree)
- Add HoverCard popovers on worktree items showing branch name, base
  branch, session count, creation date, and navigation shortcut hint
- Show directional keybind hints only for directly adjacent items
  (tabs: ⌘←/⌘→, worktrees: ⌘↑/⌘↓)
- Resolve keybindings from package.json at runtime per platform
  (Mac symbols vs Windows/Linux Ctrl+key format)
- Send resolved keybindings from extension to webview via new
  agentManager.keybindings message instead of hardcoding shortcuts
- Prevent multiple HoverCards from appearing simultaneously via
  shared hover state lifted above the For loop
- Suppress HoverCard when hovering the delete button

* refactor: extract formatKeybinding and adjacentHint into testable modules

- Extract formatKeybinding into format-keybinding.ts (pure function,
  no vscode dependency, takes mac boolean parameter)
- Extract adjacentHint into navigate.ts as a reusable pure function
  for computing directional keybind hints
- Add tests for adjacentHint (12 cases) and formatKeybinding (13 cases)
- Simplify tab/worktree direction logic in AgentManagerApp to use
  adjacentHint instead of inline index arithmetic
2026-02-20 12:26:04 +00:00

89 lines
3.2 KiB
TypeScript

/**
* Pure navigation logic for the agent manager sidebar.
*
* The sidebar has a fixed "local" item at index -1, followed by
* session items at indices 0..N-1 (sorted newest-first).
*
* Returns the action to take: select a session by ID, go to local, or do nothing.
*/
/** Sentinel value for the local workspace selection. */
export const LOCAL = "local" as const
export type NavResult = { action: "select"; id: string } | { action: typeof LOCAL } | { action: "none" }
export function resolveNavigation(direction: "up" | "down", current: string | undefined, ids: string[]): NavResult {
// Determine current position: -1 = local, 0..N-1 = session index
if (!current) {
// On local
if (direction === "up") return { action: "none" }
if (ids.length === 0) return { action: "none" }
return { action: "select", id: ids[0]! }
}
const idx = ids.indexOf(current)
// Current session not found in list — don't navigate
if (idx === -1) return { action: "none" }
const next = direction === "up" ? idx - 1 : idx + 1
// Moving up past the first session → go to local
if (next === -1) return { action: LOCAL }
// At the bottom boundary
if (next >= ids.length) return { action: "none" }
return { action: "select", id: ids[next]! }
}
/**
* Validate a persisted local session ID against the current sessions list.
* Returns the ID if it still exists, undefined otherwise.
*/
export function validateLocalSession(persisted: string | undefined, ids: string[]): string | undefined {
if (!persisted) return undefined
if (ids.indexOf(persisted) === -1) return undefined
return persisted
}
/**
* Return the keybinding hint for an item adjacent to the active item.
* Only returns a hint when the item is exactly one step away in the flat list.
* Returns empty string for non-adjacent items or the active item itself.
*
* @param itemId - The item being hovered
* @param activeId - The currently selected/active item (or undefined for LOCAL)
* @param flatIds - The full ordered sidebar list (LOCAL first, then worktrees, then sessions)
* @param prev - Display string for "go up" (e.g. "⌘↑" or keybinding)
* @param next - Display string for "go down" (e.g. "⌘↓" or keybinding)
*/
export function adjacentHint(
itemId: string,
activeId: string | undefined,
flatIds: string[],
prev: string,
next: string,
): string {
if (!activeId || itemId === activeId) return ""
const activeIdx = flatIds.indexOf(activeId)
const itemIdx = flatIds.indexOf(itemId)
if (activeIdx === -1 || itemIdx === -1) return ""
const diff = itemIdx - activeIdx
if (diff === -1) return prev
if (diff === 1) return next
return ""
}
/**
* After removing a worktree, pick the nearest remaining sidebar neighbor.
* Order: the worktree just below → the one above → LOCAL.
*/
export function nextSelectionAfterDelete(deletedId: string, worktreeIds: string[]): typeof LOCAL | string {
const idx = worktreeIds.indexOf(deletedId)
if (idx === -1) return LOCAL
const remaining = worktreeIds.filter((id) => id !== deletedId)
if (remaining.length === 0) return LOCAL
// Prefer the item that was below (same index in the shortened list), else the one above
return remaining[Math.min(idx, remaining.length - 1)]!
}