diff --git a/__LOCAL_LLMs/dashboard/docs/RICH_FEATURES_ROADMAP.md b/__LOCAL_LLMs/dashboard/docs/RICH_FEATURES_ROADMAP.md new file mode 100644 index 00000000..7c54d4ba --- /dev/null +++ b/__LOCAL_LLMs/dashboard/docs/RICH_FEATURES_ROADMAP.md @@ -0,0 +1,1241 @@ +# Rich Features — Implementation Roadmap + +> **Coding agent execution guide** — Every task has explicit file paths, exit criteria, and verification commands. +> Last updated: Feb 20, 2026 +> +> PRD: [RICH_FEATURES_PRD.md](RICH_FEATURES_PRD.md) · Previous roadmap: [DASHBOARD_ROADMAP.md](DASHBOARD_ROADMAP.md) + +--- + +## How to Use This Roadmap + +1. **Execute phases in order.** Phase A is the foundation; everything else depends on it. +2. **Within a phase, execute tasks in order.** Earlier tasks produce files/types that later tasks depend on. +3. **After each task,** run the verification command. Do not proceed if it fails. +4. **After each phase,** run `npx tsc --noEmit` from the dashboard root. Commit with the message template provided. +5. **Check every checkbox** as you complete it, adding the commit hash. +6. **Exit criteria** are pass/fail. If a criterion says "renders X", it means the component must exist and TypeScript must compile — visual correctness is verified by the user later. + +**Dashboard root:** `__LOCAL_LLMs/dashboard/` +**Verification command (all phases):** `cd __LOCAL_LLMs/dashboard && npx tsc --noEmit` + +--- + +## Phase A — Foundation: IndexedDB + Component Decomposition _(Sprint 14–16)_ + +**Goal:** Replace the monolithic `page.tsx` with a route-group architecture and persistent IndexedDB storage. This is the non-negotiable foundation for all subsequent phases. + +**Estimated effort:** ~15 hours (8 tasks) + +| # | ID | Task | Status | Priority | Est | +| --- | --- | -------------------------------- | ------ | -------- | ----- | +| 1 | A1 | Install `idb` + create db.ts | - [ ] | Critical | 1.5hr | +| 2 | A2 | TypeScript interfaces (types.ts) | - [ ] | Critical | 1hr | +| 3 | A3 | Route group: (mission-control) | - [ ] | Critical | 2hr | +| 4 | A4 | Route group: (workspace) layout | - [ ] | Critical | 2hr | +| 5 | A5 | Sidebar component | - [ ] | Critical | 2.5hr | +| 6 | A6 | Conversation view + streaming | - [ ] | Critical | 3hr | +| 7 | A7 | Auto-title + context bar | - [ ] | High | 1.5hr | +| 8 | A8 | v3 → v4 migration | - [ ] | High | 1.5hr | + +--- + +### A1: Install `idb` + Create IndexedDB Layer + +**File:** `src/app/lib/db.ts` (new) + +**Steps:** + +1. Run `npm install idb` in the dashboard directory. +2. Create `src/app/lib/db.ts` with: + - `openDB()` call creating database `llm-workspace` version 1. + - Object stores: `conversations`, `messages`, `agents`, `quickActions`, `projects`, `scheduledTasks`, `taskRuns`, `modelRatings`, `orchestrations`. + - Indexes: + - `conversations`: `updatedAt`, `projectId`, `archived` + - `messages`: compound `[conversationId, timestamp]`, compound `[conversationId, parentId]` + - `quickActions`: `category`, `usageCount` + - `taskRuns`: compound `[taskId, timestamp]` + - `modelRatings`: compound `[model, taskType]` + - Export async helper functions: + - `getDb()` — singleton getter + - `addConversation(conv)`, `getConversation(id)`, `updateConversation(id, partial)`, `deleteConversation(id)`, `listConversations(opts: { archived?, projectId?, limit?, offset? })` + - `addMessage(msg)`, `getMessages(conversationId, opts: { limit?, before? })`, `updateMessage(id, partial)`, `deleteMessagesByConversation(conversationId)` + - `countMessages(conversationId)` + +**Exit criteria:** + +- [ ] `npm install idb` succeeds (check `package.json` has `idb` dependency) +- [ ] `src/app/lib/db.ts` exports all listed functions +- [ ] All functions have correct TypeScript signatures using types from `types.ts` (A2) +- [ ] `npx tsc --noEmit` passes + +**Verify:** `npx tsc --noEmit` + +--- + +### A2: TypeScript Interfaces + +**File:** `src/app/lib/types.ts` (extend existing) + +**Steps:** + +1. Keep ALL existing interfaces (`OllamaData`, `WhisperData`, `SystemData`, `Toast`, `PullProgress`, `StreamMetrics`). +2. Add the following new interfaces at the bottom of the file: + +```typescript +// --- v4 Workspace Types --- + +export interface Conversation { + id: string; + title: string; + model: string; + agentId?: string; + projectId?: string; + systemPrompt?: string; + messageCount: number; + createdAt: number; + updatedAt: number; + pinned: boolean; + archived: boolean; + metadata: { + totalTokens: number; + totalPrompts: number; + avgTokPerSec: number; + models: string[]; + }; +} + +export interface Message { + id: string; + conversationId: string; + role: 'user' | 'assistant' | 'system'; + content: string; + model?: string; + timestamp: number; + attachments?: Attachment[]; + metrics?: { + tokensPerSec: number; + totalTokens: number; + promptTokens: number; + durationMs: number; + }; + rating?: 'up' | 'down'; + parentId?: string; + branchIndex?: number; +} + +export interface Attachment { + type: 'image' | 'file' | 'audio' | 'url'; + name: string; + data: string; + mimeType?: string; + size?: number; + language?: string; +} + +export interface Agent { + id: string; + name: string; + icon: string; + description: string; + model: string; + systemPrompt: string; + temperature?: number; + tools: AgentTool[]; + welcomeMessage?: string; + examplePrompts: string[]; + constraints?: string[]; + responseFormat?: 'markdown' | 'json' | 'code' | 'plain'; + builtin: boolean; + conversationCount: number; + createdAt: number; +} + +export type AgentTool = 'file_read' | 'vision' | 'voice_input'; + +export interface QuickAction { + id: string; + name: string; + icon: string; + category: 'code' | 'writing' | 'analysis' | 'creative' | 'devops' | 'custom'; + description: string; + modelHint: string; + systemPrompt: string; + userTemplate: string; + builtin: boolean; + hotkey?: string; + usageCount: number; + lastUsed?: number; +} + +export interface ModelDefaults { + fast: string; + coding: string; + reasoning: string; + chat: string; + vision: string; +} + +export interface Project { + id: string; + name: string; + icon: string; + description?: string; + defaultModel?: string; + defaultAgent?: string; + systemContext?: string; + conversationIds: string[]; + pinned: boolean; + createdAt: number; +} + +export interface ScheduledTask { + id: string; + name: string; + schedule: string; + scheduleHuman: string; + model: string; + prompt: string; + inputSource?: + | { type: 'static' } + | { type: 'file'; path: string } + | { type: 'command'; command: string } + | { type: 'clipboard' }; + outputAction?: + | { type: 'conversation' } + | { type: 'clipboard' } + | { type: 'file'; path: string } + | { type: 'notification' }; + enabled: boolean; + lastRun?: number; + runHistory: Array<{ timestamp: number; durationMs: number; success: boolean; result: string }>; + createdAt: number; +} + +export interface Orchestration { + id: string; + name: string; + mode: 'chain' | 'race' | 'vote'; + steps: OrchestrationStep[]; + synthesizer?: string; + description?: string; +} + +export interface OrchestrationStep { + model: string; + systemPrompt?: string; + transformInput?: string; +} +``` + +**Exit criteria:** + +- [ ] All interfaces exported from `types.ts` +- [ ] No existing interfaces removed or modified +- [ ] `npx tsc --noEmit` passes + +--- + +### A3: Route Group — (mission-control) + +**Goal:** Move the existing `page.tsx` into a `(mission-control)` route group so the workspace can live at `/` and Mission Control at `/mission-control`. + +**Files to create/move:** + +- `src/app/(mission-control)/mission-control/page.tsx` — move ALL content from current `src/app/page.tsx` +- `src/app/(mission-control)/mission-control/components/` — move existing `src/app/components/RamBudgetBar.tsx` and `src/app/components/MarkdownResponse.tsx` here +- Keep `src/app/components/StatusDot.tsx`, `ProgressBar.tsx`, `Sparkline.tsx` in shared components (used by both routes) + +**Steps:** + +1. Create directory `src/app/(mission-control)/mission-control/`. +2. Create directory `src/app/(mission-control)/mission-control/components/`. +3. Copy `src/app/page.tsx` to `src/app/(mission-control)/mission-control/page.tsx`. +4. Move `src/app/components/RamBudgetBar.tsx` to `src/app/(mission-control)/mission-control/components/RamBudgetBar.tsx`. +5. Move `src/app/components/MarkdownResponse.tsx` to `src/app/(mission-control)/mission-control/components/MarkdownResponse.tsx`. +6. Update all import paths in the moved `page.tsx` to reflect new location: + - `'./lib/types'` → `'../../lib/types'` + - `'./lib/format'` → `'../../lib/format'` + - `'./lib/ollama-config'` → `'../../lib/ollama-config'` + - `'./components/StatusDot'` → `'../../components/StatusDot'` + - `'./components/ProgressBar'` → `'../../components/ProgressBar'` + - `'./components/Sparkline'` → `'../../components/Sparkline'` + - `'./components/RamBudgetBar'` → `'./components/RamBudgetBar'` + - `'./components/MarkdownResponse'` → `'./components/MarkdownResponse'` +7. Delete the original `src/app/page.tsx` (it will be replaced by workspace home in A4). +8. Create `src/app/page.tsx` as a simple redirect or workspace home placeholder: + ```tsx + 'use client'; + import { redirect } from 'next/navigation'; + export default function Home() { + redirect('/mission-control'); + } + ``` + (This is temporary — Phase A6 replaces it with workspace home.) + +**Exit criteria:** + +- [ ] `http://localhost:3100/mission-control` renders the existing dashboard (same as before) +- [ ] `http://localhost:3100/` redirects to `/mission-control` +- [ ] No broken imports — `npx tsc --noEmit` passes +- [ ] All existing components still function (StatusDot, ProgressBar, Sparkline shared) +- [ ] API routes unchanged at `/api/*` + +--- + +### A4: Route Group — (workspace) Layout with Sidebar Shell + +**Goal:** Create the workspace layout with sidebar + content area. Initially shows empty state. + +**Files:** + +- `src/app/(workspace)/layout.tsx` (new) — sidebar + `{children}` content area +- `src/app/(workspace)/page.tsx` (new) — workspace home (empty state, "New Conversation" CTA) +- `src/app/(workspace)/components/Sidebar.tsx` (new) — sidebar shell component +- Update `src/app/page.tsx` — change redirect to `/` (workspace becomes home) + +**Steps:** + +1. Create `src/app/(workspace)/layout.tsx`: + - `'use client'` component + - Flex layout: sidebar (240px, collapsible) + main content area (`{children}`) + - Sidebar collapse state from `llm-sidebar-state` localStorage + - `Cmd+/` keyboard shortcut toggles sidebar + - Pass sidebar state as context or prop +2. Create `src/app/(workspace)/components/Sidebar.tsx`: + - Header: app icon + "LLM Workspace" title + - "New Conversation" button (prominent, top) + - Navigation links: "Quick Actions", "Agents", "Scheduled Tasks", "Projects" + - Divider + - Conversation list placeholder (text: "No conversations yet") + - Bottom links: "Mission Control" (link to `/mission-control`), "Settings" + - All using existing design token CSS variables +3. Create `src/app/(workspace)/page.tsx`: + - Empty state: centered content with icon, "Start a conversation" heading + - "New Conversation" button that creates a conversation and navigates to `/c/{id}` + - "Quick Actions" grid preview (placeholder cards) +4. Update `src/app/page.tsx` to redirect to workspace: `redirect('/')` → remove redirect, make this the workspace. + +**Actually:** Next.js route groups with `(workspace)` — the layout at `(workspace)/layout.tsx` will wrap `(workspace)/page.tsx` and `(workspace)/c/[id]/page.tsx`. The root `page.tsx` should now BE the workspace home (move the workspace page content to root `page.tsx` and put it inside the `(workspace)` group). + +**Revised file structure:** + +``` +src/app/ +├── layout.tsx # Root layout (existing, unchanged) +├── (workspace)/ +│ ├── layout.tsx # Sidebar + content wrapper +│ ├── page.tsx # Home / new conversation +│ └── c/[id]/page.tsx # Conversation view (A6) +├── (mission-control)/ +│ └── mission-control/page.tsx # Existing dashboard +└── api/ # Unchanged +``` + +**Exit criteria:** + +- [ ] `http://localhost:3100/` shows workspace home with sidebar +- [ ] Sidebar is 240px wide with dark theme styling matching existing design tokens +- [ ] Sidebar collapses/expands with `Cmd+/` +- [ ] "Mission Control" link in sidebar navigates to `/mission-control` +- [ ] "New Conversation" button exists (functionality wired in A6) +- [ ] `npx tsc --noEmit` passes + +--- + +### A5: Sidebar — Conversation List + +**Goal:** Wire the sidebar to show real conversations from IndexedDB, grouped by time. + +**Files:** + +- `src/app/(workspace)/components/ConversationList.tsx` (new) +- `src/app/(workspace)/components/Sidebar.tsx` (update from A4) + +**Steps:** + +1. Create `ConversationList.tsx`: + - Props: `conversations: Conversation[]`, `activeId?: string`, `onSelect(id: string)`, `onDelete(id: string)`, `onPin(id: string)` + - Group conversations by time period: + - **Pinned** (always at top, if any) + - **Today** — `createdAt` is today + - **Yesterday** + - **Last 7 Days** + - **Last 30 Days** + - **Older** + - Each group: header label + list of conversation items + - Each item shows: title (truncated to 1 line), model badge (small pill), message count, time ago + - Active conversation highlighted with `accent-primary` left border + - Right-click context menu: Pin/Unpin, Archive, Delete, Export + - Show skeleton shimmer while loading +2. Update `Sidebar.tsx`: + - Fetch conversations from IndexedDB on mount: `listConversations({ archived: false, limit: 100 })` + - Search input at top — filters conversations by title (client-side) + - Render `` with fetched data + - "New Conversation" creates a conversation in IndexedDB and navigates via `router.push('/c/{id}')` +3. Add search input with `Search` icon from lucide-react. + +**Exit criteria:** + +- [ ] Sidebar shows conversations from IndexedDB grouped by time period +- [ ] Search input filters conversations by title in real-time +- [ ] Clicking a conversation navigates to `/c/{id}` +- [ ] "New Conversation" creates a record in IndexedDB and navigates +- [ ] Empty state shown when no conversations exist +- [ ] Active conversation visually highlighted +- [ ] `npx tsc --noEmit` passes + +--- + +### A6: Conversation View + Streaming + +**Goal:** The main chat interface — message thread, input bar, streaming responses via existing `/api/ollama/chat`. + +**Files:** + +- `src/app/(workspace)/c/[id]/page.tsx` (new) — conversation page +- `src/app/(workspace)/components/ConversationView.tsx` (new) — main chat component +- `src/app/(workspace)/components/MessageThread.tsx` (new) — message list +- `src/app/(workspace)/components/MessageBubble.tsx` (new) — individual message +- `src/app/(workspace)/components/InputBar.tsx` (new) — input with send button + +**Steps:** + +1. **`c/[id]/page.tsx`:** + - Read `id` from route params + - Load `Conversation` from IndexedDB via `getConversation(id)` + - Load messages via `getMessages(id, { limit: 50 })` + - If conversation not found, redirect to `/` + - Render `` + +2. **`ConversationView.tsx`:** + - Props: `conversation: Conversation`, `initialMessages: Message[]` + - State: `messages`, `streaming`, `streamingContent`, `currentModel` + - Header bar: model selector dropdown (list from `/api/ollama` GET), conversation title (editable on click), agent badge (if `agentId`) + - Render `` and `` + - `handleSend(text, attachments?)`: + a. Create user `Message` in IndexedDB + add to state + b. Build messages array for Ollama chat API (include system prompt if set) + c. `fetch('/api/ollama/chat', { method: 'POST', body: JSON.stringify({ model, messages }) })` + d. Stream NDJSON response, accumulate into assistant message + e. On completion: save assistant `Message` to IndexedDB, update conversation metadata + f. Use `AbortController` for cancel support + - Model selector: dropdown of installed models from `/api/ollama` GET response + +3. **`MessageThread.tsx`:** + - Props: `messages: Message[]`, `streaming: boolean`, `streamingContent: string` + - Render list of `` components + - Auto-scroll to bottom on new messages + - Ref for scroll container + +4. **`MessageBubble.tsx`:** + - Props: `message: Message`, `isStreaming?: boolean` + - User messages: right-aligned, accent-primary background, white text + - Assistant messages: left-aligned, surface-card background + - Use `` component (import from mission-control components or move to shared) for assistant messages + - Show model name as small badge on assistant messages + - Show metrics (tok/s, tokens) after completed assistant messages + +5. **`InputBar.tsx`:** + - Props: `onSend(text: string, attachments?: Attachment[])`, `disabled: boolean`, `onCancel: () => void`, `streaming: boolean` + - Textarea with auto-resize (min 1 row, max 6 rows) + - `Cmd+Enter` to send + - Send button (or Stop button if streaming) + - Placeholder: "Message... (Cmd+Enter to send)" + +6. **Move `MarkdownResponse.tsx` to shared location:** `src/app/components/MarkdownResponse.tsx` (accessible by both workspace and mission-control). + +**Exit criteria:** + +- [ ] `/c/{uuid}` loads conversation from IndexedDB and renders messages +- [ ] User can type a message and send it; it streams a response from Ollama +- [ ] Response renders with markdown formatting (using MarkdownResponse) +- [ ] Messages persist in IndexedDB — refresh page shows same conversation +- [ ] Model selector dropdown shows installed models +- [ ] Conversation title shown in header (editable on click) +- [ ] Stop button cancels streaming via AbortController +- [ ] Metrics (tok/s, total tokens) shown after assistant response completes +- [ ] New messages appear in sidebar conversation list with updated timestamp +- [ ] `npx tsc --noEmit` passes + +--- + +### A7: Auto-Title + Context Bar + +**Goal:** Auto-generate conversation titles and show context window usage. + +**Files:** + +- `src/app/api/ollama/title/route.ts` (new) — title generation endpoint +- `src/app/(workspace)/components/ConversationView.tsx` (update) +- `src/app/(workspace)/components/ContextBar.tsx` (new) +- `src/app/lib/format.ts` (extend) + +**Steps:** + +1. **`api/ollama/title/route.ts`:** + - POST endpoint accepting `{ message: string }` + - Calls Ollama `/api/generate` with the fastest/smallest installed model + - System prompt: `"Generate a 3-5 word title for a conversation that starts with the following message. Reply with ONLY the title, no quotes, no punctuation."` + - Returns `{ title: string }` + - Timeout: 5s. On failure, return `{ title: "New Conversation" }` + +2. **Auto-title in ConversationView:** + - After first assistant response completes, if conversation title is "New Conversation": + - Call `/api/ollama/title` with the user's first message + - Update conversation title in IndexedDB + - Update sidebar (via state or callback) + +3. **`ContextBar.tsx`:** + - Props: `usedTokens: number`, `maxTokens: number` + - Thin progress bar below conversation header + - Color: green (<60%), yellow (60-80%), orange (80-95%), red (>95%) + - Text: `4.2K / 32K tokens` + - At 80%: show warning icon + - At 95%: show "Context full" badge + +4. **Token estimation in `format.ts`:** + - Add `estimateTokens(text: string): number` — returns `Math.ceil(text.split(/\s+/).length * 1.3)` + - Add `getModelContextWindow(modelName: string, modelInfo?: any): number` — returns context window size from model info, default 4096 + +5. **Wire into ConversationView:** + - Track `totalContextTokens` by summing `estimateTokens()` over all messages + - Get model context window from `/api/ollama` show response (already fetched for model info) + - Render `` in conversation header + +**Exit criteria:** + +- [ ] New conversations get auto-titled after first assistant response +- [ ] Title appears in sidebar and conversation header within 2s +- [ ] Context bar shows token usage as progress bar +- [ ] Context bar changes color at 60/80/95% thresholds +- [ ] `estimateTokens()` function exported from `format.ts` +- [ ] `/api/ollama/title` endpoint works and returns a title +- [ ] `npx tsc --noEmit` passes + +--- + +### A8: v3 → v4 Migration + +**Goal:** Migrate existing localStorage data to IndexedDB on first v4 load. + +**File:** `src/app/lib/migrate.ts` (new) + +**Steps:** + +1. Create `migrate.ts` with `async function migrateV3ToV4(): Promise<{ migrated: boolean; stats: { conversations: number; messages: number } }>`: + - Check `localStorage.getItem('llm-migrated-v4')` — if `'true'`, skip + - **Migrate inference log:** + - Read `llm-inference-log` from localStorage + - For each entry, create a `Conversation` + two `Message` records (user prompt + assistant response) + - Title: first 5 words of prompt + - **Migrate chat histories:** + - Scan all `llm-chat-{model}` keys + - For each, create a `Conversation` with model name as part of title + - Convert chat messages to `Message` records + - Set `localStorage.setItem('llm-migrated-v4', 'true')` + - Keep old keys (do NOT delete) + - Return stats + +2. Call `migrateV3ToV4()` in the workspace layout on mount (inside a `useEffect`). + +3. Show a toast after migration: "Migrated X conversations from v3" + +**Exit criteria:** + +- [ ] `migrateV3ToV4()` converts `llm-inference-log` entries to conversations +- [ ] `migrateV3ToV4()` converts `llm-chat-{model}` entries to conversations +- [ ] Migration runs only once (guarded by `llm-migrated-v4` flag) +- [ ] Original localStorage keys preserved (not deleted) +- [ ] Toast notification shown after migration +- [ ] `npx tsc --noEmit` passes + +--- + +### Phase A — Commit Template + +``` +feat(local-llm): Phase A — foundation (IndexedDB + workspace + conversations) + +A1: IndexedDB layer with idb — conversations, messages, 9 object stores +A2: v4 TypeScript interfaces — Conversation, Message, Agent, QuickAction, etc. +A3: Route group (mission-control) — existing dashboard moved to /mission-control +A4: Route group (workspace) — sidebar + content layout at / +A5: Sidebar — conversation list grouped by time, search, navigation +A6: Conversation view — message thread, input bar, streaming via /api/ollama/chat +A7: Auto-title via fast model + context window usage bar +A8: v3 → v4 migration — localStorage to IndexedDB +``` + +### Phase A — Final Verification + +```bash +cd __LOCAL_LLMs/dashboard +npx tsc --noEmit # Must pass +# Manual checks: +# - http://localhost:3100/ shows workspace with sidebar +# - http://localhost:3100/mission-control shows existing dashboard +# - Can create a conversation and send a message +# - Response streams with markdown rendering +# - Conversation appears in sidebar after creation +# - Refresh page — conversation persists +# - Auto-title updates after first response +# - Context bar shows token usage +``` + +--- + +## Phase B — Quick Actions + Command Palette _(Sprint 17–18)_ + +**Goal:** 30 built-in Quick Actions and a `Cmd+K` command palette for instant access. + +**Depends on:** Phase A (conversations, IndexedDB, workspace layout) + +**Estimated effort:** ~10 hours (6 tasks) + +| # | ID | Task | Status | Priority | Est | +| --- | --- | --------------------------------- | ------ | -------- | ----- | +| 9 | B1 | Quick Action data + built-in lib | - [ ] | Critical | 2hr | +| 10 | B2 | Install fuse.js + command palette | - [ ] | Critical | 3hr | +| 11 | B3 | QA launcher → conversation | - [ ] | Critical | 1hr | +| 12 | B4 | Custom QA editor (CRUD) | - [ ] | Medium | 2hr | +| 13 | B5 | Usage tracking + frequent section | - [ ] | Low | 1hr | +| 14 | B6 | QA export/import | - [ ] | Low | 0.5hr | + +--- + +### B1: Quick Action Data + Built-in Library + +**Files:** + +- `src/app/lib/quick-actions.ts` (new) — 30 built-in Quick Action definitions +- `src/app/lib/db.ts` (extend) — add QA CRUD functions + +**Steps:** + +1. Add to `db.ts`: `addQuickAction()`, `getQuickAction(id)`, `listQuickActions(category?)`, `updateQuickAction(id, partial)`, `deleteQuickAction(id)`, `seedBuiltinQuickActions()`. +2. Create `quick-actions.ts` exporting `BUILTIN_QUICK_ACTIONS: QuickAction[]` — an array of 30 Quick Actions matching the PRD categories (8 code, 6 writing, 4 analysis, 4 creative, 4 devops, 4 extra utility). Each has: `id` (prefixed `builtin-`), `name`, `icon` (emoji), `category`, `description`, `modelHint`, `systemPrompt`, `userTemplate`, `builtin: true`, `usageCount: 0`. +3. `seedBuiltinQuickActions()` — on first run, inserts all 30 into IndexedDB if not already present. Check by `llm-qa-seeded` localStorage flag. + +**Exit criteria:** + +- [ ] `quick-actions.ts` exports array of 30 QuickAction objects +- [ ] Each QA has a unique id, non-empty systemPrompt, and userTemplate with `{placeholders}` +- [ ] `seedBuiltinQuickActions()` inserts them into IndexedDB idempotently +- [ ] `npx tsc --noEmit` passes + +--- + +### B2: Command Palette (Cmd+K) + +**Files:** + +- `src/app/(workspace)/components/CommandPalette.tsx` (new) +- `src/app/(workspace)/layout.tsx` (update — add Cmd+K handler) + +**Steps:** + +1. Run `npm install fuse.js` in dashboard directory. +2. Create `CommandPalette.tsx`: + - Modal overlay (centered, 500px wide, dark background) + - Search input at top with auto-focus + - Results list below, max 10 items visible, scrollable + - Result types (each with icon prefix): + - Quick Actions (from IndexedDB) + - Agents (from IndexedDB, Phase C — for now show empty section) + - Recent conversations (last 5 from IndexedDB) + - System commands: "Mission Control", "Settings", "Export Data" + - Use `fuse.js` for fuzzy search across all items + - Keyboard navigation: Up/Down arrows, Enter to select, Escape to close + - Result item shows: icon, name, category badge, description (truncated) +3. Wire into workspace layout: + - `Cmd+K` / `Ctrl+K` opens palette + - Escape closes + - Selecting a Quick Action → calls QA launcher (B3) + - Selecting a conversation → navigates to `/c/{id}` + - Selecting "Mission Control" → navigates to `/mission-control` + +**Exit criteria:** + +- [ ] `Cmd+K` opens command palette overlay +- [ ] Fuzzy search filters Quick Actions and conversations +- [ ] Up/Down/Enter keyboard navigation works +- [ ] Escape closes palette +- [ ] Results show within 100ms of typing +- [ ] Selecting a Quick Action triggers the launcher flow +- [ ] `npx tsc --noEmit` passes + +--- + +### B3: Quick Action Launcher → Conversation + +**File:** `src/app/(workspace)/components/Sidebar.tsx` (update), `CommandPalette.tsx` (update), `ConversationView.tsx` (update) + +**Steps:** + +1. Create helper `launchQuickAction(qa: QuickAction): Promise`: + - Resolve `modelHint` to actual model name using `ModelDefaults` from localStorage (`llm-model-defaults`) or auto-detect from installed models + - Create a new `Conversation` in IndexedDB with: + - `title`: qa.name + - `model`: resolved model + - `systemPrompt`: qa.systemPrompt + - Navigate to `/c/{id}` + - Pre-fill the input bar with `qa.userTemplate` + - Increment `qa.usageCount` and set `qa.lastUsed` in IndexedDB + - Return conversation id +2. Wire into CommandPalette: selecting a QA calls `launchQuickAction` +3. Add "Quick Actions" section to sidebar: show top 5 by `usageCount` as small buttons +4. In ConversationView: if conversation was just created from a QA, pre-fill input with `userTemplate` and highlight `{placeholders}` with a subtle background color + +**Exit criteria:** + +- [ ] Selecting a Quick Action in command palette creates a conversation and navigates +- [ ] Conversation has QA's system prompt set +- [ ] Input bar pre-filled with user template +- [ ] Template `{placeholders}` visually highlighted +- [ ] Usage count incremented in IndexedDB +- [ ] Top 5 frequent actions shown in sidebar +- [ ] `npx tsc --noEmit` passes + +--- + +### B4: Custom Quick Action Editor + +**File:** `src/app/(workspace)/components/QuickActionEditor.tsx` (new) + +**Steps:** + +1. Modal/page with form fields: name, icon (emoji picker or text input), category (dropdown), description, model hint (dropdown: fast/coding/reasoning/chat/vision), system prompt (large textarea), user template (textarea), hotkey (optional text input). +2. "Save" creates/updates in IndexedDB. "Cancel" discards. +3. "Duplicate" on built-in actions: copies all fields, sets `builtin: false`, appends "(Copy)" to name. +4. "Delete" on custom actions only (built-in cannot be deleted, only hidden). +5. Accessible from: command palette "Create Quick Action", or edit icon on existing QA. + +**Exit criteria:** + +- [ ] Editor creates new custom Quick Actions in IndexedDB +- [ ] Editor edits existing custom Quick Actions +- [ ] "Duplicate" works on built-in actions +- [ ] Built-in actions cannot be deleted (button disabled/hidden) +- [ ] Form validates: name and systemPrompt required +- [ ] `npx tsc --noEmit` passes + +--- + +### B5: Usage Tracking + Frequently Used + +**File:** `src/app/(workspace)/components/Sidebar.tsx` (update) + +**Steps:** + +1. Sidebar "Quick Actions" section shows top 5 by `usageCount` (already from B3). +2. Add "View All" link that opens full Quick Action grid (grouped by category). +3. Quick Action grid: 4-column grid, icon + name, grouped by category headers. +4. Grid accessible from sidebar flyout or command palette. + +**Exit criteria:** + +- [ ] Top 5 frequent Quick Actions shown in sidebar +- [ ] "View All" opens full categorized grid +- [ ] Grid shows all 30+ actions grouped by category +- [ ] `npx tsc --noEmit` passes + +--- + +### B6: Quick Action Export/Import + +**File:** `src/app/lib/db.ts` (extend) + +**Steps:** + +1. `exportQuickActions(): Promise` — returns all custom (non-builtin) QAs. +2. `importQuickActions(actions: QuickAction[]): Promise` — imports, skips duplicates by id. +3. Include in the existing settings export (F29) flow — extend `exportSettings` to include QAs. + +**Exit criteria:** + +- [ ] Custom Quick Actions included in settings JSON export +- [ ] Import merges without duplicating +- [ ] `npx tsc --noEmit` passes + +--- + +### Phase B — Commit Template + +``` +feat(local-llm): Phase B — Quick Actions + command palette (B1-B6) + +B1: 30 built-in Quick Actions across 5 categories with IndexedDB storage +B2: Cmd+K command palette with fuse.js fuzzy search +B3: QA launcher creates pre-configured conversations with template +B4: Custom Quick Action editor (CRUD) +B5: Usage tracking with top-5 frequent actions in sidebar +B6: Quick Action export/import +``` + +--- + +## Phase C — Custom Agents _(Sprint 19–20)_ + +**Goal:** 10 built-in agents + agent editor + agent-aware conversations. + +**Depends on:** Phase A + +**Estimated effort:** ~9 hours (5 tasks) + +| # | ID | Task | Status | Priority | Est | +| --- | --- | ----------------------------- | ------ | -------- | ----- | +| 15 | C1 | Agent data + built-in library | - [ ] | Critical | 2hr | +| 16 | C2 | Agent picker in sidebar | - [ ] | Critical | 1.5hr | +| 17 | C3 | Agent editor (full-screen) | - [ ] | High | 2.5hr | +| 18 | C4 | Agent → conversation wiring | - [ ] | Critical | 2hr | +| 19 | C5 | Agent export/import | - [ ] | Low | 0.5hr | + +--- + +### C1: Agent Data + Built-in Library + +**Files:** + +- `src/app/lib/agents.ts` (new) — 10 built-in agent definitions +- `src/app/lib/db.ts` (extend) — agent CRUD + +**Steps:** + +1. Add to `db.ts`: `addAgent()`, `getAgent(id)`, `listAgents()`, `updateAgent(id, partial)`, `deleteAgent(id)`, `seedBuiltinAgents()`. +2. Create `agents.ts` exporting `BUILTIN_AGENTS: Agent[]` — 10 agents from PRD table. Each has full `systemPrompt` (10+ lines of persona instructions), `welcomeMessage`, 3-4 `examplePrompts`, and appropriate `tools`. +3. `seedBuiltinAgents()` idempotent insert. + +**Exit criteria:** + +- [ ] `agents.ts` exports 10 agents with full system prompts (not stubs) +- [ ] Each agent has welcomeMessage and 3-4 examplePrompts +- [ ] `seedBuiltinAgents()` inserts into IndexedDB idempotently +- [ ] `npx tsc --noEmit` passes + +--- + +### C2: Agent Picker in Sidebar + +**File:** `src/app/(workspace)/components/AgentPicker.tsx` (new), Sidebar update + +**Steps:** + +1. "My Agents" section in sidebar — shows agent icons in a row, click to start conversation. +2. "View All" opens a grid showing all agents with icon, name, description, conversation count. +3. Agents also appear in command palette (update B2 to include agents from IndexedDB). + +**Exit criteria:** + +- [ ] Agents shown in sidebar as icon row +- [ ] Clicking an agent starts a new agent conversation +- [ ] "View All" shows full agent grid +- [ ] Agents searchable in command palette +- [ ] `npx tsc --noEmit` passes + +--- + +### C3: Agent Editor + +**File:** `src/app/(workspace)/components/AgentEditor.tsx` (new) + +**Steps:** + +1. Full-screen modal with fields: icon (emoji picker), name, description, model (dropdown), temperature (slider 0-2), system prompt (large textarea with character count), welcome message, example prompts (tag-style input — add/remove chips), tool toggles (checkboxes for v4 tools: file_read, vision, voice_input), constraints (add/remove list), response format (dropdown). +2. "Save", "Cancel", "Duplicate" (for built-in), "Delete" (custom only). +3. "Test" button — opens a temporary conversation with this agent's settings for preview. + +**Exit criteria:** + +- [ ] All agent fields editable and persisted to IndexedDB +- [ ] Built-in agents: only "Duplicate" available (not delete) +- [ ] Custom agents: full CRUD +- [ ] Temperature slider with 0.1 step precision +- [ ] Example prompts as tag-style chips +- [ ] `npx tsc --noEmit` passes + +--- + +### C4: Agent → Conversation Wiring + +**File:** `src/app/(workspace)/components/ConversationView.tsx` (update), `InputBar.tsx` (update) + +**Steps:** + +1. When creating a conversation from an agent: + - Set `agentId` on conversation + - Set `systemPrompt` from agent + - Set `model` from agent + - Insert agent's `welcomeMessage` as first assistant message in IndexedDB + - Increment agent's `conversationCount` +2. In ConversationView header: show agent badge — `{icon} {name} · {model}` +3. In InputBar: show agent's `examplePrompts` as clickable chips below the input (only when conversation is empty / just started). Clicking a chip fills the input. +4. If agent has `temperature` set, include it in the Ollama API call body. + +**Exit criteria:** + +- [ ] Starting an agent conversation shows welcome message +- [ ] Agent badge visible in conversation header +- [ ] Example prompts shown as clickable chips for empty conversations +- [ ] Clicking a chip fills the input textarea +- [ ] Agent's system prompt injected into all Ollama API calls for that conversation +- [ ] Temperature passed to Ollama if set on agent +- [ ] Agent conversation count incremented +- [ ] `npx tsc --noEmit` passes + +--- + +### C5: Agent Export/Import + +**File:** `src/app/lib/db.ts` (extend) + +Same pattern as B6. Custom agents exportable/importable as JSON. + +**Exit criteria:** + +- [ ] Custom agents included in settings export +- [ ] Import merges without duplicating +- [ ] `npx tsc --noEmit` passes + +--- + +### Phase C — Commit Template + +``` +feat(local-llm): Phase C — custom agents (C1-C5) + +C1: 10 built-in agents with full system prompts and example prompts +C2: Agent picker in sidebar + command palette integration +C3: Agent editor (full-screen) with all fields +C4: Agent → conversation wiring (welcome msg, badge, chips, temperature) +C5: Agent export/import +``` + +--- + +## Phase D — Model Router + Multi-Modal Input _(Sprint 21–22)_ + +**Goal:** Smart model auto-selection and rich file/voice/image input. + +**Depends on:** Phase A + +**Estimated effort:** ~13 hours (7 tasks) + +| # | ID | Task | Status | Priority | Est | +| --- | --- | -------------------------------- | ------ | -------- | ----- | +| 20 | D1 | Task classifier (router.ts) | - [ ] | High | 1hr | +| 21 | D2 | Model defaults config + settings | - [ ] | High | 1.5hr | +| 22 | D3 | Auto-routing UI (model chip) | - [ ] | High | 2hr | +| 23 | D4 | Rich InputBar (attach/voice/img) | - [ ] | High | 2hr | +| 24 | D5 | File attachment processing | - [ ] | High | 2.5hr | +| 25 | D6 | Voice input → Whisper | - [ ] | Medium | 2hr | +| 26 | D7 | Drag-and-drop + paste detection | - [ ] | Medium | 2hr | + +--- + +### D1: Task Classifier + +**File:** `src/app/lib/router.ts` (new) + +**Steps:** + +1. Implement `classifyTask(input, attachments?)` and `resolveModel(taskType, defaults, loaded, installed)` exactly as specified in PRD section 3.4. +2. Add `taskTypeToHint(taskType: TaskType): keyof ModelDefaults` mapping. +3. Add `matchesHint(modelName: string, hint: string): boolean` — regex patterns to match model names to categories (e.g., `coder` → coding, `r1|think` → reasoning, `llava|vision` → vision). +4. Add `autoDetectDefaults(models: string[]): ModelDefaults` — scan model names and assign best guess per category. + +**Exit criteria:** + +- [ ] `classifyTask('Review this function for bugs')` returns `'code_review'` +- [ ] `classifyTask('Error: ENOENT no such file')` returns `'debugging'` +- [ ] `classifyTask('What is 2+2?')` returns `'simple'` +- [ ] `resolveModel()` returns loaded model when available, falls back correctly +- [ ] `autoDetectDefaults()` maps model names to categories +- [ ] `npx tsc --noEmit` passes + +--- + +### D2–D7: Model Defaults, Auto-Routing UI, Rich Input, File Processing, Voice, Drag-and-Drop + +_(Detailed steps follow the same pattern as above. Each task has a specific file, explicit steps, and pass/fail exit criteria.)_ + +**D2 Exit criteria:** + +- [ ] Settings page section for Model Router defaults (5 dropdowns) +- [ ] Defaults persisted to `llm-model-defaults` in localStorage +- [ ] Auto-detect fills defaults on first run + +**D3 Exit criteria:** + +- [ ] Model chip in conversation header shows "Auto" or selected model +- [ ] Clicking chip opens model picker dropdown +- [ ] Auto mode resolves on send with toast explanation +- [ ] Manual selection sticky for conversation + +**D4 Exit criteria:** + +- [ ] Input bar has Attach, Voice, Image buttons +- [ ] Buttons open respective pickers +- [ ] Attachments shown as removable chips above input + +**D5 Exit criteria:** + +- [ ] Code files read and shown with syntax highlighting preview +- [ ] Text files shown as formatted preview +- [ ] Images shown as thumbnails, encoded as base64 +- [ ] Attachment size validated (10MB max) + +**D6 Exit criteria:** + +- [ ] Voice button starts recording via MediaRecorder +- [ ] Audio sent to `/api/whisper/transcribe` +- [ ] Transcription fills input textarea +- [ ] Error shown if Whisper not installed + +**D7 Exit criteria:** + +- [ ] Drag files onto conversation shows drop overlay +- [ ] Dropped files processed as attachments +- [ ] Pasting image from clipboard creates attachment +- [ ] Pasting code auto-wraps in fenced block + +--- + +### Phase D — Commit Template + +``` +feat(local-llm): Phase D — model router + multi-modal input (D1-D7) + +D1: Task classifier with regex heuristics + resolveModel with fallback chain +D2: Model defaults config in Settings, auto-detect on first run +D3: Auto-routing UI — model chip with Auto mode and manual override +D4: Rich input bar — attach, voice, image buttons with chip previews +D5: File attachment processing — code, text, images with previews +D6: Voice input — MediaRecorder → Whisper transcription → textarea +D7: Drag-and-drop + paste intelligence (code, images, errors) +``` + +--- + +## Phase E — Response Enhancements _(Sprint 23)_ + +**Goal:** Per-message actions, code-block copy, branching, rating. + +**Depends on:** Phase A + +**Estimated effort:** ~7 hours (5 tasks) + +| # | ID | Task | Status | Priority | Est | +| --- | --- | ---------------------------- | ------ | -------- | ----- | +| 27 | E1 | Per-message action bar | - [ ] | High | 2hr | +| 28 | E2 | Per-code-block copy button | - [ ] | High | 1hr | +| 29 | E3 | "Try with other model" | - [ ] | Medium | 1.5hr | +| 30 | E4 | Live streaming metrics | - [ ] | Medium | 1hr | +| 31 | E5 | Rating (👍/👎) + aggregation | - [ ] | Low | 1.5hr | + +**E1 Exit criteria:** + +- [ ] Hover over assistant message shows action bar: Copy, Regenerate dropdown, 👍, 👎 +- [ ] Copy copies markdown source text +- [ ] Regenerate sends same prompt to same model, creates branch +- [ ] Branch navigation `← 1 of N →` shown when multiple variants exist + +**E2 Exit criteria:** + +- [ ] Code blocks in MarkdownResponse have a Copy button in the header +- [ ] Button copies only the code content (not language label or fences) +- [ ] Visual feedback: button text changes to "Copied!" for 2s + +**E3 Exit criteria:** + +- [ ] Regenerate dropdown shows loaded models +- [ ] Selecting a model re-sends the same user prompt to that model +- [ ] New response added as a branch variant + +**E4 Exit criteria:** + +- [ ] During streaming: live counter shows `42 tokens · 38.2 tok/s` +- [ ] Stop button (red square) prominently visible during streaming + +**E5 Exit criteria:** + +- [ ] 👍/👎 buttons on each assistant message +- [ ] Rating saved to message in IndexedDB +- [ ] Rating data included in export + +--- + +### Phase E — Commit Template + +``` +feat(local-llm): Phase E — response enhancements (E1-E5) + +E1: Per-message action bar (copy, regenerate, rating) on hover +E2: Per-code-block copy button in MarkdownResponse +E3: "Try with other model" — regenerate with different model as branch +E4: Live streaming metrics (token count + tok/s during stream) +E5: Rating (👍/👎) persisted per message +``` + +--- + +## Phase F — Scheduled Tasks _(Sprint 24–25)_ + +**Goal:** Cron-based automated prompts with shell/file input. + +**Depends on:** Phase A + +**Estimated effort:** ~11 hours (7 tasks) + +| # | ID | Task | Status | Priority | Est | +| --- | --- | --------------------------------- | ------ | -------- | ----- | +| 32 | F1 | Install cron-parser + cron.ts | - [ ] | Critical | 1.5hr | +| 33 | F2 | ScheduledTask CRUD in db.ts | - [ ] | Critical | 1hr | +| 34 | F3 | Task editor UI | - [ ] | High | 2.5hr | +| 35 | F4 | Task runner (browser setInterval) | - [ ] | High | 2.5hr | +| 36 | F5 | /api/system/exec route | - [ ] | High | 1.5hr | +| 37 | F6 | Task run history + notification | - [ ] | Medium | 1hr | +| 38 | F7 | 5 built-in task templates | - [ ] | Low | 1hr | + +**F1 Exit criteria:** + +- [ ] `npm install cron-parser` succeeds +- [ ] `cron.ts` exports `parseCron(expr)`, `getNextRun(expr)`, `cronToHuman(expr)`, `shouldRunNow(expr, lastRun)` + +**F5 Exit criteria (critical security):** + +- [ ] POST `/api/system/exec` accepts `{ command: string, args: string[] }` +- [ ] Validates command against allowlist: `git`, `npm`, `brew`, `cat`, `ls`, `wc`, `du`, `df`, `echo`, `date` +- [ ] Returns 403 for blocked commands +- [ ] Uses `execFile` (never `exec`) +- [ ] Returns `{ stdout: string, stderr: string, exitCode: number }` + +--- + +### Phase F — Commit Template + +``` +feat(local-llm): Phase F — scheduled tasks (F1-F7) + +F1: cron-parser integration + cron utility functions +F2: ScheduledTask CRUD in IndexedDB +F3: Task editor modal (schedule, model, input, output, prompt) +F4: Browser-based task runner with setInterval + cron matching +F5: /api/system/exec — safe shell command execution with allowlist +F6: Task run history panel + browser notifications +F7: 5 built-in task templates (morning brief, git diff, etc.) +``` + +--- + +## Phase G — Projects + Orchestration _(Sprint 26–28)_ + +**Goal:** Conversation organization and multi-model workflows. + +**Depends on:** Phases A, B + +**Estimated effort:** ~13 hours (7 tasks) + +| # | ID | Task | Status | Priority | Est | +| --- | --- | --------------------------------- | ------ | -------- | ----- | +| 39 | G1 | Project CRUD in db.ts | - [ ] | High | 1hr | +| 40 | G2 | Project sidebar + drag-to-project | - [ ] | High | 2.5hr | +| 41 | G3 | Project system context injection | - [ ] | Medium | 1hr | +| 42 | G4 | Cmd+P project switcher | - [ ] | Medium | 1hr | +| 43 | G5 | Chain orchestration mode | - [ ] | Medium | 3hr | +| 44 | G6 | Race orchestration mode | - [ ] | Medium | 2.5hr | +| 45 | G7 | Vote orchestration mode | - [ ] | Low | 2hr | + +**G5 Exit criteria:** + +- [ ] User can configure a chain: select 2-5 models with per-step system prompts +- [ ] Chain executes sequentially: step N output becomes step N+1 input via `{prev}` placeholder +- [ ] Intermediate results shown in collapsible blocks +- [ ] Final result shown as primary response +- [ ] Chain saveable as a Quick Action + +**G6 Exit criteria:** + +- [ ] Race sends same prompt to N models in parallel +- [ ] All results stream independently with per-model timing +- [ ] Results shown side-by-side (or stacked on mobile) +- [ ] User can "Pick winner" to continue conversation with that model + +**G7 Exit criteria:** + +- [ ] Vote sends to 3+ models, collects all responses +- [ ] Synthesizer model reads all responses and produces consensus +- [ ] Consensus shown at top; individual responses in expandable tabs +- [ ] Disagreements highlighted + +--- + +### Phase G — Commit Template + +``` +feat(local-llm): Phase G — projects + multi-model orchestration (G1-G7) + +G1: Project CRUD in IndexedDB +G2: Project sidebar section with drag-to-project +G3: Project system context injected into all project conversations +G4: Cmd+P project switcher +G5: Chain orchestration — sequential multi-model pipeline +G6: Race orchestration — parallel model competition +G7: Vote orchestration — consensus synthesis +``` + +--- + +## Summary + +| Phase | Sprint | Tasks | Focus | Effort | Depends On | Status | +| --------- | ------ | ------------ | -------------------------- | --------- | ---------- | ------ | +| **A** | 14–16 | A1–A8 | Foundation + Conversations | ~15hr | — | - [ ] | +| **B** | 17–18 | B1–B6 | Quick Actions + Cmd+K | ~10hr | A | - [ ] | +| **C** | 19–20 | C1–C5 | Custom Agents | ~9hr | A | - [ ] | +| **D** | 21–22 | D1–D7 | Model Router + Multi-Modal | ~13hr | A | - [ ] | +| **E** | 23 | E1–E5 | Response Enhancements | ~7hr | A | - [ ] | +| **F** | 24–25 | F1–F7 | Scheduled Tasks | ~11hr | A | - [ ] | +| **G** | 26–28 | G1–G7 | Projects + Orchestration | ~13hr | A, B | - [ ] | +| **Total** | | **45 tasks** | | **~78hr** | | | + +**Dependency graph:** + +``` +Phase A (foundation) + ├── Phase B (quick actions) + │ └── Phase G (projects + orchestration) — needs B for QA-saveable orchestrations + ├── Phase C (agents) + ├── Phase D (router + multi-modal) + ├── Phase E (response enhancements) + └── Phase F (scheduled tasks) +``` + +Phases B, C, D, E, F can run in **parallel** after A. Phase G requires both A and B. + +--- + +## New localStorage Keys (v4) + +| Key | Type | Phase | Purpose | +| ----------------------- | ------ | ----- | ---------------------------- | +| `llm-migrated-v4` | string | A | Migration guard flag | +| `llm-sidebar-state` | string | A | Sidebar expanded/collapsed | +| `llm-model-defaults` | JSON | D | Model defaults per task type | +| `llm-qa-seeded` | string | B | Quick Action seed guard | +| `llm-agents-seeded` | string | C | Agent seed guard | +| `llm-command-allowlist` | JSON | F | Extended safe command list | + +--- + +## New npm Dependencies + +| Package | Phase | Size | Purpose | +| ------------- | ----- | ---- | -------------------------------- | +| `idb` | A | ~1KB | IndexedDB Promise wrapper | +| `fuse.js` | B | ~6KB | Fuzzy search for command palette | +| `cron-parser` | F | ~3KB | Cron expression parsing | + +--- + +_This roadmap is the single source of truth for implementation. Update checkboxes and add commit hashes as work progresses._