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:
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user