Skip to content

Commit bea9984

Browse files
committed
🤖 refactor: make command palette a workspace switcher by default
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`_
1 parent 033eccc commit bea9984

File tree

4 files changed

+76
-32
lines changed

4 files changed

+76
-32
lines changed

docs/keybinds.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,16 @@ When documentation shows `Ctrl`, it means:
5454
| Open command palette | `Ctrl+Shift+P` |
5555
| Toggle sidebar | `Ctrl+P` |
5656

57+
### Command Palette
58+
59+
The command palette (`Ctrl+Shift+P`) is primarily a **workspace switcher** by default:
60+
61+
- **Default**: Shows only workspace commands (switch, create, rename, etc.)
62+
- **`>` prefix**: Shows all commands (navigation, chat, modes, projects, etc.)
63+
- **`/` prefix**: Shows slash command suggestions for inserting into chat
64+
65+
This design keeps the palette focused on the most common use case (workspace switching) while still providing quick access to all commands when needed.
66+
5767
## Tips
5868

5969
- **Vim-inspired navigation**: We use `J`/`K` for next/previous navigation, similar to Vim

src/components/CommandPalette.stories.tsx

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ const mockCommands: CommandAction[] = [
1212
id: "workspace.create",
1313
title: "Create New Workspace",
1414
subtitle: "Start a new workspace in this project",
15-
section: "Workspace",
15+
section: "Workspaces",
1616
keywords: ["new", "add", "workspace"],
1717
shortcutHint: "⌘N",
1818
run: () => action("command-executed")("workspace.create"),
@@ -21,7 +21,7 @@ const mockCommands: CommandAction[] = [
2121
id: "workspace.switch",
2222
title: "Switch Workspace",
2323
subtitle: "Navigate to a different workspace",
24-
section: "Workspace",
24+
section: "Workspaces",
2525
keywords: ["change", "go to", "workspace"],
2626
shortcutHint: "⌘P",
2727
run: () => action("command-executed")("workspace.switch"),
@@ -30,7 +30,7 @@ const mockCommands: CommandAction[] = [
3030
id: "workspace.delete",
3131
title: "Delete Workspace",
3232
subtitle: "Remove the current workspace",
33-
section: "Workspace",
33+
section: "Workspaces",
3434
keywords: ["remove", "delete", "workspace"],
3535
run: () => action("command-executed")("workspace.delete"),
3636
},
@@ -185,15 +185,14 @@ export const Default: Story = {
185185
<br />
186186
<strong>Features:</strong>
187187
<br />
188-
• Type to filter commands by title, subtitle, or keywords
188+
• By default, shows workspace switching commands
189+
<br />• Type <kbd>&gt;</kbd> to see all commands across all sections
190+
<br />• Type <kbd>/</kbd> to see slash commands for chat input
189191
<br />
190-
• Use ↑↓ arrow keys to navigate
191-
<br />
192-
• Press Enter to execute a command
192+
• Use ↑↓ arrow keys to navigate, Enter to execute
193193
<br />
194194
• Press Escape to close
195-
<br />• Start with <kbd>/</kbd> to see slash commands
196-
<br />• Commands are organized into sections (Workspace, Chat, Mode, Settings, Project,
195+
<br />• Commands are organized into sections (Workspaces, Chat, Mode, Settings, Project,
197196
Help)
198197
</div>
199198
<PaletteDemo />

src/components/CommandPalette.tsx

Lines changed: 39 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type { CommandAction } from "@/contexts/CommandRegistryContext";
55
import { formatKeybind, KEYBINDS, isEditableElement, matchesKeybind } from "@/utils/ui/keybinds";
66
import { getSlashCommandSuggestions } from "@/utils/slashCommands/suggestions";
77
import { CUSTOM_EVENTS } from "@/constants/events";
8+
import { COMMAND_SECTIONS } from "@/utils/commands/sources";
89

910
interface CommandPaletteProps {
1011
getSlashContext?: () => { providerNames: string[]; workspaceId?: string };
@@ -42,32 +43,34 @@ export const CommandPalette: React.FC<CommandPaletteProps> = ({ getSlashContext
4243
}>(null);
4344
const [promptError, setPromptError] = useState<string | null>(null);
4445

46+
const resetPaletteState = useCallback(() => {
47+
setActivePrompt(null);
48+
setPromptError(null);
49+
setQuery("");
50+
}, []);
51+
4552
// Close palette with Escape
4653
useEffect(() => {
4754
const onKey = (e: KeyboardEvent) => {
4855
if (matchesKeybind(e, KEYBINDS.CANCEL) && isOpen) {
4956
e.preventDefault();
50-
setActivePrompt(null);
51-
setPromptError(null);
52-
setQuery("");
57+
resetPaletteState();
5358
close();
5459
}
5560
};
5661
window.addEventListener("keydown", onKey);
5762
return () => window.removeEventListener("keydown", onKey);
58-
}, [isOpen, close]);
63+
}, [isOpen, close, resetPaletteState]);
5964

6065
// Reset state whenever palette visibility changes
6166
useEffect(() => {
6267
if (!isOpen) {
63-
setActivePrompt(null);
64-
setPromptError(null);
65-
setQuery("");
68+
resetPaletteState();
6669
} else {
6770
setPromptError(null);
6871
setQuery("");
6972
}
70-
}, [isOpen]);
73+
}, [isOpen, resetPaletteState]);
7174

7275
const rawActions = getActions();
7376

@@ -202,7 +205,15 @@ export const CommandPalette: React.FC<CommandPaletteProps> = ({ getSlashContext
202205
} satisfies { groups: PaletteGroup[]; emptyText: string | undefined };
203206
}
204207

205-
const filtered = [...rawActions].sort((a, b) => {
208+
// Filter actions based on prefix
209+
const showAllCommands = q.startsWith(">");
210+
211+
// When no prefix is used, only show workspace-related commands
212+
const actionsToShow = showAllCommands
213+
? rawActions
214+
: rawActions.filter((action) => action.section === COMMAND_SECTIONS.WORKSPACES);
215+
216+
const filtered = [...actionsToShow].sort((a, b) => {
206217
const ai = recentIndex.has(a.id) ? recentIndex.get(a.id)! : 9999;
207218
const bi = recentIndex.has(b.id) ? recentIndex.get(b.id)! : 9999;
208219
if (ai !== bi) return ai - bi;
@@ -300,7 +311,10 @@ export const CommandPalette: React.FC<CommandPaletteProps> = ({ getSlashContext
300311
}, [currentField, activePrompt]);
301312

302313
const isSlashQuery = !currentField && query.trim().startsWith("/");
303-
const shouldUseCmdkFilter = currentField ? currentField.type === "select" : !isSlashQuery;
314+
const isCommandQuery = !currentField && query.trim().startsWith(">");
315+
const shouldUseCmdkFilter = currentField
316+
? currentField.type === "select"
317+
: !isSlashQuery && !isCommandQuery;
304318

305319
let groups: PaletteGroup[] = generalResults.groups;
306320
let emptyText: string | undefined = generalResults.emptyText;
@@ -357,16 +371,26 @@ export const CommandPalette: React.FC<CommandPaletteProps> = ({ getSlashContext
357371
<div
358372
className="fixed inset-0 z-[2000] flex items-start justify-center bg-black/40 pt-[10vh]"
359373
onMouseDown={() => {
360-
setActivePrompt(null);
361-
setPromptError(null);
362-
setQuery("");
374+
resetPaletteState();
363375
close();
364376
}}
365377
>
366378
<Command
367379
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)]"
368380
onMouseDown={(e: React.MouseEvent) => e.stopPropagation()}
369381
shouldFilter={shouldUseCmdkFilter}
382+
filter={(value, search) => {
383+
// When using ">" prefix, filter using the text after ">"
384+
if (isCommandQuery && search.startsWith(">")) {
385+
const actualSearch = search.slice(1).trim().toLowerCase();
386+
if (!actualSearch) return 1;
387+
if (value.toLowerCase().includes(actualSearch)) return 1;
388+
return 0;
389+
}
390+
// Default cmdk filtering for other cases
391+
if (value.toLowerCase().includes(search.toLowerCase())) return 1;
392+
return 0;
393+
}}
370394
>
371395
<Command.Input
372396
className="bg-darker text-lighter border-hover w-full border-b border-none px-3.5 py-3 text-sm outline-none"
@@ -377,7 +401,7 @@ export const CommandPalette: React.FC<CommandPaletteProps> = ({ getSlashContext
377401
? currentField.type === "text"
378402
? (currentField.placeholder ?? "Type value…")
379403
: (currentField.placeholder ?? "Search options…")
380-
: `Type a command… (${formatKeybind(KEYBINDS.CANCEL)} to close, ${formatKeybind(KEYBINDS.SEND_MESSAGE)} to send in chat)`
404+
: `Switch workspaces or type > for all commands, / for slash commands…`
381405
}
382406
autoFocus
383407
onKeyDown={(e: React.KeyboardEvent) => {
@@ -391,9 +415,7 @@ export const CommandPalette: React.FC<CommandPaletteProps> = ({ getSlashContext
391415
} else if (e.key === "Escape") {
392416
e.preventDefault();
393417
e.stopPropagation();
394-
setActivePrompt(null);
395-
setPromptError(null);
396-
setQuery("");
418+
resetPaletteState();
397419
close();
398420
}
399421
return;

src/utils/commands/sources.ts

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -44,13 +44,26 @@ export interface BuildSourcesParams {
4444

4545
const THINKING_LEVELS: ThinkingLevel[] = ["off", "low", "medium", "high"];
4646

47+
/**
48+
* Command palette section names
49+
* Exported for use in filtering and command organization
50+
*/
51+
export const COMMAND_SECTIONS = {
52+
WORKSPACES: "Workspaces",
53+
NAVIGATION: "Navigation",
54+
CHAT: "Chat",
55+
MODE: "Modes & Model",
56+
HELP: "Help",
57+
PROJECTS: "Projects",
58+
} as const;
59+
4760
const section = {
48-
workspaces: "Workspaces",
49-
navigation: "Navigation",
50-
chat: "Chat",
51-
mode: "Modes & Model",
52-
help: "Help",
53-
projects: "Projects",
61+
workspaces: COMMAND_SECTIONS.WORKSPACES,
62+
navigation: COMMAND_SECTIONS.NAVIGATION,
63+
chat: COMMAND_SECTIONS.CHAT,
64+
mode: COMMAND_SECTIONS.MODE,
65+
help: COMMAND_SECTIONS.HELP,
66+
projects: COMMAND_SECTIONS.PROJECTS,
5467
};
5568

5669
export function buildCoreSources(p: BuildSourcesParams): Array<() => CommandAction[]> {

0 commit comments

Comments
 (0)