From e4a2d30b1320124f68a89c20c60c28b6a4887585 Mon Sep 17 00:00:00 2001 From: Ammar Date: Wed, 29 Oct 2025 00:39:43 +0000 Subject: [PATCH 1/3] =?UTF-8?q?=F0=9F=A4=96=20refactor:=20make=20command?= =?UTF-8?q?=20palette=20a=20workspace=20switcher=20by=20default?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Make the command palette primarily a workspace switcher while keeping all commands accessible via prefix keys. This focuses the palette on the most common use case (switching workspaces) without sacrificing functionality. **Changes:** - Default view shows only workspace commands (switch, create, rename, etc.) - Type `>` prefix to see all commands across all sections - Type `/` prefix for slash commands (existing behavior) - Updated placeholder text to reflect new behavior **Code quality improvements:** - Extracted repeated state reset logic into `resetPaletteState()` helper - Exported command section names as `COMMAND_SECTIONS` constant - Removed unused `searchQuery` variable - Updated documentation and Storybook stories **Documentation:** - Added command palette section to docs/keybinds.md explaining three modes - Updated Storybook story with accurate feature descriptions _Generated with `cmux`_ --- docs/keybinds.md | 10 ++++ src/components/CommandPalette.stories.tsx | 17 ++++--- src/components/CommandPalette.tsx | 56 ++++++++++++++++------- src/utils/commands/sources.ts | 25 +++++++--- 4 files changed, 76 insertions(+), 32 deletions(-) diff --git a/docs/keybinds.md b/docs/keybinds.md index 5a3c1d644..fedfebc1f 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 commands (switch, create, rename, etc.) +- **`>` prefix**: Shows all commands (navigation, chat, modes, projects, etc.) +- **`/` prefix**: Shows slash command suggestions for inserting into chat + +This design keeps the palette focused on the most common use case (workspace switching) 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..13767d880 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 workspace switching commands +
• Type > to see all commands across all sections +
• 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..b0f1c371c 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 } from "@/constants/events"; +import { COMMAND_SECTIONS } from "@/utils/commands/sources"; 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(); @@ -202,7 +205,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-related commands + const actionsToShow = showAllCommands + ? rawActions + : rawActions.filter((action) => action.section === COMMAND_SECTIONS.WORKSPACES); + + 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,7 +311,10 @@ export const CommandPalette: React.FC = ({ getSlashContext }, [currentField, activePrompt]); const isSlashQuery = !currentField && query.trim().startsWith("/"); - const shouldUseCmdkFilter = currentField ? currentField.type === "select" : !isSlashQuery; + const isCommandQuery = !currentField && query.trim().startsWith(">"); + const shouldUseCmdkFilter = currentField + ? currentField.type === "select" + : !isSlashQuery && !isCommandQuery; let groups: PaletteGroup[] = generalResults.groups; let emptyText: string | undefined = generalResults.emptyText; @@ -357,9 +371,7 @@ export const CommandPalette: React.FC = ({ getSlashContext
{ - setActivePrompt(null); - setPromptError(null); - setQuery(""); + resetPaletteState(); close(); }} > @@ -367,6 +379,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 +415,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[]> { From 593eec0cfd1a2c8696be2eaeb5966e07a579d63e Mon Sep 17 00:00:00 2001 From: Ammar Date: Wed, 29 Oct 2025 01:05:34 +0000 Subject: [PATCH 2/3] refine: only show workspace switching by default, not mutations Changed the default filter to only show `ws:switch:*` commands, excluding workspace mutations like create, delete, and rename. These management commands are still accessible via the `>` prefix. **Why:** - Workspace switching is the primary use case - Create/delete/rename are mutations that should require explicit intent - Keeps the default palette extremely focused and fast **Changes:** - Filter by command ID prefix `ws:switch:*` instead of section name - Updated documentation to clarify "switching" vs "workspace management" - Updated Storybook story descriptions --- docs/keybinds.md | 6 +++--- src/components/CommandPalette.stories.tsx | 4 ++-- src/components/CommandPalette.tsx | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/keybinds.md b/docs/keybinds.md index fedfebc1f..9bdf262f7 100644 --- a/docs/keybinds.md +++ b/docs/keybinds.md @@ -58,11 +58,11 @@ When documentation shows `Ctrl`, it means: The command palette (`Ctrl+Shift+P`) is primarily a **workspace switcher** by default: -- **Default**: Shows only workspace commands (switch, create, rename, etc.) -- **`>` prefix**: Shows all commands (navigation, chat, modes, projects, etc.) +- **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 (workspace switching) while still providing quick access to all commands when needed. +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 diff --git a/src/components/CommandPalette.stories.tsx b/src/components/CommandPalette.stories.tsx index 13767d880..c207218bf 100644 --- a/src/components/CommandPalette.stories.tsx +++ b/src/components/CommandPalette.stories.tsx @@ -185,8 +185,8 @@ export const Default: Story = {
Features:
- • By default, shows workspace switching commands -
• Type > to see all commands across all sections + • 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, Enter to execute diff --git a/src/components/CommandPalette.tsx b/src/components/CommandPalette.tsx index b0f1c371c..7fe1f259c 100644 --- a/src/components/CommandPalette.tsx +++ b/src/components/CommandPalette.tsx @@ -208,10 +208,10 @@ export const CommandPalette: React.FC = ({ getSlashContext // Filter actions based on prefix const showAllCommands = q.startsWith(">"); - // When no prefix is used, only show workspace-related commands + // When no prefix is used, only show workspace switching commands (not mutations like create/delete/rename) const actionsToShow = showAllCommands ? rawActions - : rawActions.filter((action) => action.section === COMMAND_SECTIONS.WORKSPACES); + : 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; From 23eea4b79438e929f62ca1b1ce3459a454f2b29e Mon Sep 17 00:00:00 2001 From: Ammar Date: Wed, 29 Oct 2025 01:43:21 +0000 Subject: [PATCH 3/3] fix: enable cmdk filtering for > queries Fixed an issue where typing `>abc` wouldn't filter commands because `shouldUseCmdkFilter` was set to false for command queries. Now cmdk filtering is enabled for `>` queries so users can search through all commands. Addresses Codex review comment about unsearchable "all commands" mode. **Before:** - `shouldFilter={false}` for `>` queries - Custom filter was never called - Typing `>abc` showed all commands unfiltered **After:** - `shouldFilter={true}` for `>` queries - Custom filter strips `>` prefix and filters normally - Typing `>abc` narrows down to matching commands --- src/components/CommandPalette.tsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/components/CommandPalette.tsx b/src/components/CommandPalette.tsx index 7fe1f259c..6b8fce3c1 100644 --- a/src/components/CommandPalette.tsx +++ b/src/components/CommandPalette.tsx @@ -5,7 +5,6 @@ import type { CommandAction } from "@/contexts/CommandRegistryContext"; import { formatKeybind, KEYBINDS, isEditableElement, matchesKeybind } from "@/utils/ui/keybinds"; import { getSlashCommandSuggestions } from "@/utils/slashCommands/suggestions"; import { CUSTOM_EVENTS } from "@/constants/events"; -import { COMMAND_SECTIONS } from "@/utils/commands/sources"; interface CommandPaletteProps { getSlashContext?: () => { providerNames: string[]; workspaceId?: string }; @@ -312,9 +311,8 @@ export const CommandPalette: React.FC = ({ getSlashContext const isSlashQuery = !currentField && query.trim().startsWith("/"); const isCommandQuery = !currentField && query.trim().startsWith(">"); - const shouldUseCmdkFilter = currentField - ? currentField.type === "select" - : !isSlashQuery && !isCommandQuery; + // 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; let emptyText: string | undefined = generalResults.emptyText;