= ({ 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),