diff --git a/docs/AGENTS.md b/docs/AGENTS.md index fc4c68c35..73c331e1f 100644 --- a/docs/AGENTS.md +++ b/docs/AGENTS.md @@ -209,6 +209,7 @@ This project uses **Make** as the primary build orchestrator. See `Makefile` for **TDD is the preferred development style for agents.** +- **When asked to do TDD, write tests in the repository** - Create proper test files (e.g., `src/utils/foo.test.ts`) that run with `bun test` or `jest`, not temporary scripts in `/tmp`. Tests should be committed with the implementation. - Prefer relocated complex logic into places where they're easily tested - E.g. pure functions in `utils` are easier to test than complex logic in a React component - Strive for broad coverage with minimal tests diff --git a/docs/keybinds.md b/docs/keybinds.md index 5a3c1d644..63d438e46 100644 --- a/docs/keybinds.md +++ b/docs/keybinds.md @@ -54,6 +54,16 @@ When documentation shows `Ctrl`, it means: | Open command palette | `Ctrl+Shift+P` | | Toggle sidebar | `Ctrl+P` | +### Command Palette + +The command palette (`Ctrl+Shift+P`) has two modes: + +- **Default (no prefix)**: Workspace switcher - shows only switching commands +- **`>` prefix**: Command mode - shows all other commands (create/delete/rename workspaces, navigation, chat, modes, projects, etc.) +- **`/` prefix**: Slash commands - shows slash command suggestions for inserting into chat + +This separation keeps the switcher clean and fast while making all other commands easily accessible via `>`. + ## Tips - **Vim-inspired navigation**: We use `J`/`K` for next/previous navigation, similar to Vim diff --git a/src/components/CommandPalette.stories.tsx b/src/components/CommandPalette.stories.tsx index 90fd91ca0..7ec208d01 100644 --- a/src/components/CommandPalette.stories.tsx +++ b/src/components/CommandPalette.stories.tsx @@ -12,7 +12,7 @@ const mockCommands: CommandAction[] = [ id: "workspace.create", title: "Create New Workspace", subtitle: "Start a new workspace in this project", - section: "Workspace", + section: "Workspaces", keywords: ["new", "add", "workspace"], shortcutHint: "⌘N", run: () => action("command-executed")("workspace.create"), @@ -21,7 +21,7 @@ const mockCommands: CommandAction[] = [ id: "workspace.switch", title: "Switch Workspace", subtitle: "Navigate to a different workspace", - section: "Workspace", + section: "Workspaces", keywords: ["change", "go to", "workspace"], shortcutHint: "⌘P", run: () => action("command-executed")("workspace.switch"), @@ -30,7 +30,7 @@ const mockCommands: CommandAction[] = [ id: "workspace.delete", title: "Delete Workspace", subtitle: "Remove the current workspace", - section: "Workspace", + section: "Workspaces", keywords: ["remove", "delete", "workspace"], run: () => action("command-executed")("workspace.delete"), }, @@ -183,18 +183,24 @@ export const Default: Story = { reopen it.

- Features: + Two Modes: +
Default: Workspace switcher (only shows switching commands) +
•{" "} + + Type > + + : Command mode (shows all other commands) +
•{" "} + + Type / + + : Slash commands for chat input
- • Type to filter commands by title, subtitle, or keywords
- • Use ↑↓ arrow keys to navigate -
- • Press Enter to execute a command + • Use ↑↓ arrow keys to navigate, Enter to execute
• Press Escape to close -
• Start with / to see slash commands -
• Commands are organized into sections (Workspace, Chat, Mode, Settings, Project, - Help) +
• Commands organized into sections (Workspaces, Chat, Mode, Settings, Project, Help) diff --git a/src/components/CommandPalette.test.ts b/src/components/CommandPalette.test.ts new file mode 100644 index 000000000..7dc4ea8de --- /dev/null +++ b/src/components/CommandPalette.test.ts @@ -0,0 +1,127 @@ +import { describe, expect, test } from "bun:test"; +import { filterCommandsByPrefix } from "@/utils/commandPaletteFiltering"; +import { CommandIds, CommandIdMatchers } from "@/utils/commandIds"; + +/** + * Tests for command palette filtering logic + * Property-based tests that verify behavior regardless of specific command data + */ + +describe("CommandPalette filtering", () => { + describe("property: default mode shows only ws:switch:* commands", () => { + test("all results start with ws:switch:", () => { + const actions = [ + { id: CommandIds.workspaceSwitch("1") }, + { id: CommandIds.workspaceSwitch("2") }, + { id: CommandIds.workspaceNew() }, + { id: CommandIds.navToggleSidebar() }, + ]; + + const result = filterCommandsByPrefix("", actions); + + expect(result.every((a) => CommandIdMatchers.isWorkspaceSwitch(a.id))).toBe(true); + }); + + test("excludes all non-switching commands", () => { + const actions = [ + { id: CommandIds.workspaceSwitch("1") }, + { id: CommandIds.workspaceNew() }, + { id: CommandIds.workspaceRemove() }, + { id: CommandIds.navToggleSidebar() }, + ]; + + const result = filterCommandsByPrefix("", actions); + + expect(result.some((a) => !CommandIdMatchers.isWorkspaceSwitch(a.id))).toBe(false); + }); + }); + + describe("property: > mode shows all EXCEPT ws:switch:* commands", () => { + test("no results start with ws:switch:", () => { + const actions = [ + { id: CommandIds.workspaceSwitch("1") }, + { id: CommandIds.workspaceNew() }, + { id: CommandIds.navToggleSidebar() }, + { id: CommandIds.chatClear() }, + ]; + + const result = filterCommandsByPrefix(">", actions); + + expect(result.every((a) => !CommandIdMatchers.isWorkspaceSwitch(a.id))).toBe(true); + }); + + test("includes all non-switching commands", () => { + const actions = [ + { id: CommandIds.workspaceSwitch("1") }, + { id: CommandIds.workspaceNew() }, + { id: CommandIds.workspaceRemove() }, + { id: CommandIds.navToggleSidebar() }, + ]; + + const result = filterCommandsByPrefix(">", actions); + + // Should include workspace mutations + expect(result.some((a) => a.id === CommandIds.workspaceNew())).toBe(true); + expect(result.some((a) => a.id === CommandIds.workspaceRemove())).toBe(true); + // Should include navigation + expect(result.some((a) => a.id === CommandIds.navToggleSidebar())).toBe(true); + // Should NOT include switching + expect(result.some((a) => a.id === CommandIds.workspaceSwitch("1"))).toBe(false); + }); + }); + + describe("property: modes partition the command space", () => { + test("default + > modes cover all commands (no overlap, no gaps)", () => { + const actions = [ + { id: CommandIds.workspaceSwitch("1") }, + { id: CommandIds.workspaceSwitch("2") }, + { id: CommandIds.workspaceNew() }, + { id: CommandIds.workspaceRemove() }, + { id: CommandIds.navToggleSidebar() }, + { id: CommandIds.chatClear() }, + ]; + + const defaultResult = filterCommandsByPrefix("", actions); + const commandResult = filterCommandsByPrefix(">", actions); + + // No overlap - disjoint sets + const defaultIds = new Set(defaultResult.map((a) => a.id)); + const commandIds = new Set(commandResult.map((a) => a.id)); + const intersection = [...defaultIds].filter((id) => commandIds.has(id)); + expect(intersection).toHaveLength(0); + + // No gaps - covers everything + expect(defaultResult.length + commandResult.length).toBe(actions.length); + }); + }); + + describe("property: / prefix always returns empty", () => { + test("returns empty array regardless of actions", () => { + const actions = [ + { id: CommandIds.workspaceSwitch("1") }, + { id: CommandIds.workspaceNew() }, + { id: CommandIds.navToggleSidebar() }, + ]; + + expect(filterCommandsByPrefix("/", actions)).toHaveLength(0); + expect(filterCommandsByPrefix("/help", actions)).toHaveLength(0); + expect(filterCommandsByPrefix("/ ", actions)).toHaveLength(0); + }); + }); + + describe("property: query with > prefix applies to all non-switching", () => { + test(">text shows same set as > (cmdk filters further)", () => { + const actions = [ + { id: CommandIds.workspaceSwitch("1") }, + { id: CommandIds.workspaceNew() }, + { id: CommandIds.navToggleSidebar() }, + ]; + + // Our filter doesn't care about text after >, just the prefix + const resultEmpty = filterCommandsByPrefix(">", actions); + const resultWithText = filterCommandsByPrefix(">abc", actions); + + expect(resultEmpty).toEqual(resultWithText); + }); + }); +}); diff --git a/src/components/CommandPalette.tsx b/src/components/CommandPalette.tsx index 307b20b73..434e882ef 100644 --- a/src/components/CommandPalette.tsx +++ b/src/components/CommandPalette.tsx @@ -5,6 +5,7 @@ import type { CommandAction } from "@/contexts/CommandRegistryContext"; import { formatKeybind, KEYBINDS, isEditableElement, matchesKeybind } from "@/utils/ui/keybinds"; import { getSlashCommandSuggestions } from "@/utils/slashCommands/suggestions"; import { CUSTOM_EVENTS, createCustomEvent } from "@/constants/events"; +import { filterCommandsByPrefix } from "@/utils/commandPaletteFiltering"; interface CommandPaletteProps { getSlashContext?: () => { providerNames: string[]; workspaceId?: string }; @@ -42,32 +43,34 @@ export const CommandPalette: React.FC = ({ getSlashContext }>(null); const [promptError, setPromptError] = useState(null); + const resetPaletteState = useCallback(() => { + setActivePrompt(null); + setPromptError(null); + setQuery(""); + }, []); + // Close palette with Escape useEffect(() => { const onKey = (e: KeyboardEvent) => { if (matchesKeybind(e, KEYBINDS.CANCEL) && isOpen) { e.preventDefault(); - setActivePrompt(null); - setPromptError(null); - setQuery(""); + resetPaletteState(); close(); } }; window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey); - }, [isOpen, close]); + }, [isOpen, close, resetPaletteState]); // Reset state whenever palette visibility changes useEffect(() => { if (!isOpen) { - setActivePrompt(null); - setPromptError(null); - setQuery(""); + resetPaletteState(); } else { setPromptError(null); setQuery(""); } - }, [isOpen]); + }, [isOpen, resetPaletteState]); const rawActions = getActions(); @@ -200,7 +203,10 @@ export const CommandPalette: React.FC = ({ getSlashContext } satisfies { groups: PaletteGroup[]; emptyText: string | undefined }; } - const filtered = [...rawActions].sort((a, b) => { + // Filter actions based on prefix (extracted to utility for testing) + const actionsToShow = filterCommandsByPrefix(q, rawActions); + + const filtered = [...actionsToShow].sort((a, b) => { const ai = recentIndex.has(a.id) ? recentIndex.get(a.id)! : 9999; const bi = recentIndex.has(b.id) ? recentIndex.get(b.id)! : 9999; if (ai !== bi) return ai - bi; @@ -298,6 +304,8 @@ export const CommandPalette: React.FC = ({ getSlashContext }, [currentField, activePrompt]); const isSlashQuery = !currentField && query.trim().startsWith("/"); + const isCommandQuery = !currentField && query.trim().startsWith(">"); + // Enable cmdk filtering for all cases except slash queries (which we handle manually) const shouldUseCmdkFilter = currentField ? currentField.type === "select" : !isSlashQuery; let groups: PaletteGroup[] = generalResults.groups; @@ -355,9 +363,7 @@ export const CommandPalette: React.FC = ({ getSlashContext
{ - setActivePrompt(null); - setPromptError(null); - setQuery(""); + resetPaletteState(); close(); }} > @@ -365,6 +371,18 @@ export const CommandPalette: React.FC = ({ getSlashContext className="bg-separator border-border text-lighter font-primary w-[min(720px,92vw)] overflow-hidden rounded-lg border shadow-[0_10px_40px_rgba(0,0,0,0.4)]" onMouseDown={(e: React.MouseEvent) => e.stopPropagation()} shouldFilter={shouldUseCmdkFilter} + filter={(value, search) => { + // When using ">" prefix, filter using the text after ">" + if (isCommandQuery && search.startsWith(">")) { + const actualSearch = search.slice(1).trim().toLowerCase(); + if (!actualSearch) return 1; + if (value.toLowerCase().includes(actualSearch)) return 1; + return 0; + } + // Default cmdk filtering for other cases + if (value.toLowerCase().includes(search.toLowerCase())) return 1; + return 0; + }} > = ({ getSlashContext ? currentField.type === "text" ? (currentField.placeholder ?? "Type value…") : (currentField.placeholder ?? "Search options…") - : `Type a command… (${formatKeybind(KEYBINDS.CANCEL)} to close, ${formatKeybind(KEYBINDS.SEND_MESSAGE)} to send in chat)` + : `Switch workspaces or type > for all commands, / for slash commands…` } autoFocus onKeyDown={(e: React.KeyboardEvent) => { @@ -389,9 +407,7 @@ export const CommandPalette: React.FC = ({ getSlashContext } else if (e.key === "Escape") { e.preventDefault(); e.stopPropagation(); - setActivePrompt(null); - setPromptError(null); - setQuery(""); + resetPaletteState(); close(); } return; diff --git a/src/utils/commandIds.ts b/src/utils/commandIds.ts new file mode 100644 index 000000000..e30ae8854 --- /dev/null +++ b/src/utils/commandIds.ts @@ -0,0 +1,75 @@ +/** + * Centralized command ID construction and matching + * Single source of truth for all command ID patterns + */ + +/** + * Command ID prefixes for pattern matching + * Single source of truth for all dynamic ID patterns + */ +const COMMAND_ID_PREFIXES = { + WS_SWITCH: "ws:switch:", + CHAT_TRUNCATE: "chat:truncate:", + PROJECT_REMOVE: "project:remove:", +} as const; + +/** + * Command ID builders - construct IDs with consistent patterns + */ +export const CommandIds = { + // Workspace commands + workspaceSwitch: (workspaceId: string) => + `${COMMAND_ID_PREFIXES.WS_SWITCH}${workspaceId}` as const, + workspaceNew: () => "ws:new" as const, + workspaceNewInProject: () => "ws:new-in-project" as const, + workspaceRemove: () => "ws:remove" as const, + workspaceRemoveAny: () => "ws:remove-any" as const, + workspaceRename: () => "ws:rename" as const, + workspaceRenameAny: () => "ws:rename-any" as const, + workspaceOpenTerminal: () => "ws:open-terminal" as const, + workspaceOpenTerminalCurrent: () => "ws:open-terminal-current" as const, + + // Navigation commands + navNext: () => "nav:next" as const, + navPrev: () => "nav:prev" as const, + navToggleSidebar: () => "nav:toggleSidebar" as const, + + // Chat commands + chatClear: () => "chat:clear" as const, + chatTruncate: (pct: number) => `${COMMAND_ID_PREFIXES.CHAT_TRUNCATE}${pct}` as const, + chatInterrupt: () => "chat:interrupt" as const, + chatJumpBottom: () => "chat:jumpBottom" as const, + + // Mode commands + modeToggle: () => "mode:toggle" as const, + modelChange: () => "model:change" as const, + thinkingSetLevel: () => "thinking:set-level" as const, + + // Project commands + projectAdd: () => "project:add" as const, + projectRemove: (projectPath: string) => + `${COMMAND_ID_PREFIXES.PROJECT_REMOVE}${projectPath}` as const, + + // Help commands + helpKeybinds: () => "help:keybinds" as const, +} as const; + +/** + * Command ID matchers - test if an ID matches a pattern + */ +export const CommandIdMatchers = { + /** + * Check if ID is a workspace switching command (ws:switch:*) + */ + isWorkspaceSwitch: (id: string): boolean => id.startsWith(COMMAND_ID_PREFIXES.WS_SWITCH), + + /** + * Check if ID is a chat truncate command (chat:truncate:*) + */ + isChatTruncate: (id: string): boolean => id.startsWith(COMMAND_ID_PREFIXES.CHAT_TRUNCATE), + + /** + * Check if ID is a project remove command (project:remove:*) + */ + isProjectRemove: (id: string): boolean => id.startsWith(COMMAND_ID_PREFIXES.PROJECT_REMOVE), +} as const; diff --git a/src/utils/commandPaletteFiltering.ts b/src/utils/commandPaletteFiltering.ts new file mode 100644 index 000000000..e92902d84 --- /dev/null +++ b/src/utils/commandPaletteFiltering.ts @@ -0,0 +1,40 @@ +/** + * Filtering logic for command palette + * Separates workspace switching from all other commands + */ + +import { CommandIdMatchers } from "@/utils/commandIds"; + +export interface CommandActionMinimal { + id: string; +} + +/** + * Filters commands based on query prefix + * + * @param query - User's search query + * @param actions - All available actions + * @returns Filtered actions based on mode: + * - Default (no prefix): Only workspace switching commands (ws:switch:*) + * - ">" prefix: All commands EXCEPT workspace switching + * - "/" prefix: Empty (slash commands handled separately) + */ +export function filterCommandsByPrefix( + query: string, + actions: T[] +): T[] { + const q = query.trim(); + + // Slash commands are handled separately in the component + if (q.startsWith("/")) { + return []; + } + + const showAllCommands = q.startsWith(">"); + + // Default: show only workspace switching commands + // With ">": show all commands EXCEPT workspace switching + return showAllCommands + ? actions.filter((action) => !CommandIdMatchers.isWorkspaceSwitch(action.id)) + : actions.filter((action) => CommandIdMatchers.isWorkspaceSwitch(action.id)); +} diff --git a/src/utils/commands/sources.ts b/src/utils/commands/sources.ts index 99fa6ba1a..8018717ac 100644 --- a/src/utils/commands/sources.ts +++ b/src/utils/commands/sources.ts @@ -2,6 +2,7 @@ import type { CommandAction } from "@/contexts/CommandRegistryContext"; import { formatKeybind, KEYBINDS } from "@/utils/ui/keybinds"; import type { ThinkingLevel } from "@/types/thinking"; import { CUSTOM_EVENTS, createCustomEvent } from "@/constants/events"; +import { CommandIds } from "@/utils/commandIds"; import type { ProjectConfig } from "@/config"; import type { FrontendWorkspaceMetadata } from "@/types/workspace"; @@ -44,13 +45,26 @@ export interface BuildSourcesParams { const THINKING_LEVELS: ThinkingLevel[] = ["off", "low", "medium", "high"]; +/** + * Command palette section names + * Exported for use in filtering and command organization + */ +export const COMMAND_SECTIONS = { + WORKSPACES: "Workspaces", + NAVIGATION: "Navigation", + CHAT: "Chat", + MODE: "Modes & Model", + HELP: "Help", + PROJECTS: "Projects", +} as const; + const section = { - workspaces: "Workspaces", - navigation: "Navigation", - chat: "Chat", - mode: "Modes & Model", - help: "Help", - projects: "Projects", + workspaces: COMMAND_SECTIONS.WORKSPACES, + navigation: COMMAND_SECTIONS.NAVIGATION, + chat: COMMAND_SECTIONS.CHAT, + mode: COMMAND_SECTIONS.MODE, + help: COMMAND_SECTIONS.HELP, + projects: COMMAND_SECTIONS.PROJECTS, }; export function buildCoreSources(p: BuildSourcesParams): Array<() => CommandAction[]> { @@ -64,7 +78,7 @@ export function buildCoreSources(p: BuildSourcesParams): Array<() => CommandActi selected: NonNullable ): CommandAction => { return { - id: "ws:new", + id: CommandIds.workspaceNew(), title: "Create New Workspace…", subtitle: `for ${selected.projectName}`, section: section.workspaces, @@ -88,7 +102,7 @@ export function buildCoreSources(p: BuildSourcesParams): Array<() => CommandActi const isCurrent = selected?.workspaceId === meta.id; const isStreaming = p.streamingModels?.has(meta.id) ?? false; list.push({ - id: `ws:switch:${meta.id}`, + id: CommandIds.workspaceSwitch(meta.id), title: `${isCurrent ? "• " : ""}Switch to ${meta.name}`, subtitle: `${meta.projectName}${isStreaming ? " • streaming" : ""}`, section: section.workspaces, @@ -107,7 +121,7 @@ export function buildCoreSources(p: BuildSourcesParams): Array<() => CommandActi if (selected?.namedWorkspacePath) { const workspaceDisplayName = `${selected.projectName}/${selected.namedWorkspacePath.split("/").pop() ?? selected.namedWorkspacePath}`; list.push({ - id: "ws:open-terminal-current", + id: CommandIds.workspaceOpenTerminalCurrent(), title: "Open Current Workspace in Terminal", subtitle: workspaceDisplayName, section: section.workspaces, @@ -117,7 +131,7 @@ export function buildCoreSources(p: BuildSourcesParams): Array<() => CommandActi }, }); list.push({ - id: "ws:remove", + id: CommandIds.workspaceRemove(), title: "Remove Current Workspace…", subtitle: workspaceDisplayName, section: section.workspaces, @@ -127,7 +141,7 @@ export function buildCoreSources(p: BuildSourcesParams): Array<() => CommandActi }, }); list.push({ - id: "ws:rename", + id: CommandIds.workspaceRename(), title: "Rename Current Workspace…", subtitle: workspaceDisplayName, section: section.workspaces, @@ -155,7 +169,7 @@ export function buildCoreSources(p: BuildSourcesParams): Array<() => CommandActi if (p.workspaceMetadata.size > 0) { list.push({ - id: "ws:open-terminal", + id: CommandIds.workspaceOpenTerminal(), title: "Open Workspace in Terminal…", section: section.workspaces, run: () => undefined, @@ -185,7 +199,7 @@ export function buildCoreSources(p: BuildSourcesParams): Array<() => CommandActi }, }); list.push({ - id: "ws:rename-any", + id: CommandIds.workspaceRenameAny(), title: "Rename Workspace…", section: section.workspaces, run: () => undefined, @@ -227,7 +241,7 @@ export function buildCoreSources(p: BuildSourcesParams): Array<() => CommandActi }, }); list.push({ - id: "ws:remove-any", + id: CommandIds.workspaceRemoveAny(), title: "Remove Workspace…", section: section.workspaces, run: () => undefined, @@ -270,21 +284,21 @@ export function buildCoreSources(p: BuildSourcesParams): Array<() => CommandActi // Navigation / Interface actions.push(() => [ { - id: "nav:next", + id: CommandIds.navNext(), title: "Next Workspace", section: section.navigation, shortcutHint: formatKeybind(KEYBINDS.NEXT_WORKSPACE), run: () => p.onNavigateWorkspace("next"), }, { - id: "nav:prev", + id: CommandIds.navPrev(), title: "Previous Workspace", section: section.navigation, shortcutHint: formatKeybind(KEYBINDS.PREV_WORKSPACE), run: () => p.onNavigateWorkspace("prev"), }, { - id: "nav:toggleSidebar", + id: CommandIds.navToggleSidebar(), title: "Toggle Sidebar", section: section.navigation, shortcutHint: formatKeybind(KEYBINDS.TOGGLE_SIDEBAR), @@ -298,7 +312,7 @@ export function buildCoreSources(p: BuildSourcesParams): Array<() => CommandActi if (p.selectedWorkspace) { const id = p.selectedWorkspace.workspaceId; list.push({ - id: "chat:clear", + id: CommandIds.chatClear(), title: "Clear History", section: section.chat, run: async () => { @@ -307,7 +321,7 @@ export function buildCoreSources(p: BuildSourcesParams): Array<() => CommandActi }); for (const pct of [0.75, 0.5, 0.25]) { list.push({ - id: `chat:truncate:${pct}`, + id: CommandIds.chatTruncate(pct), title: `Truncate History to ${Math.round((1 - pct) * 100)}%`, section: section.chat, run: async () => { @@ -316,7 +330,7 @@ export function buildCoreSources(p: BuildSourcesParams): Array<() => CommandActi }); } list.push({ - id: "chat:interrupt", + id: CommandIds.chatInterrupt(), title: "Interrupt Streaming", section: section.chat, run: async () => { @@ -324,7 +338,7 @@ export function buildCoreSources(p: BuildSourcesParams): Array<() => CommandActi }, }); list.push({ - id: "chat:jumpBottom", + id: CommandIds.chatJumpBottom(), title: "Jump to Bottom", section: section.chat, shortcutHint: formatKeybind(KEYBINDS.JUMP_TO_BOTTOM), @@ -342,7 +356,7 @@ export function buildCoreSources(p: BuildSourcesParams): Array<() => CommandActi actions.push(() => { const list: CommandAction[] = [ { - id: "mode:toggle", + id: CommandIds.modeToggle(), title: "Toggle Plan/Exec Mode", section: section.mode, shortcutHint: formatKeybind(KEYBINDS.TOGGLE_MODE), @@ -352,7 +366,7 @@ export function buildCoreSources(p: BuildSourcesParams): Array<() => CommandActi }, }, { - id: "model:change", + id: CommandIds.modelChange(), title: "Change Model…", section: section.mode, shortcutHint: formatKeybind(KEYBINDS.OPEN_MODEL_SELECTOR), @@ -374,7 +388,7 @@ export function buildCoreSources(p: BuildSourcesParams): Array<() => CommandActi const currentLevel = p.getThinkingLevel(workspaceId); list.push({ - id: "thinking:set-level", + id: CommandIds.thinkingSetLevel(), title: "Set Thinking Effort…", subtitle: `Current: ${levelDescriptions[currentLevel] ?? currentLevel}`, section: section.mode, @@ -417,7 +431,7 @@ export function buildCoreSources(p: BuildSourcesParams): Array<() => CommandActi // Help / Docs actions.push(() => [ { - id: "help:keybinds", + id: CommandIds.helpKeybinds(), title: "Show Keyboard Shortcuts", section: section.help, run: () => { @@ -434,13 +448,13 @@ export function buildCoreSources(p: BuildSourcesParams): Array<() => CommandActi actions.push(() => { const list: CommandAction[] = [ { - id: "project:add", + id: CommandIds.projectAdd(), title: "Add Project…", section: section.projects, run: () => p.onAddProject(), }, { - id: "ws:new-in-project", + id: CommandIds.workspaceNewInProject(), title: "Create New Workspace in Project…", section: section.projects, run: () => undefined, @@ -472,7 +486,7 @@ export function buildCoreSources(p: BuildSourcesParams): Array<() => CommandActi for (const [projectPath] of p.projects.entries()) { const projectName = projectPath.split("/").pop() ?? projectPath; list.push({ - id: `project:remove:${projectPath}`, + id: CommandIds.projectRemove(projectPath), title: `Remove Project ${projectName}…`, section: section.projects, run: () => p.onRemoveProject(projectPath),