Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions docs/keybinds.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
17 changes: 8 additions & 9 deletions src/components/CommandPalette.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand All @@ -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"),
Expand All @@ -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"),
},
Expand Down Expand Up @@ -185,15 +185,14 @@ export const Default: Story = {
<br />
<strong>Features:</strong>
<br />
• Type to filter commands by title, subtitle, or keywords
• By default, shows only workspace switching commands (no create/delete/rename)
<br />• Type <kbd>&gt;</kbd> to see all commands including workspace management
<br />• Type <kbd>/</kbd> to see slash commands for chat input
<br />
• Use ↑↓ arrow keys to navigate
<br />
• Press Enter to execute a command
• Use ↑↓ arrow keys to navigate, Enter to execute
<br />
• Press Escape to close
<br />• Start with <kbd>/</kbd> to see slash commands
<br />• Commands are organized into sections (Workspace, Chat, Mode, Settings, Project,
<br />• Commands are organized into sections (Workspaces, Chat, Mode, Settings, Project,
Help)
</div>
<PaletteDemo />
Expand Down
52 changes: 36 additions & 16 deletions src/components/CommandPalette.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,32 +42,34 @@ export const CommandPalette: React.FC<CommandPaletteProps> = ({ getSlashContext
}>(null);
const [promptError, setPromptError] = useState<string | null>(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();

Expand Down Expand Up @@ -202,7 +204,15 @@ export const CommandPalette: React.FC<CommandPaletteProps> = ({ 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;
Expand Down Expand Up @@ -300,6 +310,8 @@ export const CommandPalette: React.FC<CommandPaletteProps> = ({ 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;
Expand Down Expand Up @@ -357,16 +369,26 @@ export const CommandPalette: React.FC<CommandPaletteProps> = ({ getSlashContext
<div
className="fixed inset-0 z-[2000] flex items-start justify-center bg-black/40 pt-[10vh]"
onMouseDown={() => {
setActivePrompt(null);
setPromptError(null);
setQuery("");
resetPaletteState();
close();
}}
>
<Command
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;
}}
>
<Command.Input
className="bg-darker text-lighter border-hover w-full border-b border-none px-3.5 py-3 text-sm outline-none"
Expand All @@ -377,7 +399,7 @@ export const CommandPalette: React.FC<CommandPaletteProps> = ({ 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) => {
Expand All @@ -391,9 +413,7 @@ export const CommandPalette: React.FC<CommandPaletteProps> = ({ getSlashContext
} else if (e.key === "Escape") {
e.preventDefault();
e.stopPropagation();
setActivePrompt(null);
setPromptError(null);
setQuery("");
resetPaletteState();
close();
}
return;
Expand Down
25 changes: 19 additions & 6 deletions src/utils/commands/sources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[]> {
Expand Down