mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
184 lines
8.0 KiB
Markdown
184 lines
8.0 KiB
Markdown
# Command Palette Reference
|
|||
|
|
|
||
|
|
The global command palette enables keyboard-driven navigation across the entire RoboCo panel. Press **Cmd+K** (Mac) or **Ctrl+K** (Windows/Linux) from any page to open it, then fuzzy-search tasks, agents, projects, and navigation pages. When the input is empty, recently visited items appear instead.
|
||
|
|
|
||
|
|
## Usage
|
||
|
|
|
||
|
|
### Opening
|
||
|
|
|
||
|
|
- **Mac**: Cmd+K
|
||
|
|
- **Windows/Linux**: Ctrl+K
|
||
|
|
- Opens a Radix Dialog-based search overlay positioned at 20% from the top of the viewport
|
||
|
|
|
||
|
|
### Keyboard Navigation
|
||
|
|
|
||
|
|
| Key | Action |
|
||
|
|
|-----|--------|
|
||
|
|
| Arrow Down | Move selection down (wraps at end) |
|
||
|
|
| Arrow Up | Move selection up (wraps at start) |
|
||
|
|
| Enter | Navigate to the currently selected item |
|
||
|
|
| Escape | Close the palette (Radix Dialog built-in dismiss) |
|
||
|
|
|
||
|
|
### Search Categories
|
||
|
|
|
||
|
|
Results are grouped into four categories, appearing in this order:
|
||
|
|
|
||
|
|
1. **Tasks** — searched server-side by title, description, and short ID (via `tasksApi.list({q})`)
|
||
|
|
2. **Agents** — fuzzy-matched on name and slug (client-side)
|
||
|
|
3. **Projects** — fuzzy-matched on project name (client-side)
|
||
|
|
4. **Pages** — fuzzy-matched on page title from the sidebar navigation (client-side)
|
||
|
|
|
||
|
|
Each category shows up to 6 results. Enter navigates to the entity's detail page:
|
||
|
|
- Task: `/tasks/{id}`
|
||
|
|
- Agent: `/agents/{id}`
|
||
|
|
- Project: `/projects?q={name}` (filters the project list since no dedicated detail route exists)
|
||
|
|
- Page: the page's navigation href
|
||
|
|
|
||
|
|
### Recents
|
||
|
|
|
||
|
|
When the input is empty (before or after clearing a search), the palette shows **Recent** items — up to 10 entries pulled from localStorage under the key `roboco-cmd-recents`. These are ordered by most recent first.
|
||
|
|
|
||
|
|
**Recents are populated on navigation**: whenever you press Enter or click a result, that item is added to recents (or moved to the front if already there). The recents list is capped at 10 items; older entries are automatically dropped.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Architecture
|
||
|
|
|
||
|
|
### Component: `CommandPalette`
|
||
|
|
|
||
|
|
**File**: `panel/src/components/layout/command-palette.tsx`
|
||
|
|
|
||
|
|
A thin Radix Dialog renderer around the `useCommandPalette` hook. The component:
|
||
|
|
- Mounts once in the dashboard layout (`panel/src/app/(dashboard)/layout.tsx`) so the hotkey works on every page
|
||
|
|
- Listens for Cmd+K / Ctrl+K globally and opens the dialog
|
||
|
|
- Delegates all search/navigation logic to the hook
|
||
|
|
- Renders grouped results with icons, titles, and subtitles
|
||
|
|
- Handles arrow key and Enter navigation
|
||
|
|
|
||
|
|
**Props**: None — fully self-contained.
|
||
|
|
|
||
|
|
**Key Features**:
|
||
|
|
- Auto-focuses the input on open (via Radix's `onOpenAutoFocus`)
|
||
|
|
- Wrapping text (long titles are truncated with `text-truncate`)
|
||
|
|
- Shows "No results" when a search returns nothing; "No recent items yet" when recents are empty
|
||
|
|
- Icon for each result type (task = ListTodo, agent = Bot, project = FolderGit2, page = Compass)
|
||
|
|
|
||
|
|
### Hook: `useCommandPalette`
|
||
|
|
|
||
|
|
**File**: `panel/src/hooks/use-command-palette.ts`
|
||
|
|
|
||
|
|
Manages all data, keyboard navigation, and navigation state for the palette. Returns:
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
{
|
||
|
|
open: boolean;
|
||
|
|
setOpen: (next: boolean) => void;
|
||
|
|
query: string;
|
||
|
|
setQuery: (value: string) => void;
|
||
|
|
groups: CommandGroup[]; // Grouped results: Tasks, Agents, Projects, Pages
|
||
|
|
flatItems: CommandItem[]; // Flattened items for keyboard navigation
|
||
|
|
selectedIndex: number; // Currently highlighted result
|
||
|
|
moveSelection: (delta: number) => void; // +1/-1 for arrow keys
|
||
|
|
selectCurrent: () => void; // Navigate to flatItems[selectedIndex]
|
||
|
|
navigateTo: (item: CommandItem) => void; // Navigate + add to recents + close
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**Features**:
|
||
|
|
- **Dialog state driven by UI Store**: `useUIStore().commandPaletteOpen` and `.setCommandPaletteOpen()` manage open/close (so other components like header search can open it via the store)
|
||
|
|
- **Debounced search**: Typed input is debounced 150ms before triggering results (cheap debounce, not caching the query)
|
||
|
|
- **Server-side task search**: Tasks are fetched via `tasksApi.list({ q: trimmedQuery })` only while the dialog is open and the query is non-empty; client-side for agents/projects/pages
|
||
|
|
- **Recents on empty query**: `loadRecents()` is called fresh each time the query becomes empty, so a pick made just before closing appears immediately on the next open
|
||
|
|
- **Selection resets on new query**: Typing clears and resets `selectedIndex` to 0 (jump to top result)
|
||
|
|
- **Dialog close resets state**: Closing the dialog clears the query and selection so the next open starts fresh
|
||
|
|
- **Flat navigation index**: `flatItems` is a flat array across all groups; `selectedIndex` wraps around and addresses items by position, not group
|
||
|
|
|
||
|
|
**Export**: Exported as named export from `@/hooks/use-command-palette`, used by `CommandPalette` component.
|
||
|
|
|
||
|
|
### Helper: `fuzzyScore(query: string, label: string): number | null`
|
||
|
|
|
||
|
|
**File**: `panel/src/lib/fuzzy-match.ts`
|
||
|
|
|
||
|
|
Pure function that scores a label against a fuzzy query. Returns a score (lower is better) or `null` if the query doesn't match.
|
||
|
|
|
||
|
|
**Algorithm**: Subsequence matching — every character in the query must appear in the label in order (not necessarily contiguous). Score rewards contiguous matches and early matches; gaps are penalized.
|
||
|
|
|
||
|
|
**Usage**: Called by `scoreAndSort()` to rank agents, projects, and pages.
|
||
|
|
|
||
|
|
### Helper: `loadRecents() / addRecent(item)`
|
||
|
|
|
||
|
|
**File**: `panel/src/lib/command-palette-recents.ts`
|
||
|
|
|
||
|
|
localStorage-backed functions for managing recent items.
|
||
|
|
|
||
|
|
- `loadRecents()`: Returns the current recent items array (up to 10) from localStorage key `roboco-cmd-recents`, or `[]` if empty/missing
|
||
|
|
- `addRecent(item)`: Adds or moves `item` to the front of recents, caps the array at 10, and saves back to localStorage
|
||
|
|
|
||
|
|
**Item shape**: `{ type: CommandRecentType, id: string, title: string }`
|
||
|
|
|
||
|
|
**CommandRecentType**: Union type of `"task" | "agent" | "project" | "page"`
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Data Flow
|
||
|
|
|
||
|
|
```
|
||
|
|
1. User presses Cmd+K
|
||
|
|
↓
|
||
|
|
2. Global keydown listener in CommandPalette detects it
|
||
|
|
↓
|
||
|
|
3. setOpen(true) fires, opening the Radix Dialog
|
||
|
|
↓
|
||
|
|
4. User types in the input
|
||
|
|
↓
|
||
|
|
5. onChange → setQuery() → debounceMs 150ms → groups re-compute
|
||
|
|
↓
|
||
|
|
6. Groups fetch tasks (server-side if query non-empty, only while open)
|
||
|
|
↓
|
||
|
|
7. Agents/projects/pages are fuzzy-scored client-side
|
||
|
|
↓
|
||
|
|
8. Arrow keys move selectedIndex within flatItems
|
||
|
|
↓
|
||
|
|
9. Enter presses selectCurrent() → navigateTo(item)
|
||
|
|
↓
|
||
|
|
10. navigateTo() calls addRecent() then router.push()
|
||
|
|
↓
|
||
|
|
11. setDialogOpen(false) closes the palette and resets state
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Verification Against Live Data
|
||
|
|
|
||
|
|
All four search categories are backed by live API data or real navigation config:
|
||
|
|
|
||
|
|
- **Tasks**: Fetched from the backend via `tasksApi.list({ q, limit: 6 })`, which already fuzzy-searches by title/description/id-prefix
|
||
|
|
- **Agents**: Fetched from the backend via `useAgentDefinitions()`, which returns all agent definitions
|
||
|
|
- **Projects**: Fetched from the backend via `useProjects()`, which returns all projects
|
||
|
|
- **Pages**: Statically imported from `navItems` in `panel/src/components/layout/sidebar.tsx` — the source of truth for the sidebar navigation
|
||
|
|
|
||
|
|
This ensures the palette always searches against populated, real data. Mock or stub data is never used in place of live API calls.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Known Limitations & Future Improvements
|
||
|
|
|
||
|
|
1. **No dedicated project detail route**: Projects link to a filtered list view (`/projects?q={name}`) rather than a detail page, since no such route exists
|
||
|
|
2. **Maximum 6 results per group**: This is a hardcoded limit (`MAX_RESULTS_PER_GROUP`) to keep the results concise and readable
|
||
|
|
3. **localStorage persistence**: Recents are stored in the browser's localStorage, so they are device/profile-specific and not synced across sessions
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Testing
|
||
|
|
|
||
|
|
The command palette is tested in `panel/src/components/layout/__tests__/command-palette.test.tsx` and `panel/src/hooks/__tests__/use-command-palette.test.ts` with coverage for:
|
||
|
|
|
||
|
|
- Opening on Cmd+K / Ctrl+K
|
||
|
|
- Rendering grouped results with live data
|
||
|
|
- Keyboard navigation (arrow keys, Enter, Escape)
|
||
|
|
- Closing on Escape or navigation
|
||
|
|
- Empty-query recents display
|
||
|
|
- "No results" message on empty results
|
||
|
|
- Recents updates on navigation
|
||
|
|
- Result click handling
|