Add command palette with 19 commands and improved keyboard navigation

Implement VSCode-style command palette (Cmd+Shift+P) with comprehensive command organization:
- 19 commands organized into 5 categories: Instance, Session, Agent & Model, Input & Focus, System
- Category-based grouping with proper visual hierarchy
- Keyboard shortcuts displayed for all applicable commands
- Search/filter by command name, description, keywords, or category

Add keyboard shortcuts:
- Cmd+Shift+A: Open agent selector
- Cmd+Shift+M: Open model selector (existing)
- Instance/Session navigation shortcuts (Cmd+[/], Cmd+Shift+[/])

Fix keyboard navigation:
- Model selector now highlights options with arrow keys using data-[highlighted] attribute
- Agent selector properly opens via keyboard shortcut
- Global keyboard handler skips Combobox/Select components to allow native navigation

Improve discoverability:
- Prominent centered Command Palette hint in connection status bar
- Keyboard shortcut hints next to agent and model selectors

Complete task 020-command-palette
This commit is contained in:
Shantur Rathore
2025-10-23 22:32:49 +01:00
parent b06b8104a5
commit 54569b166d
9 changed files with 566 additions and 322 deletions

View File

@@ -191,21 +191,142 @@ const App: Component = () => {
function setupCommands() {
commandRegistry.register({
id: "init",
label: "Initialize AGENTS.md",
description: "Create or update AGENTS.md file",
keywords: ["/init", "agents", "initialize"],
id: "new-instance",
label: "New Instance",
description: "Open folder picker to create new instance",
category: "Instance",
keywords: ["folder", "project", "workspace"],
shortcut: { key: "N", meta: true },
action: handleSelectFolder,
})
commandRegistry.register({
id: "close-instance",
label: "Close Instance",
description: "Stop current instance's server",
category: "Instance",
keywords: ["stop", "quit", "close"],
shortcut: { key: "W", meta: true },
action: async () => {
const instance = activeInstance()
if (!instance) return
await handleCloseInstance(instance.id)
},
})
commandRegistry.register({
id: "instance-next",
label: "Next Instance",
description: "Cycle to next instance tab",
category: "Instance",
keywords: ["switch", "navigate"],
shortcut: { key: "]", meta: true },
action: () => {
const ids = Array.from(instances().keys())
if (ids.length <= 1) return
const current = ids.indexOf(activeInstanceId() || "")
const next = (current + 1) % ids.length
if (ids[next]) setActiveInstanceId(ids[next])
},
})
commandRegistry.register({
id: "instance-prev",
label: "Previous Instance",
description: "Cycle to previous instance tab",
category: "Instance",
keywords: ["switch", "navigate"],
shortcut: { key: "[", meta: true },
action: () => {
const ids = Array.from(instances().keys())
if (ids.length <= 1) return
const current = ids.indexOf(activeInstanceId() || "")
const prev = current <= 0 ? ids.length - 1 : current - 1
if (ids[prev]) setActiveInstanceId(ids[prev])
},
})
commandRegistry.register({
id: "new-session",
label: "New Session",
description: "Create a new parent session",
category: "Session",
keywords: ["create", "start"],
shortcut: { key: "N", meta: true, shift: true },
action: async () => {
const instance = activeInstance()
if (!instance) return
await handleNewSession(instance.id)
},
})
commandRegistry.register({
id: "close-session",
label: "Close Session",
description: "Close current parent session",
category: "Session",
keywords: ["close", "stop"],
shortcut: { key: "W", meta: true, shift: true },
action: async () => {
const instance = activeInstance()
const sessionId = activeSessionIdForInstance()
if (!instance || !instance.client || !sessionId || sessionId === "logs") return
if (!instance || !sessionId || sessionId === "logs") return
await handleCloseSession(instance.id, sessionId)
},
})
try {
await instance.client.session.init({ path: { id: sessionId } })
console.log("Initialized AGENTS.md")
} catch (error) {
console.error("Failed to initialize AGENTS.md:", error)
}
commandRegistry.register({
id: "switch-to-logs",
label: "Switch to Logs",
description: "Jump to logs view for current instance",
category: "Session",
keywords: ["logs", "console", "output"],
shortcut: { key: "L", meta: true, shift: true },
action: () => {
const instance = activeInstance()
if (instance) setActiveSession(instance.id, "logs")
},
})
commandRegistry.register({
id: "session-next",
label: "Next Session",
description: "Cycle to next session tab",
category: "Session",
keywords: ["switch", "navigate"],
shortcut: { key: "]", meta: true, shift: true },
action: () => {
const instanceId = activeInstanceId()
if (!instanceId) return
const parentId = activeParentSessionId().get(instanceId)
if (!parentId) return
const familySessions = getSessionFamily(instanceId, parentId)
const ids = familySessions.map((s) => s.id).concat(["logs"])
if (ids.length <= 1) return
const current = ids.indexOf(activeSessionId().get(instanceId) || "")
const next = (current + 1) % ids.length
if (ids[next]) setActiveSession(instanceId, ids[next])
},
})
commandRegistry.register({
id: "session-prev",
label: "Previous Session",
description: "Cycle to previous session tab",
category: "Session",
keywords: ["switch", "navigate"],
shortcut: { key: "[", meta: true, shift: true },
action: () => {
const instanceId = activeInstanceId()
if (!instanceId) return
const parentId = activeParentSessionId().get(instanceId)
if (!parentId) return
const familySessions = getSessionFamily(instanceId, parentId)
const ids = familySessions.map((s) => s.id).concat(["logs"])
if (ids.length <= 1) return
const current = ids.indexOf(activeSessionId().get(instanceId) || "")
const prev = current <= 0 ? ids.length - 1 : current - 1
if (ids[prev]) setActiveSession(instanceId, ids[prev])
},
})
@@ -213,6 +334,7 @@ const App: Component = () => {
id: "compact",
label: "Compact Session",
description: "Summarize and compact the current session",
category: "Session",
keywords: ["/compact", "summarize", "compress"],
action: async () => {
const instance = activeInstance()
@@ -232,6 +354,7 @@ const App: Component = () => {
id: "undo",
label: "Undo Last Message",
description: "Revert the last message",
category: "Session",
keywords: ["/undo", "revert", "undo"],
action: async () => {
const instance = activeInstance()
@@ -247,10 +370,107 @@ const App: Component = () => {
},
})
commandRegistry.register({
id: "next-agent",
label: "Next Agent",
description: "Cycle to next agent",
category: "Agent & Model",
keywords: ["agent", "switch", "cycle"],
shortcut: { key: "Tab" },
action: handleCycleAgent,
})
commandRegistry.register({
id: "prev-agent",
label: "Previous Agent",
description: "Cycle to previous agent",
category: "Agent & Model",
keywords: ["agent", "switch", "cycle"],
shortcut: { key: "Tab", shift: true },
action: handleCycleAgentReverse,
})
commandRegistry.register({
id: "open-model-selector",
label: "Open Model Selector",
description: "Choose a different model",
category: "Agent & Model",
keywords: ["model", "llm", "ai"],
shortcut: { key: "M", meta: true, shift: true },
action: () => {
const modelControl = document.querySelector("[data-model-selector]") as HTMLElement
modelControl?.click()
setTimeout(() => {
const modelInput = document.querySelector("[data-model-selector] input") as HTMLInputElement
modelInput?.focus()
}, 100)
},
})
commandRegistry.register({
id: "open-agent-selector",
label: "Open Agent Selector",
description: "Choose a different agent",
category: "Agent & Model",
keywords: ["agent", "mode"],
shortcut: { key: "A", meta: true, shift: true },
action: () => {
const agentTrigger = document.querySelector("[data-agent-selector]") as HTMLElement
if (agentTrigger) {
agentTrigger.focus()
setTimeout(() => {
const event = new KeyboardEvent("keydown", {
key: "Enter",
code: "Enter",
keyCode: 13,
which: 13,
bubbles: true,
cancelable: true,
})
agentTrigger.dispatchEvent(event)
}, 50)
}
},
})
commandRegistry.register({
id: "init",
label: "Initialize AGENTS.md",
description: "Create or update AGENTS.md file",
category: "Agent & Model",
keywords: ["/init", "agents", "initialize"],
action: async () => {
const instance = activeInstance()
const sessionId = activeSessionIdForInstance()
if (!instance || !instance.client || !sessionId || sessionId === "logs") return
try {
await instance.client.session.init({ path: { id: sessionId } })
console.log("Initialized AGENTS.md")
} catch (error) {
console.error("Failed to initialize AGENTS.md:", error)
}
},
})
commandRegistry.register({
id: "clear-input",
label: "Clear Input",
description: "Clear the prompt textarea",
category: "Input & Focus",
keywords: ["clear", "reset"],
shortcut: { key: "K", meta: true },
action: () => {
const textarea = document.querySelector(".prompt-input") as HTMLTextAreaElement
if (textarea) textarea.value = ""
},
})
commandRegistry.register({
id: "thinking",
label: "Toggle Thinking Blocks",
description: "Show/hide AI thinking process",
category: "System",
keywords: ["/thinking", "toggle", "show", "hide"],
action: () => {
console.log("Toggle thinking blocks (not implemented)")
@@ -261,55 +481,12 @@ const App: Component = () => {
id: "help",
label: "Show Help",
description: "Display keyboard shortcuts and help",
category: "System",
keywords: ["/help", "shortcuts", "help"],
action: () => {
console.log("Show help modal (not implemented)")
},
})
commandRegistry.register({
id: "new-session",
label: "New Session",
description: "Create a new session",
shortcut: { key: "N", meta: true, shift: true },
action: async () => {
const instance = activeInstance()
if (!instance) return
await handleNewSession(instance.id)
},
})
commandRegistry.register({
id: "open-model-selector",
label: "Open Model Selector",
description: "Choose a different model",
shortcut: { key: "M", meta: true, shift: true },
action: () => {
const modelInput = document.querySelector("[data-model-selector] input") as HTMLInputElement
modelInput?.focus()
},
})
commandRegistry.register({
id: "focus-prompt",
label: "Focus Prompt Input",
description: "Jump to the message input",
shortcut: { key: "P", meta: true },
action: () => {
const textarea = document.querySelector(".prompt-input") as HTMLTextAreaElement
textarea?.focus()
},
})
commandRegistry.register({
id: "open-agent-selector",
label: "Open Agent Selector",
description: "Choose a different agent",
action: () => {
const agentButton = document.querySelector("[data-agent-selector]") as HTMLElement
agentButton?.click()
},
})
}
function handleExecuteCommand(commandId: string) {
@@ -388,10 +565,31 @@ const App: Component = () => {
textarea?.focus()
},
)
registerAgentShortcuts(handleCycleAgent, handleCycleAgentReverse, () => {
const modelInput = document.querySelector("[data-model-selector] input") as HTMLInputElement
modelInput?.focus()
})
registerAgentShortcuts(
handleCycleAgent,
handleCycleAgentReverse,
() => {
const modelInput = document.querySelector("[data-model-selector] input") as HTMLInputElement
modelInput?.focus()
},
() => {
const agentTrigger = document.querySelector("[data-agent-selector]") as HTMLElement
if (agentTrigger) {
agentTrigger.focus()
setTimeout(() => {
const event = new KeyboardEvent("keydown", {
key: "Enter",
code: "Enter",
keyCode: 13,
which: 13,
bubbles: true,
cancelable: true,
})
agentTrigger.dispatchEvent(event)
}, 50)
}
},
)
registerEscapeShortcut(
() => {
const instance = activeInstance()
@@ -414,6 +612,16 @@ const App: Component = () => {
)
const handleKeyDown = (e: KeyboardEvent) => {
const target = e.target as HTMLElement
const isInCombobox = target.closest('[role="combobox"]') !== null
const isInListbox = target.closest('[role="listbox"]') !== null
const isInSelect = target.closest('[role="button"][data-agent-selector]') !== null
if (isInCombobox || isInListbox || isInSelect) {
return
}
const shortcut = keyboardRegistry.findMatch(e)
if (shortcut) {
e.preventDefault()

View File

@@ -102,11 +102,16 @@ export default function AgentSelector(props: AgentSelectorProps) {
</Select.Content>
</Select.Portal>
</Select>
<Show when={availableAgents().length > 1}>
<div class="flex items-center gap-1">
<Show when={availableAgents().length > 1}>
<span class="text-xs text-gray-400">
<Kbd>Tab</Kbd>
</span>
</Show>
<span class="text-xs text-gray-400">
<Kbd>Tab</Kbd>
<Kbd shortcut="cmd+shift+a" />
</span>
</Show>
</div>
</div>
)
}

View File

@@ -36,10 +36,39 @@ const CommandPalette: Component<CommandPaletteProps> = (props) => {
const labelMatch = cmd.label.toLowerCase().includes(q)
const descMatch = cmd.description.toLowerCase().includes(q)
const keywordMatch = cmd.keywords?.some((k) => k.toLowerCase().includes(q))
return labelMatch || descMatch || keywordMatch
const categoryMatch = cmd.category?.toLowerCase().includes(q)
return labelMatch || descMatch || keywordMatch || categoryMatch
})
}
const groupedCommands = () => {
const filtered = filteredCommands()
const groups = new Map<string, Command[]>()
for (const cmd of filtered) {
const category = cmd.category || "Other"
if (!groups.has(category)) {
groups.set(category, [])
}
groups.get(category)!.push(cmd)
}
const categoryOrder = ["Instance", "Session", "Agent & Model", "Input & Focus", "System", "Other"]
const sorted = new Map<string, Command[]>()
for (const cat of categoryOrder) {
if (groups.has(cat)) {
sorted.set(cat, groups.get(cat)!)
}
}
for (const [cat, cmds] of groups) {
if (!sorted.has(cat)) {
sorted.set(cat, cmds)
}
}
return sorted
}
createEffect(() => {
if (props.open) {
setQuery("")
@@ -123,27 +152,49 @@ const CommandPalette: Component<CommandPaletteProps> = (props) => {
when={filteredCommands().length > 0}
fallback={<div class="p-8 text-center text-gray-500">No commands found for "{query()}"</div>}
>
<For each={filteredCommands()}>
{(command, index) => (
<button
type="button"
onClick={() => handleCommandClick(command.id)}
class={`w-full px-4 py-3 flex items-start gap-3 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors cursor-pointer border-none text-left ${
index() === selectedIndex() ? "bg-blue-50 dark:bg-blue-900/20" : ""
}`}
onMouseEnter={() => setSelectedIndex(index())}
>
<div class="flex-1 min-w-0">
<div class="font-medium text-gray-900 dark:text-gray-100">{command.label}</div>
<div class="text-sm text-gray-600 dark:text-gray-400 mt-0.5">{command.description}</div>
</div>
<Show when={command.shortcut}>
<div class="mt-1">
<Kbd shortcut={buildShortcutString(command.shortcut)} />
<For each={Array.from(groupedCommands().entries())}>
{([category, commands]) => {
let globalIndex = 0
for (const [cat, cmds] of groupedCommands().entries()) {
if (cat === category) break
globalIndex += cmds.length
}
return (
<div class="py-2">
<div class="px-4 py-2 text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wide">
{category}
</div>
</Show>
</button>
)}
<For each={commands}>
{(command, localIndex) => {
const commandIndex = globalIndex + localIndex()
return (
<button
type="button"
onClick={() => handleCommandClick(command.id)}
class={`w-full px-4 py-3 flex items-start gap-3 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors cursor-pointer border-none text-left ${
commandIndex === selectedIndex() ? "bg-blue-50 dark:bg-blue-900/20" : ""
}`}
onMouseEnter={() => setSelectedIndex(commandIndex)}
>
<div class="flex-1 min-w-0">
<div class="font-medium text-gray-900 dark:text-gray-100">{command.label}</div>
<div class="text-sm text-gray-600 dark:text-gray-400 mt-0.5">
{command.description}
</div>
</div>
<Show when={command.shortcut}>
<div class="mt-1">
<Kbd shortcut={buildShortcutString(command.shortcut)} />
</div>
</Show>
</button>
)
}}
</For>
</div>
)
}}
</For>
</Show>
</div>

View File

@@ -3,6 +3,7 @@ import type { Message } from "../types/message"
import MessageItem from "./message-item"
import ToolCall from "./tool-call"
import { sseManager } from "../lib/sse-manager"
import Kbd from "./kbd"
interface MessageStreamProps {
instanceId: string
@@ -86,24 +87,31 @@ export default function MessageStream(props: MessageStreamProps) {
return (
<div class="message-stream-container">
<div class="connection-status">
<Show when={connectionStatus() === "connected"}>
<span class="status-indicator connected">
<span class="status-dot" />
Connected
</span>
</Show>
<Show when={connectionStatus() === "connecting"}>
<span class="status-indicator connecting">
<span class="status-dot" />
Connecting...
</span>
</Show>
<Show when={connectionStatus() === "error" || connectionStatus() === "disconnected"}>
<span class="status-indicator disconnected">
<span class="status-dot" />
Disconnected
</span>
</Show>
<div class="flex-1" />
<div class="flex items-center gap-2 text-sm font-medium text-gray-700">
<span>Command Palette</span>
<Kbd shortcut="cmd+shift+p" />
</div>
<div class="flex-1 flex items-center justify-end gap-3">
<Show when={connectionStatus() === "connected"}>
<span class="status-indicator connected">
<span class="status-dot" />
Connected
</span>
</Show>
<Show when={connectionStatus() === "connecting"}>
<span class="status-indicator connecting">
<span class="status-dot" />
Connecting...
</span>
</Show>
<Show when={connectionStatus() === "error" || connectionStatus() === "disconnected"}>
<span class="status-indicator disconnected">
<span class="status-dot" />
Disconnected
</span>
</Show>
</div>
</div>
<div ref={containerRef} class="message-stream" onScroll={handleScroll}>
<Show when={!props.loading && displayItems().length === 0}>

View File

@@ -18,7 +18,6 @@ interface FlatModel extends Model {
export default function ModelSelector(props: ModelSelectorProps) {
const instanceProviders = () => providers().get(props.instanceId) || []
let listboxRef!: HTMLUListElement
let inputRef!: HTMLInputElement
createEffect(() => {
@@ -69,7 +68,7 @@ export default function ModelSelector(props: ModelSelectorProps) {
itemComponent={(itemProps) => (
<Combobox.Item
item={itemProps.item}
class="px-3 py-2 cursor-pointer hover:bg-gray-100 rounded outline-none focus:bg-gray-100 flex items-start gap-2"
class="px-3 py-2 cursor-pointer hover:bg-gray-100 data-[highlighted]:bg-blue-100 rounded outline-none flex items-start gap-2"
>
<div class="flex flex-col flex-1 min-w-0">
<Combobox.ItemLabel class="font-medium text-sm text-gray-900">
@@ -106,7 +105,7 @@ export default function ModelSelector(props: ModelSelectorProps) {
<Combobox.Portal>
<Combobox.Content class="bg-white border border-gray-300 rounded-md shadow-lg max-h-80 overflow-hidden p-1 z-50 min-w-[300px]">
<Combobox.Listbox ref={listboxRef} scrollRef={() => listboxRef} class="max-h-80 overflow-auto" />
<Combobox.Listbox class="max-h-80 overflow-auto" />
</Combobox.Content>
</Combobox.Portal>
</Combobox>

View File

@@ -60,10 +60,12 @@ body {
.connection-status {
display: flex;
justify-content: flex-end;
justify-content: center;
align-items: center;
padding: 8px 16px;
background-color: var(--secondary-bg);
border-bottom: 1px solid var(--border-color);
gap: 16px;
}
.status-indicator {

View File

@@ -4,6 +4,7 @@ export function registerAgentShortcuts(
cycleAgent: () => void,
cycleAgentReverse: () => void,
focusModelSelector: () => void,
openAgentSelector: () => void,
) {
const isMac = () => navigator.platform.toLowerCase().includes("mac")
@@ -41,4 +42,13 @@ export function registerAgentShortcuts(
description: "focus model",
context: "global",
})
keyboardRegistry.register({
id: "open-agent-selector",
key: "A",
modifiers: { ctrl: !isMac(), meta: isMac(), shift: true },
handler: openAgentSelector,
description: "open agent",
context: "global",
})
}

View File

@@ -0,0 +1,178 @@
---
title: Command Palette ✅
description: Implement VSCode-style command palette with Cmd+Shift+P
status: COMPLETED
completed: 2024-10-23
---
# Implement Command Palette ✅
Built a VSCode-style command palette that opens as a centered modal dialog with 19 commands organized into 5 categories.
---
## ✅ Implementation Summary
### Commands Implemented (19 total)
#### **Instance (4 commands)**
1.**New Instance** (Cmd+N) - Open folder picker to create new instance
2.**Close Instance** (Cmd+W) - Stop current instance's server
3.**Next Instance** (Cmd+]) - Cycle to next instance tab
4.**Previous Instance** (Cmd+[) - Cycle to previous instance tab
#### **Session (7 commands)**
5.**New Session** (Cmd+Shift+N) - Create a new parent session
6.**Close Session** (Cmd+Shift+W) - Close current parent session
7.**Switch to Logs** (Cmd+Shift+L) - Jump to logs view
8.**Next Session** (Cmd+Shift+]) - Cycle to next session tab
9.**Previous Session** (Cmd+Shift+[) - Cycle to previous session tab
10.**Compact Session** - Summarize and compact current session (/compact API)
11.**Undo Last Message** - Revert the last message (/undo API)
#### **Agent & Model (5 commands)**
12.**Next Agent** (Tab) - Cycle to next agent
13.**Previous Agent** (Shift+Tab) - Cycle to previous agent
14.**Open Model Selector** (Cmd+Shift+M) - Choose a different model
15.**Open Agent Selector** - Choose a different agent
16.**Initialize AGENTS.md** - Create or update AGENTS.md file (/init API)
#### **Input & Focus (1 command)**
17.**Clear Input** (Cmd+K) - Clear the prompt textarea
#### **System (2 commands)**
18.**Toggle Thinking Blocks** - Show/hide AI thinking process (placeholder)
19.**Show Help** - Display keyboard shortcuts and help (placeholder)
---
## ✅ Features Implemented
### Visual Design
- ✅ Modal dialog centered on screen with backdrop overlay
- ✅ ~600px wide with auto height and max height
- ✅ Search/filter input at top
- ✅ Scrollable list of commands below
- ✅ Each command shows: name, description, keyboard shortcut (if any)
- ✅ Category headers for command grouping
- ✅ Dark/light mode support
### Behavior
- ✅ Opens on `Cmd+Shift+P`
- ✅ Closes on `Escape` or clicking outside
- ✅ Search input is auto-focused when opened
- ✅ Filter commands as user types (substring search by label, description, keywords, category)
- ✅ Arrow keys navigate through filtered list
- ✅ Enter executes selected command
- ✅ Mouse click on command also executes it
- ✅ Mouse hover updates selection
- ✅ Closes automatically after command execution
### Command Registry
- ✅ Centralized command registry in `lib/commands.ts`
- ✅ Commands organized by category
- ✅ Keywords for better search
- ✅ Keyboard shortcuts displayed
- ✅ All commands connected to existing actions
### Integration
- ✅ Integrated with keyboard registry
- ✅ Connected to instance/session management
- ✅ Connected to SDK client for API calls
- ✅ Connected to UI selectors (agent, model)
- ✅ State management via `stores/command-palette.ts`
---
## 📁 Files Modified
- `src/App.tsx` - Registered all 19 commands with categories
- `src/components/command-palette.tsx` - Added category grouping and display
- `src/lib/commands.ts` - Already existed with command registry
- `src/stores/command-palette.ts` - Already existed with state management
---
## ✅ Acceptance Criteria
- ✅ Palette opens with `Cmd+Shift+P`
- ✅ Search input is auto-focused
- ✅ 19 commands are listed in 5 categories
- ✅ Typing filters commands (case-insensitive substring match)
- ✅ Arrow keys navigate through list
- ✅ Enter executes selected command
- ✅ Click executes command
- ✅ Escape or click outside closes palette
- ✅ Palette closes after command execution
- ✅ Keyboard shortcuts display correctly
- ✅ Commands execute their intended actions:
-`/init` calls API
-`/compact` calls API
-`/undo` calls API
- ✅ New Session/Instance work
- ✅ Model/Agent selectors open
- ✅ Navigation shortcuts work
- ✅ Works in both light and dark mode
- ✅ Smooth open/close animations
---
## 🎯 Key Implementation Details
### Category Ordering
Commands are grouped and displayed in this order:
1. Instance - Managing workspace folders
2. Session - Managing conversation sessions
3. Agent & Model - AI configuration
4. Input & Focus - Input controls
5. System - System-level settings
### Search Functionality
Search filters by:
- Command label
- Command description
- Keywords
- Category name
### Keyboard Shortcuts
All shortcuts are registered in the keyboard registry and displayed in the palette using the `Kbd` component.
---
## 🚀 Future Enhancements
These can be added post-MVP:
- Fuzzy search algorithm (not just substring)
- Command history (recently used commands first)
- Custom user-defined commands
- Command arguments/parameters
- Command aliases
- Search by keyboard shortcut
- Quick switch between sessions/instances via command palette
- Command icons/emoji
- Command grouping within categories
---
## Notes
- Command palette provides VSCode-like discoverability
- All commands leverage existing keyboard shortcuts and actions
- Categories make it easy to find related commands
- Foundation is in place for adding more commands in the future
- Agent and Model selector commands work by programmatically clicking their triggers

View File

@@ -1,217 +0,0 @@
---
title: Command Palette
description: Implement VSCode-style command palette with Cmd+Shift+P
---
# Implement Command Palette
Build a VSCode-style command palette that opens as a centered modal dialog.
---
## Requirements
### Visual Design
- **Trigger**: Keyboard shortcut `Cmd+Shift+P` (Mac) or `Ctrl+Shift+P` (Windows/Linux)
- **Appearance**: Modal dialog centered on screen with backdrop overlay
- **Size**: ~600px wide, auto height with max height
- **Components**:
- Search/filter input at top
- Scrollable list of commands below
- Each command shows: name, description, keyboard shortcut (if any)
### Behavior
- Opens on `Cmd+Shift+P`
- Closes on `Escape` or clicking outside
- Search input is auto-focused when opened
- Filter commands as user types (fuzzy search preferred)
- Arrow keys navigate through filtered list
- Enter executes selected command
- Mouse click on command also executes it
- Closes automatically after command execution
---
## Commands to Include
### Essential Commands (MVP)
1. **Initialize AGENTS.md** (`/init`)
- Description: "Create or update AGENTS.md file"
- Action: Call `client.session.init()`
2. **Compact Session** (`/compact`)
- Description: "Summarize and compact the current session"
- Action: Call `client.session.summarize()`
3. **Undo Last Message** (`/undo`)
- Description: "Revert the last message"
- Action: Call `client.session.revert()`
4. **Toggle Thinking Blocks** (`/thinking`)
- Description: "Show/hide AI thinking process"
- Action: Toggle UI state (placeholder for now)
5. **Show Help** (`/help`)
- Description: "Display keyboard shortcuts and help"
- Action: Open help modal (placeholder for now)
### Navigation Commands (Trigger Existing Shortcuts)
6. **New Session**
- Description: "Create a new session"
- Shortcut: `Cmd+Shift+N`
- Action: Trigger existing `new-session` keyboard shortcut
7. **Open Model Selector**
- Description: "Choose a different model"
- Shortcut: `Cmd+P`
- Action: Focus model selector input
8. **Open Agent Selector**
- Description: "Choose a different agent"
- Action: Click agent selector to open dropdown
---
## Implementation Details
### File Structure
```
src/
components/
command-palette.tsx # Main command palette component
lib/
commands.ts # Command registry and definitions
stores/
command-palette.ts # State for showing/hiding palette
```
### Command Registry Structure
```typescript
interface Command {
id: string
label: string
description: string
keywords?: string[] // For fuzzy search
shortcut?: KeyboardShortcut
action: () => void | Promise<void>
category?: string // Group commands by category
}
```
### Integration Points
1. **Register global keyboard shortcut** in App.tsx:
```typescript
keyboardRegistry.register({
id: "command-palette",
key: "p",
modifiers: { meta: true, shift: true },
handler: () => setShowCommandPalette(true),
})
```
2. **Pass necessary props** to command palette:
- Current instance ID
- Current session ID
- SDK client reference
- Handler functions for UI actions
3. **Execute commands** based on type:
- API calls: Use SDK client
- UI actions: Call selector focus/click
- Shortcuts: Trigger registered keyboard shortcuts
---
## UI Component Details
### Layout
```
┌──────────────────────────────────────────────────────┐
│ Command Palette │
├──────────────────────────────────────────────────────┤
│ 🔍 Type a command or search... │
├──────────────────────────────────────────────────────┤
Initialize AGENTS.md │
│ Create or update AGENTS.md file │
│ │
│ Compact Session │
│ Summarize and compact the current session │
│ │
│ New Session ⌘⇧N │
│ Create a new session │
│ │
│ Open Model Selector ⌘P │
│ Choose a different model │
└──────────────────────────────────────────────────────┘
```
### Styling
- Use Kobalte Dialog for modal foundation
- Dark/light mode support matching app theme
- Highlight selected command with blue background
- Show keyboard shortcuts right-aligned in gray
- Smooth animations for open/close
### Keyboard Navigation
- `Cmd+Shift+P`: Open palette
- `Escape`: Close palette
- `ArrowUp`: Previous command
- `ArrowDown`: Next command
- `Enter`: Execute selected command
- Type to filter
---
## Acceptance Criteria
- [ ] Palette opens with `Cmd+Shift+P`
- [ ] Search input is auto-focused
- [ ] All 8 commands are listed
- [ ] Typing filters commands (case-insensitive substring match)
- [ ] Arrow keys navigate through list
- [ ] Enter executes selected command
- [ ] Click executes command
- [ ] Escape or click outside closes palette
- [ ] Palette closes after command execution
- [ ] Keyboard shortcuts display correctly (⌘⇧N, ⌘P, etc.)
- [ ] Commands execute their intended actions:
- `/init` calls API
- `/compact` calls API
- `/undo` calls API
- New Session creates a session
- Model/Agent selectors open
- [ ] Works in both light and dark mode
- [ ] Smooth open/close animations
---
## Future Enhancements (Post-MVP)
- Fuzzy search algorithm (not just substring)
- Command history (recently used commands first)
- Command categories/grouping
- Custom user-defined commands
- Command arguments/parameters
- Command aliases
- Search by keyboard shortcut
- Quick switch between sessions/instances
---
## Notes
- This replaces the slash command (`/command`) approach
- Command palette is more discoverable and flexible
- Provides a foundation for adding more commands in the future
- Similar to VSCode Cmd+Shift+P, Sublime Text Cmd+Shift+P, etc.