diff --git a/docs/keybinds.md b/docs/keybinds.md index 5a3c1d644..9bdf262f7 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`) is primarily a **workspace switcher** by default: + +- **Default**: Shows only workspace switching commands (no mutations like create/delete/rename) +- **`>` prefix**: Shows all commands (navigation, chat, modes, projects, workspace management, etc.) +- **`/` prefix**: Shows slash command suggestions for inserting into chat + +This design keeps the palette focused on the most common use case (switching between workspaces) while still providing quick access to all commands when needed. + ## 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..c207218bf 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"), }, @@ -185,15 +185,14 @@ export const Default: Story = {
Features:
- • Type to filter commands by title, subtitle, or keywords + • By default, shows only workspace switching commands (no create/delete/rename) +
• Type > to see all commands including workspace management +
• Type / to see slash commands for chat input
- • 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, +
• Commands are organized into sections (Workspaces, Chat, Mode, Settings, Project, Help) diff --git a/src/components/CommandPalette.tsx b/src/components/CommandPalette.tsx index 06da96b9c..6b8fce3c1 100644 --- a/src/components/CommandPalette.tsx +++ b/src/components/CommandPalette.tsx @@ -42,32 +42,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(); @@ -202,7 +204,15 @@ export const CommandPalette: React.FC = ({ getSlashContext } satisfies { groups: PaletteGroup[]; emptyText: string | undefined }; } - const filtered = [...rawActions].sort((a, b) => { + // Filter actions based on prefix + const showAllCommands = q.startsWith(">"); + + // When no prefix is used, only show workspace switching commands (not mutations like create/delete/rename) + const actionsToShow = showAllCommands + ? rawActions + : rawActions.filter((action) => action.id.startsWith("ws:switch:")); + + 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; @@ -300,6 +310,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; @@ -357,9 +369,7 @@ export const CommandPalette: React.FC = ({ getSlashContext
{ - setActivePrompt(null); - setPromptError(null); - setQuery(""); + resetPaletteState(); close(); }} > @@ -367,6 +377,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) => { @@ -391,9 +413,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/commands/sources.ts b/src/utils/commands/sources.ts index 6a24e2644..f0658f673 100644 --- a/src/utils/commands/sources.ts +++ b/src/utils/commands/sources.ts @@ -44,13 +44,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[]> {