# 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.

**Status:** ✅ Completed in commit [`e17bb31`](https://github.com/saravanakumardb1/learning_ai_common_plat/commit/e17bb31)

**Estimated effort:** ~15 hours (8 tasks)

| #   | ID  | Task                             | Status                                                                                          | Priority | Est   |
| --- | --- | -------------------------------- | ----------------------------------------------------------------------------------------------- | -------- | ----- |
| 1   | A1  | Install `idb` + create db.ts     | - [x] ([`e17bb31`](https://github.com/saravanakumardb1/learning_ai_common_plat/commit/e17bb31)) | Critical | 1.5hr |
| 2   | A2  | TypeScript interfaces (types.ts) | - [x] ([`e17bb31`](https://github.com/saravanakumardb1/learning_ai_common_plat/commit/e17bb31)) | Critical | 1hr   |
| 3   | A3  | Route group: (mission-control)   | - [x] ([`e17bb31`](https://github.com/saravanakumardb1/learning_ai_common_plat/commit/e17bb31)) | Critical | 2hr   |
| 4   | A4  | Route group: (workspace) layout  | - [x] ([`e17bb31`](https://github.com/saravanakumardb1/learning_ai_common_plat/commit/e17bb31)) | Critical | 2hr   |
| 5   | A5  | Sidebar component                | - [x] ([`e17bb31`](https://github.com/saravanakumardb1/learning_ai_common_plat/commit/e17bb31)) | Critical | 2.5hr |
| 6   | A6  | Conversation view + streaming    | - [x] ([`e17bb31`](https://github.com/saravanakumardb1/learning_ai_common_plat/commit/e17bb31)) | Critical | 3hr   |
| 7   | A7  | Auto-title + context bar         | - [x] ([`e17bb31`](https://github.com/saravanakumardb1/learning_ai_common_plat/commit/e17bb31)) | High     | 1.5hr |
| 8   | A8  | v3 → v4 migration                | - [x] ([`e17bb31`](https://github.com/saravanakumardb1/learning_ai_common_plat/commit/e17bb31)) | 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 `<ConversationList>` 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 `<ConversationView>`

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 `<MessageThread>` and `<InputBar>`
   - `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 `<MessageBubble>` 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 `<MarkdownResponse>` 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 `<ContextBar>` 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.

**Status:** ✅ Completed in commit [`7ae92da`](https://github.com/saravanakumardb1/learning_ai_common_plat/commit/7ae92da)

**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  | - [x] ([`7ae92da`](https://github.com/saravanakumardb1/learning_ai_common_plat/commit/7ae92da)) | Critical | 2hr   |
| 10  | B2  | Install fuse.js + command palette | - [x] ([`7ae92da`](https://github.com/saravanakumardb1/learning_ai_common_plat/commit/7ae92da)) | Critical | 3hr   |
| 11  | B3  | QA launcher → conversation        | - [x] ([`7ae92da`](https://github.com/saravanakumardb1/learning_ai_common_plat/commit/7ae92da)) | Critical | 1hr   |
| 12  | B4  | Custom QA editor (CRUD)           | - [x] ([`7ae92da`](https://github.com/saravanakumardb1/learning_ai_common_plat/commit/7ae92da)) | Medium   | 2hr   |
| 13  | B5  | Usage tracking + frequent section | - [x] ([`7ae92da`](https://github.com/saravanakumardb1/learning_ai_common_plat/commit/7ae92da)) | Low      | 1hr   |
| 14  | B6  | QA export/import                  | - [x] ([`7ae92da`](https://github.com/saravanakumardb1/learning_ai_common_plat/commit/7ae92da)) | 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<string>`:
   - 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<QuickAction[]>` — returns all custom (non-builtin) QAs.
2. `importQuickActions(actions: QuickAction[]): Promise<number>` — 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.

**Status:** ✅ Completed in commit [`d18b695`](https://github.com/saravanakumardb1/learning_ai_common_plat/commit/d18b695)

**Depends on:** Phase A

**Estimated effort:** ~9 hours (5 tasks)

| #   | ID  | Task                          | Status                                                                                          | Priority | Est   |
| --- | --- | ----------------------------- | ----------------------------------------------------------------------------------------------- | -------- | ----- |
| 15  | C1  | Agent data + built-in library | - [x] ([`d18b695`](https://github.com/saravanakumardb1/learning_ai_common_plat/commit/d18b695)) | Critical | 2hr   |
| 16  | C2  | Agent picker in sidebar       | - [x] ([`d18b695`](https://github.com/saravanakumardb1/learning_ai_common_plat/commit/d18b695)) | Critical | 1.5hr |
| 17  | C3  | Agent editor (full-screen)    | - [x] ([`d18b695`](https://github.com/saravanakumardb1/learning_ai_common_plat/commit/d18b695)) | High     | 2.5hr |
| 18  | C4  | Agent → conversation wiring   | - [x] ([`d18b695`](https://github.com/saravanakumardb1/learning_ai_common_plat/commit/d18b695)) | Critical | 2hr   |
| 19  | C5  | Agent export/import           | - [x] ([`d18b695`](https://github.com/saravanakumardb1/learning_ai_common_plat/commit/d18b695)) | 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.

**Status:** ✅ Completed in commit [`d625be2`](https://github.com/saravanakumardb1/learning_ai_common_plat/commit/d625be2)

**Depends on:** Phase A

**Estimated effort:** ~13 hours (7 tasks)

| #   | ID  | Task                             | Status                                                                                          | Priority | Est   |
| --- | --- | -------------------------------- | ----------------------------------------------------------------------------------------------- | -------- | ----- |
| 20  | D1  | Task classifier (router.ts)      | - [x] ([`d625be2`](https://github.com/saravanakumardb1/learning_ai_common_plat/commit/d625be2)) | High     | 1hr   |
| 21  | D2  | Model defaults config + settings | - [x] ([`d625be2`](https://github.com/saravanakumardb1/learning_ai_common_plat/commit/d625be2)) | High     | 1.5hr |
| 22  | D3  | Auto-routing UI (model chip)     | - [x] ([`d625be2`](https://github.com/saravanakumardb1/learning_ai_common_plat/commit/d625be2)) | High     | 2hr   |
| 23  | D4  | Rich InputBar (attach/voice/img) | - [x] ([`d625be2`](https://github.com/saravanakumardb1/learning_ai_common_plat/commit/d625be2)) | High     | 2hr   |
| 24  | D5  | File attachment processing       | - [x] ([`d625be2`](https://github.com/saravanakumardb1/learning_ai_common_plat/commit/d625be2)) | High     | 2.5hr |
| 25  | D6  | Voice input → Whisper            | - [x] ([`d625be2`](https://github.com/saravanakumardb1/learning_ai_common_plat/commit/d625be2)) | Medium   | 2hr   |
| 26  | D7  | Drag-and-drop + paste detection  | - [x] ([`d625be2`](https://github.com/saravanakumardb1/learning_ai_common_plat/commit/d625be2)) | 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.

**Status:** ✅ Completed in commit [`e15a5a2`](https://github.com/saravanakumardb1/learning_ai_common_plat/commit/e15a5a2)

**Depends on:** Phase A

**Estimated effort:** ~7 hours (5 tasks)

| #   | ID  | Task                         | Status                                                                                          | Priority | Est   |
| --- | --- | ---------------------------- | ----------------------------------------------------------------------------------------------- | -------- | ----- |
| 27  | E1  | Per-message action bar       | - [x] ([`e15a5a2`](https://github.com/saravanakumardb1/learning_ai_common_plat/commit/e15a5a2)) | High     | 2hr   |
| 28  | E2  | Per-code-block copy button   | - [x] ([`e15a5a2`](https://github.com/saravanakumardb1/learning_ai_common_plat/commit/e15a5a2)) | High     | 1hr   |
| 29  | E3  | "Try with other model"       | - [x] ([`e15a5a2`](https://github.com/saravanakumardb1/learning_ai_common_plat/commit/e15a5a2)) | Medium   | 1.5hr |
| 30  | E4  | Live streaming metrics       | - [x] ([`e15a5a2`](https://github.com/saravanakumardb1/learning_ai_common_plat/commit/e15a5a2)) | Medium   | 1hr   |
| 31  | E5  | Rating (👍/👎) + aggregation | - [x] ([`e15a5a2`](https://github.com/saravanakumardb1/learning_ai_common_plat/commit/e15a5a2)) | 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.

**Status:** ✅ Completed in commit [`52f3d16`](https://github.com/saravanakumardb1/learning_ai_common_plat/commit/52f3d16)

**Depends on:** Phase A

**Estimated effort:** ~11 hours (7 tasks)

| #   | ID  | Task                              | Status                                                                                          | Priority | Est   |
| --- | --- | --------------------------------- | ----------------------------------------------------------------------------------------------- | -------- | ----- |
| 32  | F1  | Install cron-parser + cron.ts     | - [x] ([`52f3d16`](https://github.com/saravanakumardb1/learning_ai_common_plat/commit/52f3d16)) | Critical | 1.5hr |
| 33  | F2  | ScheduledTask CRUD in db.ts       | - [x] ([`52f3d16`](https://github.com/saravanakumardb1/learning_ai_common_plat/commit/52f3d16)) | Critical | 1hr   |
| 34  | F3  | Task editor UI                    | - [x] ([`52f3d16`](https://github.com/saravanakumardb1/learning_ai_common_plat/commit/52f3d16)) | High     | 2.5hr |
| 35  | F4  | Task runner (browser setInterval) | - [x] ([`52f3d16`](https://github.com/saravanakumardb1/learning_ai_common_plat/commit/52f3d16)) | High     | 2.5hr |
| 36  | F5  | /api/system/exec route            | - [x] ([`52f3d16`](https://github.com/saravanakumardb1/learning_ai_common_plat/commit/52f3d16)) | High     | 1.5hr |
| 37  | F6  | Task run history + notification   | - [x] ([`52f3d16`](https://github.com/saravanakumardb1/learning_ai_common_plat/commit/52f3d16)) | Medium   | 1hr   |
| 38  | F7  | 5 built-in task templates         | - [x] ([`52f3d16`](https://github.com/saravanakumardb1/learning_ai_common_plat/commit/52f3d16)) | 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.

**Status:** ✅ Completed in commit [`6d98d12`](https://github.com/saravanakumardb1/learning_ai_common_plat/commit/6d98d12)

**Depends on:** Phases A, B

**Estimated effort:** ~13 hours (7 tasks)

| #   | ID  | Task                              | Status                                                                                          | Priority | Est   |
| --- | --- | --------------------------------- | ----------------------------------------------------------------------------------------------- | -------- | ----- |
| 39  | G1  | Project CRUD in db.ts             | - [x] ([`52f3d16`](https://github.com/saravanakumardb1/learning_ai_common_plat/commit/52f3d16)) | High     | 1hr   |
| 40  | G2  | Project sidebar + drag-to-project | - [x] ([`6d98d12`](https://github.com/saravanakumardb1/learning_ai_common_plat/commit/6d98d12)) | High     | 2.5hr |
| 41  | G3  | Project system context injection  | - [x] ([`6d98d12`](https://github.com/saravanakumardb1/learning_ai_common_plat/commit/6d98d12)) | Medium   | 1hr   |
| 42  | G4  | Cmd+P project switcher            | - [x] ([`6d98d12`](https://github.com/saravanakumardb1/learning_ai_common_plat/commit/6d98d12)) | Medium   | 1hr   |
| 43  | G5  | Chain orchestration mode          | - [x] ([`6d98d12`](https://github.com/saravanakumardb1/learning_ai_common_plat/commit/6d98d12)) | Medium   | 3hr   |
| 44  | G6  | Race orchestration mode           | - [x] ([`6d98d12`](https://github.com/saravanakumardb1/learning_ai_common_plat/commit/6d98d12)) | Medium   | 2.5hr |
| 45  | G7  | Vote orchestration mode           | - [x] ([`6d98d12`](https://github.com/saravanakumardb1/learning_ai_common_plat/commit/6d98d12)) | 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     | —          | - [x] [`e17bb31`](https://github.com/saravanakumardb1/learning_ai_common_plat/commit/e17bb31) |
| **B**     | 17–18  | B1–B6        | Quick Actions + Cmd+K      | ~10hr     | A          | - [x] [`7ae92da`](https://github.com/saravanakumardb1/learning_ai_common_plat/commit/7ae92da) |
| **C**     | 19–20  | C1–C5        | Custom Agents              | ~9hr      | A          | - [x] [`d18b695`](https://github.com/saravanakumardb1/learning_ai_common_plat/commit/d18b695) |
| **D**     | 21–22  | D1–D7        | Model Router + Multi-Modal | ~13hr     | A          | - [x] [`d625be2`](https://github.com/saravanakumardb1/learning_ai_common_plat/commit/d625be2) |
| **E**     | 23     | E1–E5        | Response Enhancements      | ~7hr      | A          | - [x] [`e15a5a2`](https://github.com/saravanakumardb1/learning_ai_common_plat/commit/e15a5a2) |
| **F**     | 24–25  | F1–F7        | Scheduled Tasks            | ~11hr     | A          | - [x] [`52f3d16`](https://github.com/saravanakumardb1/learning_ai_common_plat/commit/52f3d16) |
| **G**     | 26–28  | G1–G7        | Projects + Orchestration   | ~13hr     | A, B       | - [x] [`6d98d12`](https://github.com/saravanakumardb1/learning_ai_common_plat/commit/6d98d12) |
| **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._
