experiments in a post-browser web
10
fork

Configure Feed

Select the types of activity you want to include in your feed.

docs: research integrated command suggestions flow

+663
+663
notes/research-command-suggestions.md
··· 1 + # Command Suggestions: Integrated Search-and-Act Flow for Cmd 2 + 3 + **Status:** Research complete, ready for implementation 4 + **Last Updated:** 2026-02-10 5 + **Purpose:** Design a generic command + search matcher pattern for the cmd bar, enabling flows like `edit vampire` -> pick item from search results -> act on it 6 + 7 + --- 8 + 9 + ## 1. Problem Statement 10 + 11 + Users want to type a command with a search term (e.g., `edit vampire`) and see matching items appear in the dropdown as they type. Currently, the `edit` command runs its search only on Enter, then dumps all results into output selection mode. The flow is: 12 + 13 + 1. Type `edit vampire`, press Enter 14 + 2. Command executes, fetches all matching notes 15 + 3. Panel enters output selection mode with results 16 + 4. User picks one, panel routes to editor 17 + 18 + The desired flow is: 19 + 20 + 1. Type `edit vampire` 21 + 2. **As the user types**, matching items stream into the dropdown below the input 22 + 3. Arrow down to browse results 23 + 4. Press Enter on a result to open it 24 + 25 + This should not be specific to `edit` or notes. It should be a generic pattern that any command can opt into: `tag <search>`, `open <search>`, `delete <search>`, `group add <search>`. 26 + 27 + --- 28 + 29 + ## 2. Current Cmd Architecture Analysis 30 + 31 + ### 2.1 File Layout 32 + 33 + | File | Role | 34 + |------|------| 35 + | `extensions/cmd/background.js` | Command registry (provider pattern), shortcut handling, caching | 36 + | `extensions/cmd/panel.js` | Panel UI, input handling, matching, execution, chain mode | 37 + | `extensions/cmd/panel.html` | Panel layout and CSS | 38 + | `extensions/cmd/commands.js` | Command loader, proxy command creation for cross-extension dispatch | 39 + | `extensions/cmd/commands/index.js` | Aggregates built-in commands (edit, note, url, history, lists) | 40 + | `extensions/cmd/commands/edit.js` | Edit command -- search notes, return items | 41 + | `extensions/cmd/commands/note.js` | Note command -- save items | 42 + | `extensions/cmd/config.js` | Extension ID, defaults, schemas | 43 + 44 + ### 2.2 Command Registration Flow 45 + 46 + Two paths for registering commands: 47 + 48 + **Path A -- Built-in commands** (same process as panel): 49 + - `commands/index.js` exports an array of command objects 50 + - `commands.js` loads them and dispatches `cmd-update-commands` event 51 + - `panel.js` receives event, stores commands in `state.commands` 52 + - Execute is called directly (same JS context) 53 + 54 + **Path B -- Extension commands** (cross-process via pubsub): 55 + - Extension calls `api.commands.register({ name, description, execute })` 56 + - Preload batches and publishes `cmd:register-batch` via IPC 57 + - `background.js` stores metadata in `commandRegistry` Map 58 + - When panel opens, it publishes `cmd:query-commands` 59 + - Background responds with `cmd:query-commands-response` 60 + - `commands.js` creates proxy commands that dispatch via `cmd:execute:{name}` pubsub 61 + - Results come back via `cmd:execute:{name}:result` 62 + 63 + ### 2.3 Current Matching Flow 64 + 65 + In `panel.js`, when the user types: 66 + 67 + 1. `input` event fires, `state.typed` is updated 68 + 2. `findMatchingCommands(typed)` is called -- this matches **command names only** 69 + 3. Matches are sorted by exact match priority, then adaptive score, then frecency 70 + 4. `updateCommandUI()` renders inline completion hint 71 + 5. `updateResultsUI()` renders dropdown (only if `state.showResults` is true, toggled by ArrowDown) 72 + 6. On Enter, the selected command is executed with a `context` object containing `{ typed, name, params, search }` 73 + 74 + ### 2.4 Current Output Selection Mode 75 + 76 + After a command returns `{ output: { data: [...], mimeType: 'item' } }`: 77 + 78 + 1. Panel enters `outputSelectionMode` 79 + 2. Items are rendered in the results dropdown 80 + 3. Arrow keys navigate, Enter/ArrowRight selects 81 + 4. For `mimeType: 'item'`, selection publishes `editor:open` with `itemId` 82 + 5. For other mimeTypes, selection enters chain mode 83 + 84 + ### 2.5 Execution Context 85 + 86 + Commands receive: 87 + ```javascript 88 + { 89 + typed: 'edit vampire', // Full typed string 90 + name: 'edit', // Command name 91 + params: ['vampire'], // Array of params after command name 92 + search: 'vampire', // Text after command name (joined) 93 + // Chain mode fields omitted for brevity 94 + } 95 + ``` 96 + 97 + ### 2.6 Key Observation: The Gap 98 + 99 + The current architecture has a hard boundary between **command matching** (which operates on command names) and **item searching** (which happens inside command execution). There is no mechanism for a command to provide search suggestions back to the panel in real-time as the user types. The edit command's `searchNotes()` only runs on Enter. 100 + 101 + --- 102 + 103 + ## 3. Existing Search Infrastructure 104 + 105 + ### 3.1 Datastore Query API 106 + 107 + The backend provides SQL-based search: 108 + 109 + ```javascript 110 + // Basic query with type filter 111 + api.datastore.queryItems({ type: 'text' }) 112 + 113 + // Query with search (LIKE %term% on content, title, domain) 114 + api.datastore.queryItems({ type: 'url', search: 'github' }) 115 + 116 + // Frecency-ranked URL search 117 + api.datastore.queryItemsByFrecency({ search: 'github', limit: 10 }) 118 + ``` 119 + 120 + The `search` parameter in `queryItems` does a SQL `LIKE %term%` match against `content`, `title`, and `domain` columns. This is a basic substring search -- no fuzzy matching, no ranking beyond frecency. 121 + 122 + ### 3.2 Current Search Usage in Commands 123 + 124 + - **`edit.js`**: Calls `queryItems({ type: 'text' })` then filters client-side with `content.toLowerCase().includes(query)` 125 + - **`history.js`**: Calls `queryItems({ type: 'url' })` then filters client-side on URL and metadata title 126 + - **`note.js`**: Calls `queryItems({ type: 'text' })` with `getItemTags()` per item (N+1 query pattern) 127 + - **`tags/background.js`**: Uses `getTagsByFrecency()` for tag search 128 + 129 + None use the built-in `search` parameter on `queryItems` -- they all fetch everything then filter client-side. This is an opportunity for optimization. 130 + 131 + ### 3.3 History Command: Dynamic Commands Pattern 132 + 133 + The history module has a notable pattern: it registers **each URL as a separate command**. This makes URLs searchable by the command matcher, but does not scale and conflates items with commands. 134 + 135 + --- 136 + 137 + ## 4. Design Decision: Streaming Suggestions vs Top-Level Filter 138 + 139 + ### Option A: Extensions Stream Suggestions 140 + 141 + Each extension publishes matching items for a query via pubsub. Cmd stitches results together from all responders. 142 + 143 + **Flow:** 144 + 1. User types `edit vam` 145 + 2. Panel detects `edit` is a known command, extracts query `vam` 146 + 3. Panel publishes `cmd:suggest:edit` with `{ query: 'vam' }` 147 + 4. Edit command's extension responds with `cmd:suggest:edit:response` containing matching items 148 + 5. Panel merges responses from all extensions and renders dropdown 149 + 150 + **Pros:** 151 + - Fully decoupled -- any extension can provide suggestions for any command 152 + - Multiple extensions can respond to the same query 153 + - Works across process boundaries (pubsub is already cross-process) 154 + 155 + **Cons:** 156 + - Complex coordination (multiple async responders, merging, deduplication) 157 + - Race conditions between slow and fast responders 158 + - UI jank from incremental rendering as responses arrive 159 + - Pubsub round-trips add latency (~5-20ms each in Electron IPC) 160 + - Overkill for the common case (one command, one data source) 161 + 162 + ### Option B: Command-Level Suggestion Provider 163 + 164 + Commands opt into providing suggestions via a `suggest(query)` function declared alongside `execute()`. The panel calls it directly (for built-in commands) or via a targeted pubsub call (for extension commands). 165 + 166 + **Flow:** 167 + 1. User types `edit vam` 168 + 2. Panel detects `edit` is a known command with `suggest` capability 169 + 3. Panel calls `command.suggest('vam')` (or publishes targeted request) 170 + 4. Command returns an array of suggestion items 171 + 5. Panel renders them in the dropdown 172 + 173 + **Pros:** 174 + - Simple protocol: one command, one response 175 + - No coordination complexity 176 + - Direct function call for built-in commands (zero latency) 177 + - Easy to reason about per-command behavior 178 + - Natural extension of existing command object shape 179 + 180 + **Cons:** 181 + - Only the owning command provides suggestions (by design -- this is actually a feature) 182 + - Extension commands need a pubsub round-trip (but only one, with a single responder) 183 + 184 + ### Recommendation: Option B (Command-Level Suggestion Provider) 185 + 186 + Option B is the clear winner. It adds a single optional method to the command protocol, keeps the panel logic simple, and maps directly to the user's mental model: "I'm running the `edit` command, show me what `edit` can find." 187 + 188 + The multi-extension streaming model (Option A) would only matter if we wanted `edit vampire` to simultaneously search notes, URLs, AND tags. That is a different feature (unified search / omnibox), and should be built as its own command (e.g., `find`) rather than complicating every command's suggestion path. 189 + 190 + --- 191 + 192 + ## 5. Suggestion Protocol Design 193 + 194 + ### 5.1 Command Object Extension 195 + 196 + Add an optional `suggest` method to the command interface: 197 + 198 + ```javascript 199 + { 200 + name: 'edit', 201 + description: 'Edit a note', 202 + produces: ['item'], 203 + 204 + // NEW: Optional suggestion provider 205 + // Called as user types after the command name 206 + // Returns array of suggestion items for the dropdown 207 + suggest: async (query, options) => { 208 + // query: string -- the text after the command name 209 + // options: { limit: 10 } -- hints from the panel 210 + // Returns: Array of { id, title, subtitle, icon?, data? } 211 + return [ 212 + { id: 'abc-123', title: 'Vampire Notes', subtitle: 'text -- 2 hours ago', data: { itemId: 'abc-123' } }, 213 + { id: 'def-456', title: 'Vampire Weekend Review', subtitle: 'url -- yesterday' } 214 + ]; 215 + }, 216 + 217 + // Existing execute method 218 + execute: async (ctx) => { ... } 219 + } 220 + ``` 221 + 222 + ### 5.2 Suggestion Item Shape 223 + 224 + ```javascript 225 + { 226 + id: string, // Unique identifier (for dedup and selection tracking) 227 + title: string, // Primary display text (required) 228 + subtitle: string, // Secondary text (type, date, tags, etc.) 229 + icon: string, // Optional icon identifier or emoji 230 + data: object, // Opaque payload passed to execute() on selection 231 + // The panel does NOT interpret `data` -- it passes it through 232 + } 233 + ``` 234 + 235 + ### 5.3 Panel Behavior When Suggestions Are Available 236 + 237 + The panel needs to change its behavior when a typed command has a `suggest` method: 238 + 239 + **Current flow:** Type command -> ArrowDown shows command list -> Enter executes 240 + 241 + **New flow:** Type command + space + query -> suggestions appear automatically -> ArrowDown/Enter to pick 242 + 243 + Specifically: 244 + 245 + 1. User types `edit ` (command name + space) 246 + 2. Panel recognizes `edit` as matched command 247 + 3. Panel detects `edit` has `suggest` method 248 + 4. Panel enters **suggestion mode** for this command 249 + 5. As user continues typing (`edit vam`), panel calls `suggest('vam')` with debounce 250 + 6. Results render in the dropdown (replacing command matches) 251 + 7. ArrowDown/Up navigate suggestions 252 + 8. Enter on a suggestion calls `execute()` with the suggestion's `data` in context 253 + 9. Escape exits suggestion mode (back to command matching) 254 + 10. Clearing back past the space exits suggestion mode 255 + 256 + ### 5.4 Execution Context with Suggestion Selection 257 + 258 + When a user selects a suggestion, the execute context gets an extra field: 259 + 260 + ```javascript 261 + { 262 + typed: 'edit vampire', 263 + name: 'edit', 264 + params: ['vampire'], 265 + search: 'vampire', 266 + 267 + // NEW: populated when user selected a suggestion 268 + selectedSuggestion: { 269 + id: 'abc-123', 270 + title: 'Vampire Notes', 271 + data: { itemId: 'abc-123' } 272 + } 273 + } 274 + ``` 275 + 276 + The command's `execute()` can check for `ctx.selectedSuggestion` and skip its own search, directly acting on the selected item: 277 + 278 + ```javascript 279 + execute: async (ctx) => { 280 + // Fast path: user already picked a specific item 281 + if (ctx.selectedSuggestion?.data?.itemId) { 282 + return { 283 + output: { 284 + data: { id: ctx.selectedSuggestion.data.itemId }, 285 + mimeType: 'item' 286 + } 287 + }; 288 + } 289 + 290 + // Slow path: execute search (for Enter without selecting) 291 + const notes = await searchNotes(ctx.search); 292 + // ... existing logic 293 + } 294 + ``` 295 + 296 + ### 5.5 Cross-Process Suggestion Protocol (Extension Commands) 297 + 298 + For commands registered by other extensions (which execute via pubsub proxy): 299 + 300 + 1. Panel publishes `cmd:suggest:{commandName}` with `{ query, limit }` 301 + 2. Extension's preload handler calls the local `suggest()` function 302 + 3. Result published back via `cmd:suggest:{commandName}:result` 303 + 4. Panel renders suggestions 304 + 305 + This requires extending `preload.js` to store `suggest` handlers alongside `execute` handlers, and creating the pubsub wiring similar to how `cmd:execute:{name}` works today. 306 + 307 + ### 5.6 Debouncing and Cancellation 308 + 309 + - **Debounce:** 150ms after typing stops (fast enough to feel instant, slow enough to avoid thrashing) 310 + - **Cancellation:** If user types more before suggestions return, discard stale results (use a generation counter or AbortController) 311 + - **Minimum query length:** 1 character (commands can enforce their own minimum internally) 312 + - **Maximum results:** Panel requests `limit: 10` by default, commands can return fewer 313 + 314 + --- 315 + 316 + ## 6. UI Flow Mockup 317 + 318 + ### 6.1 Initial State -- Command Matching (unchanged) 319 + 320 + ``` 321 + ┌──────────────────────────────────────┐ 322 + │ edi │ <- User types "edi" 323 + │ edit (grey completion) │ <- Inline suggestion 324 + └──────────────────────────────────────┘ 325 + ``` 326 + 327 + User presses ArrowDown to see commands: 328 + 329 + ``` 330 + ┌──────────────────────────────────────┐ 331 + │ edi │ 332 + ├──────────────────────────────────────┤ 333 + │ ▸ edit Edit a note → item│ 334 + │ open editor Open markdown editor │ 335 + └──────────────────────────────────────┘ 336 + ``` 337 + 338 + ### 6.2 Entering Suggestion Mode 339 + 340 + User presses Tab (autocompletes to `edit `) or types `edit `: 341 + 342 + ``` 343 + ┌──────────────────────────────────────┐ 344 + │ edit │ <- Command matched, space typed 345 + ├──────────────────────────────────────┤ 346 + │ (type to search...) │ <- Placeholder prompt 347 + └──────────────────────────────────────┘ 348 + ``` 349 + 350 + User types `vam`: 351 + 352 + ``` 353 + ┌──────────────────────────────────────┐ 354 + │ edit vam │ 355 + ├──────────────────────────────────────┤ 356 + │ ▸ Vampire Notes text 2h ago│ 357 + │ Vampire Weekend Review url 1d ago│ 358 + │ Interview with a Vampire text 3d ago│ 359 + └──────────────────────────────────────┘ 360 + ``` 361 + 362 + ### 6.3 Selection and Action 363 + 364 + User arrows down to "Vampire Weekend Review" and presses Enter: 365 + 366 + ``` 367 + ┌──────────────────────────────────────┐ 368 + │ edit vam │ 369 + ├──────────────────────────────────────┤ 370 + │ Vampire Notes text 2h ago│ 371 + │ ▸ Vampire Weekend Review url 1d ago│ <- Selected 372 + │ Interview with a Vampire text 3d ago│ 373 + └──────────────────────────────────────┘ 374 + ``` 375 + 376 + Panel calls `edit.execute()` with `selectedSuggestion` -> routes to editor -> panel closes. 377 + 378 + ### 6.4 Enter Without Selection 379 + 380 + If user presses Enter without arrowing down (no suggestion selected), the command executes with `ctx.search = 'vam'` and no `selectedSuggestion`. The command does its own search and returns results via the existing output selection mode. 381 + 382 + This preserves backward compatibility -- suggestions are an acceleration, not a requirement. 383 + 384 + ### 6.5 Empty Query (No Search Term) 385 + 386 + Some commands could provide "recent items" when the query is empty: 387 + 388 + ``` 389 + ┌──────────────────────────────────────┐ 390 + │ edit │ 391 + ├──────────────────────────────────────┤ 392 + │ ▸ Meeting Notes text 1h ago│ <- Recent items 393 + │ Shopping List text 3h ago│ 394 + │ Project Ideas text 1d ago│ 395 + └──────────────────────────────────────┘ 396 + ``` 397 + 398 + This is opt-in per command. `suggest('')` with empty string means "show recents." 399 + 400 + --- 401 + 402 + ## 7. Commands That Would Benefit 403 + 404 + | Command | Suggest Source | Action on Selection | 405 + |---------|---------------|-------------------| 406 + | `edit <query>` | `queryItems({ type: 'text', search })` | Open item in editor | 407 + | `open <query>` | `queryItemsByFrecency({ search })` | Open URL in window | 408 + | `tag <query>` | `getTagsByFrecency()` + filter | Apply tag to active window | 409 + | `delete <query>` | `queryItems({ search })` | Soft-delete item | 410 + | `open group <query>` | Groups list | Open group's URLs | 411 + | `history <query>` | `queryItemsByFrecency({ search })` | Open URL from history | 412 + | `note <query>` | `queryItems({ type: 'text', search })` | Open existing note in editor | 413 + 414 + The history command currently registers every URL as a separate command. With the suggestion protocol, it could be a single `history` command with `suggest()` that searches URLs -- much cleaner and more scalable. 415 + 416 + --- 417 + 418 + ## 8. Implementation Phases 419 + 420 + ### Phase 1: Core Suggestion Infrastructure (panel.js) 421 + 422 + **Goal:** Panel can call `suggest()` on built-in commands and render results in the dropdown. 423 + 424 + **Changes to `extensions/cmd/panel.js`:** 425 + - Add `suggestionMode` state: `{ active, commandName, query, results, selectedIndex, generation }` 426 + - In `input` event handler: detect when typed text matches a command + has a space + command has `suggest` 427 + - Call `command.suggest(query)` with debounce (150ms) 428 + - Render suggestion items in `#results` instead of command matches 429 + - Handle ArrowUp/Down for suggestion navigation 430 + - On Enter with selection: build context with `selectedSuggestion`, call execute 431 + - On Enter without selection: existing behavior (execute with `search`) 432 + - On Escape: exit suggestion mode 433 + - On Backspace past space: exit suggestion mode 434 + 435 + **Changes to `extensions/cmd/panel.html`:** 436 + - Add CSS for suggestion items (title + subtitle layout, type badge, timestamp) 437 + 438 + **Estimated effort:** 1-2 days 439 + 440 + ### Phase 2: Migrate `edit` Command 441 + 442 + **Goal:** `edit` command has a working `suggest()` method. 443 + 444 + **Changes to `extensions/cmd/commands/edit.js`:** 445 + - Add `suggest(query)` method that calls `queryItems({ type: 'text', search: query })` (use server-side search instead of client-side filter) 446 + - Return formatted suggestion items: `{ id, title: firstLine, subtitle: 'text -- relative time' }` 447 + - Modify `execute()` to check `ctx.selectedSuggestion` for fast-path routing 448 + 449 + **Estimated effort:** Half a day 450 + 451 + ### Phase 3: Cross-Process Suggestion Protocol (preload + background) 452 + 453 + **Goal:** Extension commands (registered via `api.commands.register`) can provide suggestions. 454 + 455 + **Changes to `preload.js`:** 456 + - Accept optional `suggest` in command registration 457 + - Store suggest handler alongside execute handler in `window._cmdHandlers` 458 + - Subscribe to `cmd:suggest:{name}` topic 459 + - On receive: call local suggest handler, publish result to `cmd:suggest:{name}:result` 460 + 461 + **Changes to `extensions/cmd/commands.js`:** 462 + - When creating proxy commands, check if source extension declared `suggest` capability 463 + - Add `suggest()` method on proxy that publishes `cmd:suggest:{name}` and awaits response 464 + 465 + **Changes to `extensions/cmd/background.js`:** 466 + - Store `hasSuggest` flag in command registry metadata 467 + - Include in `cmd:query-commands-response` 468 + 469 + **Estimated effort:** 1 day 470 + 471 + ### Phase 4: Migrate More Commands 472 + 473 + **Goal:** `history`, `open group`, `tag` commands gain `suggest()` methods. 474 + 475 + - **`history`**: Replace dynamic command registration with a single `suggest()` that calls `queryItemsByFrecency({ search })` 476 + - **`open group`**: `suggest()` searches group names via `getTagsByFrecency()` 477 + - **`tag`**: `suggest()` searches tag names for autocomplete 478 + 479 + **Estimated effort:** 1 day 480 + 481 + ### Phase 5: Polish and UX 482 + 483 + - Loading indicator while suggestions are fetching (subtle spinner after 200ms) 484 + - "No results" message when search returns empty 485 + - Keyboard hint in placeholder: "Type to search, Arrow to browse" 486 + - Suggestion result count indicator 487 + - Adaptive ranking: boost suggestions the user previously selected for a given query (reuse existing adaptive feedback mechanism) 488 + 489 + **Estimated effort:** 1 day 490 + 491 + --- 492 + 493 + ## 9. Key Files That Need Changes 494 + 495 + | File | Change Type | Description | 496 + |------|------------|-------------| 497 + | `extensions/cmd/panel.js` | Major | Add suggestion mode state machine, debounced suggest calls, suggestion rendering, keyboard navigation within suggestions | 498 + | `extensions/cmd/panel.html` | Minor | CSS for suggestion item layout (title/subtitle/badge/time) | 499 + | `extensions/cmd/commands.js` | Moderate | Proxy command `suggest()` via pubsub for extension commands | 500 + | `extensions/cmd/commands/edit.js` | Moderate | Add `suggest()` method, update `execute()` with selectedSuggestion fast-path | 501 + | `extensions/cmd/commands/history.js` | Moderate | Add `suggest()` method, potentially remove dynamic command registration | 502 + | `extensions/cmd/background.js` | Minor | Track `hasSuggest` in registry metadata | 503 + | `preload.js` | Moderate | Accept and wire `suggest` handler in command registration, pubsub plumbing | 504 + | `extensions/tags/background.js` | Minor | Add `suggest()` to tag command | 505 + | `extensions/groups/background.js` | Minor | Add `suggest()` to open group command | 506 + 507 + --- 508 + 509 + ## 10. State Machine: Suggestion Mode in panel.js 510 + 511 + The panel currently has these modes: 512 + - **Normal mode:** typing matches commands 513 + - **Output selection mode:** navigating array results from command execution 514 + - **Chain mode:** selecting next command after receiving typed output 515 + 516 + The new **suggestion mode** fits between normal mode and output selection mode: 517 + 518 + ``` 519 + Normal Mode 520 + 521 + ├─ (type command + space, command has suggest) 522 + 523 + Suggestion Mode ──── (Escape / Backspace past space) ──── Normal Mode 524 + 525 + ├─ (Enter with selection) → execute(ctx + selectedSuggestion) → close/chain 526 + ├─ (Enter without selection) → execute(ctx) → may enter Output Selection Mode 527 + 528 + 529 + Output Selection Mode (if command returns array) 530 + 531 + ├─ (select item) 532 + 533 + Chain Mode (if item has chainable mimeType) 534 + or 535 + Close Panel (if mimeType is 'item' → editor routing) 536 + ``` 537 + 538 + ### Suggestion Mode State 539 + 540 + ```javascript 541 + // Add to state object in panel.js 542 + suggestionMode: false, // Whether we're in suggestion mode 543 + suggestionCommand: null, // The command providing suggestions 544 + suggestionQuery: '', // Current query text 545 + suggestionResults: [], // Array of suggestion items 546 + suggestionIndex: -1, // Selected index (-1 = none) 547 + suggestionGeneration: 0, // Counter to discard stale results 548 + suggestionTimer: null, // Debounce timer 549 + ``` 550 + 551 + ### Entry/Exit Conditions 552 + 553 + **Enter suggestion mode when:** 554 + 1. Typed text matches exactly one command (or the top match) 555 + 2. There is a space after the command name 556 + 3. The matched command has a `suggest` method 557 + 558 + **Exit suggestion mode when:** 559 + 1. User presses Escape 560 + 2. User deletes back past the space (no longer has command + space pattern) 561 + 3. User clears input entirely 562 + 4. A suggestion is selected and executed (mode exits naturally) 563 + 564 + --- 565 + 566 + ## 11. Alternative Considered: Unified Omnibox 567 + 568 + An alternative to per-command suggestions is a unified "search everything" omnibox where typing any text simultaneously searches commands, items, URLs, tags, and groups: 569 + 570 + ``` 571 + ┌──────────────────────────────────────┐ 572 + │ vampire │ 573 + ├──────────────────────────────────────┤ 574 + │ Commands │ 575 + │ edit Edit a note │ 576 + │ Notes │ 577 + │ ▸ Vampire Notes 2h ago │ 578 + │ Interview with a Vampire 3d ago │ 579 + │ URLs │ 580 + │ vampirefreaks.com 1w ago │ 581 + │ Tags │ 582 + │ #vampire │ 583 + └──────────────────────────────────────┘ 584 + ``` 585 + 586 + This is a compelling UX but is architecturally different from the command suggestion pattern. It would require: 587 + - A central search coordinator that queries all data sources 588 + - Category grouping in the dropdown 589 + - Disambiguation of what "Enter" does on different result types 590 + - Priority ranking across heterogeneous result types 591 + 592 + **Recommendation:** Build command suggestions (this doc) first. The omnibox can be built later as a special `find` command that uses the same `suggest()` protocol, aggregating suggestions from multiple sources. 593 + 594 + --- 595 + 596 + ## 12. Performance Considerations 597 + 598 + ### Query Optimization 599 + 600 + The current `edit.js` fetches ALL text items then filters client-side. With the `suggest()` protocol, we should: 601 + 602 + 1. **Use server-side search:** `queryItems({ type: 'text', search: query })` instead of fetching all + filtering 603 + 2. **Limit results:** Always pass `limit: 10` to avoid transferring large result sets 604 + 3. **Index content column:** Consider adding SQLite FTS5 index for full-text search (future enhancement) 605 + 606 + ### Latency Budget 607 + 608 + Target: suggestions visible within 100ms of typing pause. 609 + 610 + - Debounce wait: 150ms 611 + - IPC round-trip (built-in command): ~0ms (same process) 612 + - IPC round-trip (extension command): ~10-20ms (pubsub via main process) 613 + - SQLite query: ~1-5ms for LIKE search on <10K items 614 + - **Total (built-in):** ~155ms -- meets target 615 + - **Total (extension):** ~175ms -- meets target 616 + 617 + For larger datasets (>10K items), FTS5 indexing would keep query time <5ms. 618 + 619 + ### Caching 620 + 621 + Suggestions are ephemeral and short-lived. No persistent caching needed. In-memory cache with 5s TTL could help if user is rapidly editing the same query (backspace + retype), but this is premature optimization. 622 + 623 + --- 624 + 625 + ## 13. Relationship to Existing Research 626 + 627 + ### Search Extension (notes/research-search-extension.md) 628 + 629 + The search extension research designs web search suggestions (Google, DuckDuckGo, etc.) in the cmd bar. The suggestion protocol designed here is complementary: 630 + 631 + - Search extension commands (`google`, `ddg`) would use `suggest()` to fetch web search suggestions from engine APIs 632 + - The same dropdown rendering, debounce, and selection logic applies 633 + - The search extension is a **consumer** of the suggestion protocol, not the foundation of it 634 + 635 + ### Entity Recognition (notes/research-entity-recognition.md) 636 + 637 + Entity recognition could enrich suggestion results with extracted metadata (people, places, dates). But it is orthogonal to the suggestion protocol itself. 638 + 639 + --- 640 + 641 + ## 14. Summary 642 + 643 + ### What to build 644 + 645 + A `suggest(query)` optional method on commands, called by the panel as the user types after a command name, with results rendered in the dropdown for immediate selection. 646 + 647 + ### Why this approach 648 + 649 + - Minimal protocol addition (one optional method) 650 + - Zero breaking changes (commands without `suggest` work exactly as before) 651 + - Natural extension of existing command shape 652 + - Works for both built-in and extension commands 653 + - Provides the desired `edit vampire` flow as the first use case 654 + - Generalizes cleanly to `open`, `tag`, `delete`, `history`, `group` commands 655 + - Leaves room for a unified omnibox as a future `find` command 656 + 657 + ### Key architectural decisions 658 + 659 + 1. **Command-level, not global:** Each command owns its suggestions. No central search coordinator. 660 + 2. **Optional opt-in:** Commands without `suggest()` are unaffected. 661 + 3. **Selection passthrough:** Selected suggestion's `data` is passed to `execute()` via `ctx.selectedSuggestion`, enabling fast-path execution. 662 + 4. **Enter without selection still works:** Falls back to existing execute-then-select flow. 663 + 5. **Debounced, not streamed:** Panel calls `suggest()` after 150ms typing pause, not on every keystroke.