diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index 61151cf..8f7f8d7 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -67,10 +67,44 @@ To add a new table or column: edit `schema.ts`, run `drizzle-kit generate`, then 3. **Types** — `AppRouter` is exported from `src/main/router/index.ts` and imported in `src/renderer/lib/trpc.ts` — types flow automatically 4. **UI** — Create components under `src/renderer/components//`, use `trpc.*.*useQuery()` for data +### AI Subsystem (`src/main/ai/`) + +LangGraph-based agentic system with pluggable LLM providers (OpenAI, Anthropic, GitHub Copilot). + +**Orchestrator** (`orchestrator.ts`): Classifies user intent → routes to one of three specialist agents: +- **Project agent** — project-scoped Q&A with tools: `read_project_notes`, `add_task`, `get_summary`, `suggest_checkpoints`, `suggest_tasks` +- **Knowledge agent** — cross-project semantic search via `vector_search_all` +- **General agent** — workspace-wide `add_task` + +Tool-calling strategy differs by provider: OpenAI/Anthropic use LangChain `bindTools()` + ToolMessage loop (max 5 iterations); Copilot uses SDK-native tools (loop handled internally). + +**Streaming**: Orchestrator calls `sendStreamChunk(sender, token, done)` over IPC channel `'ai:stream'`. Renderer subscribes via `window.electronAI.onStreamChunk()` in `AIChatPanel.tsx`. `` blocks are filtered before sending to renderer. + +**Provider factory** (`llm.ts`): `gpt-4o-mini` (OpenAI), `claude-sonnet-4-20250514` (Anthropic), or ChatCopilot wrapper — all with `temperature: 0.3` and streaming enabled. + +**Token storage** (`token.ts`) — three-tier fallback: +1. keytar (OS keychain) — preferred, encrypted per-user +2. electron-store + `safeStorage` — encrypted at rest +3. Plain electron-store — WSL fallback + +Keytar service name is `'adiuva'`. Once keytar fails, `keytarFailed` flag skips it for the session. + +**AI approval pattern**: Tasks and checkpoints have `isAiSuggested` (bool) and `isApproved` (bool) columns. AI-suggested items appear in the UI pending user approval before being treated as real records. + +### Vector Embeddings (`src/main/db/vectordb.ts`) + +LanceDB stored in `{userData}/vectors/`. Table schema: `{ id, projectId, content, vector }`. Vectors are 1536-dimensional (`text-embedding-3-small`). Embeddings use a priority chain: Copilot CLI token → OpenAI token. + +- Note create/update fires `upsertNoteEmbedding()` (fire-and-forget, errors swallowed) +- `migrateNotesIfNeeded()` backfills existing notes on first startup +- `searchNotes(query, limit=5)` is called by the Knowledge agent tool + ### Key Config Notes - Vite configs use `.mts` extension (not `.ts`) to avoid ESM/CJS conflicts with electron-forge's externalize-deps plugin - `@/*` path alias resolves to `src/renderer/*` (TypeScript + Vite + shadcn/ui all share this alias) - shadcn/ui style: **new-york**, base color: **neutral** - Icons: **lucide-react** throughout — do not introduce other icon libraries -- Tailwind 4 (not 3) — use CSS variable theming via `globals.css`, not `tailwind.config.js` \ No newline at end of file +- Tailwind 4 (not 3) — use CSS variable theming via `globals.css`, not `tailwind.config.js` +- Notes use Milkdown (`@milkdown/crepe`) as the markdown editor (`src/renderer/components/notes/MilkdownEditor.tsx`) +- Routes: `index`, `tasks`, `timeline`, `projects`, `notes.$noteId` (note ID is a URL param) \ No newline at end of file diff --git a/.claude/DEFAULT_PROMPT.md b/.claude/DEFAULT_PROMPT.md deleted file mode 100644 index 4fe3a20..0000000 --- a/.claude/DEFAULT_PROMPT.md +++ /dev/null @@ -1,44 +0,0 @@ -## Your Task US-025 - -1. Read the full app PRD at `prd-main.md` (in the same directory as this file) -2. Read the PRD at `prd.json` (in the same directory as this file) -3. Read the progress log at `progress.txt` (check Codebase Patterns section first) -4. **DO YOUR JOB** -5. Update the PRD to set `passes: true` for the completed story -6. Append your progress to `progress.txt` - -## Progress Report Format - -APPEND to progress.txt (never replace, always append): -``` -## [Date/Time] - [Story ID] -- What was implemented -- Files changed -- **Learnings for future iterations:** - - Patterns discovered (e.g., "this codebase uses X for Y") - - Gotchas encountered (e.g., "don't forget to update Z when changing W") - - Useful context (e.g., "the evaluation panel is in component X") ---- -``` - -## USER REQUEST -{ - "id": "US-025", - "title": "Home dashboard — AI daily brief and suggestion chips", - "description": "As a user, I want the Home screen to greet me with an AI-generated daily brief and pre-populated suggestion chips for quick queries.", - "acceptanceCriteria": [ - "Greeting rendered as '✦ Hello, {name}' in Geist Semibold 30px with -1px letter-spacing; name sourced from electron-store (defaults to 'there' if not set)", - "Top-right corner stat chip uses shadcn/ui Badge (variant=secondary) showing 'N Task due' where N = count of tasks with dueDate on or before end of today", - "On app open, ai.chat called with global context to generate a daily brief paragraph highlighting tasks due today/this week and recent project activity", - "Brief displayed below greeting in a shadcn/ui Card; bold key phrases rendered as (model wraps them in **markdown bold**)", - "4 suggestion chips rendered in a 4-column flex row below the chat box using shadcn/ui Button (variant=outline); each chip has a Lucide icon + short prompt text", - "Clicking a suggestion chip populates the chat input with the chip's prompt text", - "Chat box uses shadcn/ui Textarea: white bg, border #d4d4d4, shadow-lg, min-height 109px, placeholder 'Ask me anything...'; Send uses shadcn/ui Button (black bg) bottom-right", - "All UI uses shadcn/ui components (already installed)", - "Typecheck passes", - "Verify in browser using dev-browser skill" - ], - "priority": 25, - "passes": false, - "notes": "" -} \ No newline at end of file diff --git a/.claude/prd-main.md b/.claude/prd-main.md deleted file mode 100644 index c9da5d0..0000000 --- a/.claude/prd-main.md +++ /dev/null @@ -1,487 +0,0 @@ -# PRD: Adiuva — MVP Implementation - -> **Status:** APPROVED / READY FOR DEV -> **Version:** 1.0 (MVP) -> **Date:** 2026-02-19 -> **Stack:** Electron · React · TypeScript · shadcn/ui · Tailwind · Drizzle ORM · SQLite · LanceDB · GitHub Copilot SDK -> **Figma:** [Full File](https://www.figma.com/design/FxyJG9kpou4DfD7jM9WHKP/Desk) - ---- - -## Introduction - -Adiuva is a local-first desktop workspace acting as a "Digital Executive Secretary." It centralizes notes, tasks, and project context into a local SQLite database and exposes a multi-agent AI layer (via GitHub Copilot SDK) that proactively surfaces insights and drafts actions. Data never leaves the machine, making it safe for enterprise environments. - ---- - -## Goals - -- Ship a working Electron desktop app with five sections: Home, Timeline, Tasks, Projects, Notes. -- All data persisted locally in SQLite (zero cloud dependency for data storage). -- Hierarchical Client → Sub-Client → Project structure fully navigable from a sidebar tree. -- "Fluid Curtain" pull-down gesture that transitions any view into a full-screen AI chat scoped to the current context. -- Multi-agent AI system (@Orchestrator, @ProjectAgent, @EmailAgent, @KnowledgeAgent) integrated via GitHub Copilot SDK. -- Milestone completion = every section functional at the level described in User Stories below. - ---- - -## UI Summary (from Figma) - -### Shared Shell -- **Left sidebar:** 240px, `#fafafa` background, border-right `#e5e5e5`. - - Top: Adiuva logo/wordmark. - - Nav items: Home (house icon), Timeline (chart-gantt icon), Tasks (clipboard-check icon), Projects (folder-kanban icon). Active item gets `#f5f5f5` accent + no extra border. - - Bottom: Collapse button (panel-left icon). -- **Right edge:** Vertical rotated label "keep scrolling for AI / next section" + chevron-down. This is the visual affordance for the Fluid Curtain pull-down gesture. -- **Font:** Geist (Regular 400, Medium 500, Semibold 600). Sizes: sm=14px, base=16px. -- **Colors:** bg=`#ffffff`, foreground=`#0a0a0a`, muted=`#737373`, border=`#e5e5e5`, sidebar=`#fafafa`, sidebar-accent=`#f5f5f5`, primary=`#171717`, primary-fg=`#fafafa`. - -### HOME -- Top-right corner: stat chip showing "N Task due" count. -- Main area (centered, max-w ~1088px): AI greeting `✦ Hello, {name}` (Heading 2, Geist Semibold 30px, -1px tracking). -- Below: AI-generated daily brief paragraph with **bold** key phrases inline. -- Chat input box: white, border `#d4d4d4`, shadow-lg, 109px tall, placeholder "Ask me anythings...", Send button (black, icon + label) bottom-right of the box. -- Below chat: 4 suggestion chips (`Item` component) — icon badge + short prompt text — in a 4-column flex row. - -### TIMELINE -- Main content area: placeholder background `#fef2f2` (the Gantt chart is not yet designed in Figma; implementation is free to choose a library). -- Same sidebar + right-edge Fluid Curtain affordance. - -### TASKS -- Header row: 4 stat cards — "Total task", "To Do", "In Progress", "Completed" — each with an icon and count. -- Below: search bar (full-width, placeholder "Search tasks or projects...") + "Order by" dropdown (right) + status filter tabs (All | To Do | In Progress | Completed). -- Task list rows (flat, full-width): - - Checkbox (left) - - Title (bold, 14px) + description subtitle (gray, 14px) - - Priority chip: `HIGH` (up-arrow, red-toned) | `MEDIUM` (right-arrow, gray) | `LOW` (down-arrow, green-toned) - - Due date chip (calendar icon + "Due Mon DD") - - Breadcrumb path (Client > Sub-Client > Project, chevron-separated) - - Assignee (person icon + name string) -- Completed tasks show row with green-tinted background. - -### PROJECTS -- **Left panel (tree):** "Projects" heading + `+` new button + search input. Hierarchical tree: Client (folder, bold) → Sub-Client (folder) → Project (circle/file). Expand/collapse chevrons. Active project highlighted. -- **Right panel (project detail):** - - Breadcrumb (Client > Sub-Client) at top. - - Project name as H1. - - 3 stat cards: Notes count | Tasks Complete (x/y fraction) | Checkpoints (x/y fraction). - - AI Project Summary card (sparkle icon + generated paragraph). - - **Project Timeline:** inline Gantt — months across top (Feb 2026, Mar 2026 …), horizontal bar with dot markers for each checkpoint. Legend: To Do (dark) / Completed (green). "+ Add" button top-right. - - **Tasks (Kanban):** 3 columns — To Do / In progress / Completed. Task cards: title, description, priority chip, due date, assignee. "+ Add" per column header. - - **Notes list:** flat list of note entries + "+ Add" button. - -### NOTES -- Milkdown editor (standalone route) for writing/editing a single note. -- Markdown-native, full-screen editor style. - ---- - -## Data Schema - -```typescript -// clients — hierarchical (self-referencing parentId) -export const clients = sqliteTable('clients', { - id: text('id').primaryKey(), // UUID - parentId: text('parent_id'), // null = top-level client - name: text('name').notNull(), - industry: text('industry'), - createdAt: integer('created_at').notNull(), -}); - -// projects — attached to a client/sub-client, or orphan -export const projects = sqliteTable('projects', { - id: text('id').primaryKey(), - clientId: text('client_id').references(() => clients.id), // nullable - name: text('name').notNull(), - status: text('status').default('active'), // active | archived - aiSummary: text('ai_summary'), // AI-generated paragraph - createdAt: integer('created_at').notNull(), -}); - -// tasks — belong to a project (or global/orphan if projectId null) -export const tasks = sqliteTable('tasks', { - id: text('id').primaryKey(), - projectId: text('project_id').references(() => projects.id), // nullable - title: text('title').notNull(), - description: text('description'), - status: text('status').default('todo'), // todo | in_progress | done - priority: text('priority').default('medium'), // high | medium | low - assignee: text('assignee'), // plain string name - dueDate: integer('due_date'), // unix timestamp - createdAt: integer('created_at').notNull(), -}); - -// checkpoints — milestones on the per-project timeline -export const checkpoints = sqliteTable('checkpoints', { - id: text('id').primaryKey(), - projectId: text('project_id').references(() => projects.id).notNull(), - title: text('title').notNull(), - date: integer('date').notNull(), // unix timestamp - isAiSuggested: integer('is_ai_suggested').default(0), // 0=manual, 1=AI - isApproved: integer('is_approved').default(1), // 0=pending AI approval - createdAt: integer('created_at').notNull(), -}); - -// notes — markdown content attached to a project -export const notes = sqliteTable('notes', { - id: text('id').primaryKey(), - projectId: text('project_id').references(() => projects.id), // nullable - title: text('title').notNull(), - content: text('content').notNull(), // raw Markdown - createdAt: integer('created_at').notNull(), - updatedAt: integer('updated_at').notNull(), -}); -``` - ---- - -## User Stories - -### PHASE 1 — Foundation - ---- - -#### US-001: Electron + React scaffold -**Description:** As a developer, I need a working Electron app with React+TypeScript and a shared main/renderer process setup so that all other features have a platform to run on. - -**Acceptance Criteria:** -- [ ] `electron-builder` or `electron-vite` scaffold with hot-reload in dev. -- [ ] Main process can open a `BrowserWindow` serving the React app. -- [ ] TypeScript strict mode enabled, `tsconfig.json` configured. -- [ ] `package.json` scripts: `dev`, `build`, `preview`. -- [ ] App opens without errors on Linux, macOS, Windows. - ---- - -#### US-002: SQLite database + Drizzle ORM setup -**Description:** As a developer, I need the SQLite database initialized with Drizzle ORM so that all CRUD operations use a typed, schema-driven interface. - -**Acceptance Criteria:** -- [ ] `better-sqlite3` (or `@electric-sql/pglite` alternative) installed in main process. -- [ ] Drizzle schema file defines all 5 tables (clients, projects, tasks, checkpoints, notes). -- [ ] Migration runs on app start; DB file created at `~/.adiuva/data.db` (or `app.getPath('userData')`). -- [ ] Drizzle Studio accessible in dev mode (`drizzle-kit studio`). -- [ ] TypeScript types inferred from schema (no manual type duplication). - ---- - -#### US-003: App shell — sidebar navigation -**Description:** As a user, I want a persistent left sidebar so that I can navigate between all sections of the app. - -**Acceptance Criteria:** -- [ ] Sidebar renders at 240px with `#fafafa` background and right border. -- [ ] Nav items: Home (house), Timeline (chart-gantt), Tasks (clipboard-check), Projects (folder-kanban). Each uses Lucide icon + label. -- [ ] Active route highlights item with `#f5f5f5` accent background. -- [ ] Collapse button at bottom toggles sidebar to icon-only mode (64px). -- [ ] Router renders correct view for each nav item (React Router or TanStack Router). -- [ ] Verify in browser using dev-browser skill. - ---- - -#### US-004: Client CRUD (hierarchical) -**Description:** As a user, I want to create, rename, and delete Clients and Sub-Clients so that I can mirror real-world corporate structures. - -**Acceptance Criteria:** -- [ ] "New Client" action creates a top-level client (parentId = null). -- [ ] "New Sub-Client" action (available on a selected client) creates a child (parentId = selected client's id). -- [ ] Client and Sub-Client names are editable via inline rename or modal. -- [ ] Deleting a client warns if it has child clients or projects; cascade delete is opt-in. -- [ ] Changes are immediately persisted to SQLite. -- [ ] TypeScript types pass; no `any`. - ---- - -#### US-005: Project CRUD -**Description:** As a user, I want to create projects attached to a client/sub-client or as standalone orphans so that I can track both client and internal work. - -**Acceptance Criteria:** -- [ ] "New Project" dialog asks for name and optionally a client (dropdown, searchable). -- [ ] Projects with no client appear under an "Internal / No Client" group in the tree. -- [ ] Project can be re-parented (moved to a different client) via edit dialog. -- [ ] Project status toggled between `active` and `archived`. -- [ ] Archived projects hidden by default; toggle to show. -- [ ] Persisted to SQLite immediately. - ---- - -#### US-006: Projects sidebar tree view -**Description:** As a user, I want to see all clients, sub-clients, and projects in a collapsible tree in the Projects section so that I can navigate the hierarchy at a glance. - -**Acceptance Criteria:** -- [ ] Tree renders: Client (folder icon) → Sub-Client (folder icon) → Project (circle icon). -- [ ] Clients and sub-clients expand/collapse independently. -- [ ] Search input filters tree in real-time (client name, sub-client name, project name). -- [ ] Clicking a project loads the Project Detail panel on the right. -- [ ] Active project is highlighted in the tree. -- [ ] Verify in browser using dev-browser skill. - ---- - -#### US-007: Task CRUD (global Tasks view) -**Description:** As a user, I want a global task list where I can create, filter, search, and update tasks so that I can manage all work across projects in one place. - -**Acceptance Criteria:** -- [ ] 4 stat cards at top: Total, To Do, In Progress, Completed — update reactively. -- [ ] Search filters tasks by title or description, case-insensitive. -- [ ] Status filter tabs (All | To Do | In Progress | Completed) filter list. -- [ ] "Order by" dropdown supports: Due Date, Priority, Created Date. -- [ ] Task rows show: checkbox, title, description, priority chip (HIGH/MEDIUM/LOW with color), due date chip, breadcrumb (Client > Sub-Client > Project), assignee. -- [ ] Clicking checkbox toggles status: todo → done (skip in_progress for quick-complete). -- [ ] Inline "New Task" button opens a creation modal with fields: title, description, priority, due date, project (optional), assignee (optional). -- [ ] All changes persisted to SQLite. -- [ ] Verify in browser using dev-browser skill. - ---- - -#### US-008: Manual Timeline (global & per-project) -**Description:** As a user, I want to view and manually create timeline checkpoints on a Gantt-style view so that I have full control over milestone dates. - -**Acceptance Criteria:** -- [ ] Timeline view renders a horizontal time axis (months) with dot markers for each checkpoint. -- [ ] Global Timeline shows checkpoints from all projects (color-coded by project or status). -- [ ] Per-project timeline (in Project Detail) scoped to that project's checkpoints only. -- [ ] "+ Add" button opens a dialog: title, date picker, project (in global view). -- [ ] Checkpoint dots distinguish status: To Do (dark/filled) vs Completed (green). -- [ ] "Today" marker line displayed on the timeline. -- [ ] Clicking a checkpoint dot shows a popover with title, date, and delete action. -- [ ] Persisted to SQLite. -- [ ] Verify in browser using dev-browser skill. - ---- - -#### US-009: Project Detail view -**Description:** As a user, I want a rich project detail panel that shows notes count, tasks summary, AI summary, timeline, Kanban, and notes list in one scrollable view. - -**Acceptance Criteria:** -- [ ] Breadcrumb (Client > Sub-Client) rendered at top. -- [ ] Stat cards: Notes count | Tasks Complete (x/y) | Checkpoints (x/y). -- [ ] AI Project Summary card shows sparkle icon + placeholder text ("AI summary will appear here") until agent generates it. -- [ ] Inline Project Timeline (same Gantt component as US-008, scoped). -- [ ] Kanban board: To Do / In Progress / Completed columns. Task cards show title, description, priority, due date, assignee. Drag between columns updates `status`. -- [ ] Notes list: title + creation date for each note. Click opens Milkdown editor. "+ Add" creates new note. -- [ ] Verify in browser using dev-browser skill. - ---- - -#### US-010: Notes editor (Milkdown) -**Description:** As a user, I want a full-screen Markdown editor for each note so that I can write rich content without leaving the app. - -**Acceptance Criteria:** -- [ ] Milkdown editor renders in a dedicated route (`/notes/:noteId`). -- [ ] Supports: headings, bold, italic, code blocks, bullet lists, ordered lists, blockquotes. -- [ ] Auto-saves content to SQLite on change (debounced, 500ms). -- [ ] Back navigation returns to the project detail view (or previous location). -- [ ] Note title editable at the top (separate from Milkdown content). -- [ ] Verify in browser using dev-browser skill. - ---- - -### PHASE 2 — The Fluid Curtain & Agents - ---- - -#### US-011: Fluid Curtain — pull-down gesture + animation -**Description:** As a user, I want to pull down from the top of any view to slide the app off-screen and reveal the AI chat layer beneath. - -**Acceptance Criteria:** -- [ ] Scrolling up past the top of content (overscroll) OR pressing a keyboard shortcut (e.g., `Cmd/Ctrl+K` or `⌥↓`) triggers the curtain. -- [ ] App panel slides down using Framer Motion spring animation, exiting the bottom of the viewport. -- [ ] AI Chat view is fully revealed below (full-screen, no sidebar obstruction). -- [ ] Pulling the app back up (swipe/scroll from bottom or shortcut) re-covers the chat. -- [ ] Animation is smooth (no jank). Spring config: stiffness 300, damping 30. -- [ ] Right-edge "keep scrolling for AI" label and chevron are visible in every section as the affordance. -- [ ] Verify in browser using dev-browser skill. - ---- - -#### US-012: Context-scoped AI chat -**Description:** As a user, I want the AI chat (revealed by the curtain) to know the context I was in so that answers are scoped to the right project or global scope. - -**Acceptance Criteria:** -- [ ] When curtain is pulled from a Project Detail view, a context header displays "Chatting about: [Project Name]". -- [ ] When pulled from Home, context is global (all data). -- [ ] Context is passed as a system message to the GitHub Copilot SDK call (project notes, tasks, checkpoints as structured JSON). -- [ ] Agent responses reference only documents within the scoped project. -- [ ] Chat history is session-only (not persisted in MVP). - ---- - -#### US-013: GitHub Copilot SDK integration + @Orchestrator -**Description:** As a developer, I need the GitHub Copilot SDK wired up with an Orchestrator agent that routes user messages to the correct specialist agent. - -**Acceptance Criteria:** -- [ ] SDK initialized in main process with enterprise credentials (from env/config). -- [ ] `@Orchestrator` reads user intent and calls `route_to_project`, `route_to_general`, or `route_to_email` tool. -- [ ] Routing result invokes the correct specialist agent and returns its response. -- [ ] Streaming responses supported (tokens shown incrementally in chat UI). -- [ ] Errors handled gracefully (SDK timeout, auth failure) with user-facing message. - ---- - -#### US-014: @ProjectAgent with project tools -**Description:** As a user, I want the AI to answer project-specific questions and take actions (add task, suggest checkpoints) within the scoped project. - -**Acceptance Criteria:** -- [ ] `read_project_notes` tool fetches all notes for the project from SQLite. -- [ ] `add_task` tool creates a task in the project (writes to SQLite) and confirms in chat. -- [ ] `suggest_checkpoints` tool returns a list of proposed checkpoints (title + date) as interactive cards in chat (Approve / Reject each). -- [ ] Approved checkpoints are inserted into `checkpoints` table with `is_ai_suggested=1, is_approved=1`. -- [ ] `get_summary` tool generates a 2-3 sentence project summary and updates `projects.ai_summary` in SQLite. - ---- - -### PHASE 3 — Intelligence & RAG - ---- - -#### US-015: LanceDB vector store setup + note embedding -**Description:** As a developer, I need notes and project content embedded into LanceDB so that semantic search is possible across all projects. - -**Acceptance Criteria:** -- [ ] LanceDB initialized in main process, storing vectors at `~/.adiuva/vectors/`. -- [ ] On note save (create or update), content is embedded via GitHub Copilot SDK embeddings endpoint and stored in LanceDB with `{noteId, projectId, content}` metadata. -- [ ] Existing notes are indexed on first startup (migration script). -- [ ] Embedding errors logged but do not block the save operation. - ---- - -#### US-016: @KnowledgeAgent — semantic search across all projects -**Description:** As a user, I want to ask "what did we decide about X?" and get answers pulled from across all past project notes, not just the current one. - -**Acceptance Criteria:** -- [ ] `vector_search_all` tool accepts a query string, returns top-5 semantically similar note chunks from LanceDB. -- [ ] Results include source note title and project name for attribution. -- [ ] @Orchestrator routes knowledge queries to @KnowledgeAgent. -- [ ] Response in chat includes inline citations ("From: Project A — Meeting Notes, Feb 12"). - ---- - -#### US-017: AI checkpoint suggestions from notes -**Description:** As a user, I want the AI to proactively analyze my meeting notes and suggest timeline checkpoints I may have missed. - -**Acceptance Criteria:** -- [ ] Triggered manually ("Suggest checkpoints" button in Project Detail timeline header) or by @ProjectAgent tool call. -- [ ] @ProjectAgent reads all notes for the project, extracts date-anchored commitments, returns as suggested checkpoints. -- [ ] Suggestions appear as dismissible cards in the Timeline UI with `isAiSuggested=1, isApproved=0`. -- [ ] Approve → `isApproved` set to 1, checkpoint appears on timeline. -- [ ] Reject → checkpoint deleted. -- [ ] Verify in browser using dev-browser skill. - ---- - -#### US-018: Home dashboard — AI daily brief -**Description:** As a user, I want the Home screen to greet me with an AI-generated daily brief summarizing my tasks and suggesting actions. - -**Acceptance Criteria:** -- [ ] On app open, @Orchestrator queries tasks due today/this week and recent project activity. -- [ ] AI generates a personalized paragraph with key highlights (tasks due, suggested calls/emails). -- [ ] Brief is displayed below the greeting with **bold** key phrases inline (as in Figma). -- [ ] 4 suggestion chips below the chat box are pre-populated with context-relevant queries. -- [ ] Chat box on Home is scoped globally (no project context). -- [ ] Verify in browser using dev-browser skill. - ---- - -## Functional Requirements - -- **FR-01:** All data stored locally in SQLite at `app.getPath('userData')/adiuva.db`. -- **FR-02:** App functions fully offline; AI features degrade gracefully when network is unavailable. -- **FR-03:** Client tree supports unlimited nesting depth but UI only needs to display 3 levels (Client → Sub-Client → Project). -- **FR-04:** Tasks table has a nullable `projectId`; global Tasks view shows all tasks regardless. -- **FR-05:** The "Fluid Curtain" animation must not lose the underlying view state (app slides but remains mounted). -- **FR-06:** GitHub Copilot SDK credentials are stored in OS keychain (not plaintext config). -- **FR-07:** Milkdown auto-save uses a 500ms debounce; unsaved indicator shown if pending. -- **FR-08:** All IDs are UUIDs (use `crypto.randomUUID()`). -- **FR-09:** Drizzle migrations run automatically on startup; never destructive. -- **FR-10:** Kanban drag-and-drop updates `tasks.status` and `tasks.updatedAt` immediately in SQLite. - ---- - -## Non-Goals (Out of Scope for MVP) - -- Email client / inbox integration (EmailAgent tools are stubs only). -- Cloud sync or multi-device support. -- Real assignee accounts (assignee is a plain name string, not a user entity). -- Notifications or system tray alerts. -- Dark mode. -- Mobile or web version. -- Export to PDF/CSV. -- Notes version history. - ---- - -## Design Considerations - -- **Font:** Geist via `@fontsource/geist` or CDN. Apply globally via CSS variable. -- **Icons:** Lucide React (house, chart-gantt, clipboard-check, folder-kanban, panel-left, send, sparkles, chevron-down). -- **Gantt:** ✅ Custom SVG component. Month labels on X axis, `` dots for checkpoints, `` baseline, ``. Use `ResizeObserver` for responsive width. -- **Kanban:** ✅ `@hello-pangea/dnd` — `` wrapping 3 `` columns, each task a ``. -- **Fluid Curtain:** ✅ `framer-motion` `useMotionValue` + `useSpring`. Trigger: `wheel` event at `scrollTop === 0 && deltaY < 0` OR `Cmd/Ctrl+K`. Right-edge "keep scrolling for AI" label is a **visual hint only** (not interactive). -- **shadcn/ui components to reuse:** Button, Input, Badge, Card, Dialog, Separator, Tabs, Tooltip, DropdownMenu, Popover. - ---- - -## Technical Considerations - -- **IPC:** ✅ `electron-trpc`. Define a single `appRouter` in main process exposing all domains (`tasks`, `projects`, `clients`, `checkpoints`, `notes`, `ai`). Renderer uses `trpc.[domain].[procedure].useQuery/useMutation()`. Zod validates all inputs at the boundary. -- GitHub Copilot SDK may require enterprise SSO token; provide a settings screen for token input (US not in MVP scope, but infrastructure must exist). -- LanceDB Node.js binding (`vectordb` package) runs in main process only. -- Milkdown v7+ with React adapter. Plugin list: `commonmark`, `history`, `clipboard`, `math` (optional). -- Use `electron-store` or `conf` for lightweight app settings (user name for greeting, sidebar collapsed state, etc.). - ---- - -## Success Metrics - -- All 5 sections navigable and functional with real SQLite-persisted data. -- Fluid Curtain animation runs at 60fps with no layout shift on return. -- @ProjectAgent correctly scopes a context query (zero responses sourcing from another project). -- Note embedding + LanceDB retrieval returns relevant results for a simple semantic query. -- App cold-start < 3 seconds on a modern machine. - ---- - -## Open Questions - -1. ~~**Gantt library vs. custom SVG?**~~ ✅ Resolved: custom SVG component. -2. ~~**GitHub Copilot SDK auth flow:**~~ ✅ Resolved: `keytar` (OS keychain). A minimal "Settings" screen for token input writes to keychain on save. -3. ~~**IPC architecture:**~~ ✅ Resolved: `electron-trpc` with Zod validation. All DB/AI operations exposed as tRPC procedures in main process; renderer uses typed React Query hooks. -4. **Milkdown vs. simpler editor:** Milkdown is powerful but has a learning curve. Is a simpler `CodeMirror`-based Markdown editor acceptable for MVP? -5. **"Fluid Curtain" on Linux:** Overscroll behavior differs across OS/window managers. What's the fallback trigger (keyboard shortcut only)? - ---- - -## Implementation Roadmap - -### Phase 1 — Foundation (US-001 → US-010) - -| Step | Story | Key Decision Point | -|------|-------|-------------------| -| 1.1 | US-001: Electron+React scaffold | ✅ **electron-forge + Vite plugin** (`npm init electron-app@latest -- --template=vite-typescript`) | -| 1.2 | US-002: SQLite + Drizzle setup | Schema finalized; migrations strategy | -| 1.3 | US-003: App shell + sidebar | ✅ **TanStack Router** (fully type-safe, `$projectId` params typed) | -| 1.4 | US-004 + US-005: Client & Project CRUD | Data model confirmed | -| 1.5 | US-006: Projects tree view | ✅ **Radix Collapsible + recursive `TreeNode`** (no extra dep, matches Figma) | -| 1.6 | US-007: Tasks global view | | -| 1.7 | US-008: Manual Timeline / Gantt | ✅ **Custom SVG component** (dot-on-axis, zero deps, matches Figma exactly) | -| 1.8 | US-009: Project Detail view | ✅ **@hello-pangea/dnd** for Kanban drag-and-drop | -| 1.9 | US-010: Milkdown editor | Plugin scope for MVP | - -### Phase 2 — The Curtain & Agents (US-011 → US-014) - -| Step | Story | Key Decision Point | -|------|-------|-------------------| -| 2.1 | US-011: Fluid Curtain animation | ✅ Wheel overscroll-up at `scrollTop=0` + `Cmd/Ctrl+K` shortcut. Right-edge label is visual-only (not a button). Framer Motion spring (`y` to viewport height). | -| 2.2 | US-012: Context-scoped chat UI | Chat bubble components, streaming UI | -| 2.3 | US-013: Copilot SDK + @Orchestrator | ✅ **`keytar`** for OS keychain token storage (main process only, IPC to renderer). | -| 2.4 | US-014: @ProjectAgent tools | Tool schema definition + SQLite write-back | - -### Phase 3 — Intelligence & RAG (US-015 → US-018) - -| Step | Story | Key Decision Point | -|------|-------|-------------------| -| 3.1 | US-015: LanceDB setup + embedding | ✅ **GitHub Copilot SDK embeddings** (`text-embedding-3-small`). Chunk notes by paragraph (~500 tokens). | -| 3.2 | US-016: @KnowledgeAgent search | Vector search tuning, k=5 default | -| 3.3 | US-017: AI checkpoint suggestions | Prompt engineering for date extraction | -| 3.4 | US-018: Home daily brief | Orchestrator routing for daily summary | - diff --git a/.claude/prd.json b/.claude/prd.json deleted file mode 100644 index adc082a..0000000 --- a/.claude/prd.json +++ /dev/null @@ -1,461 +0,0 @@ -{ - "project": "Adiuva", - "branchName": "ralph/adiuva-mvp", - "description": "Adiuva MVP — Local-first desktop workspace with hierarchical project management, Fluid Curtain AI chat overlay, and multi-agent intelligence via GitHub Copilot SDK", - "userStories": [ - { - "id": "US-001", - "title": "Electron + React scaffold", - "description": "As a developer, I need a working Electron app with React+TypeScript and a shared main/renderer process setup so that all other features have a platform to run on.", - "acceptanceCriteria": [ - "electron-forge + Vite plugin scaffold with hot-reload in dev", - "Main process opens a BrowserWindow serving the React app", - "TypeScript strict mode enabled, tsconfig.json configured", - "package.json scripts: dev, build, preview", - "App opens without errors", - "Typecheck passes" - ], - "priority": 1, - "passes": true, - "notes": "Completed in initial scaffold commit (f6cc8bb)" - }, - { - "id": "US-002", - "title": "SQLite + Drizzle ORM schema and migrations", - "description": "As a developer, I need the SQLite database initialized with Drizzle ORM so that all CRUD operations use a typed, schema-driven interface.", - "acceptanceCriteria": [ - "better-sqlite3 and drizzle-orm installed as main-process dependencies", - "Drizzle schema file defines all 5 tables matching the PRD exactly: clients (id, parentId, name, industry, createdAt), projects (id, clientId, name, status, aiSummary, createdAt), tasks (id, projectId, title, description, status, priority, assignee, dueDate, createdAt), checkpoints (id, projectId, title, date, isAiSuggested, isApproved, createdAt), notes (id, projectId, title, content, createdAt, updatedAt)", - "DB file created at app.getPath('userData')/adiuva.db on startup", - "Migration runs automatically on app start (drizzle-kit migrate or push); never destructive", - "All IDs are UUIDs generated via crypto.randomUUID()", - "TypeScript types inferred from schema with no manual type duplication", - "Typecheck passes" - ], - "priority": 2, - "passes": true, - "notes": "Completed: better-sqlite3 + drizzle-orm, 5-table schema, non-destructive push migration via CREATE TABLE IF NOT EXISTS, WAL mode enabled" - }, - { - "id": "US-003", - "title": "electron-trpc IPC bridge and appRouter scaffold", - "description": "As a developer, I need electron-trpc wired up between main and renderer processes so that all DB and AI operations are exposed as type-safe tRPC procedures callable from the renderer.", - "acceptanceCriteria": [ - "electron-trpc installed; IPC bridge configured in main process and preload script", - "appRouter defined in main process with stub routers for all domains: clients, projects, tasks, checkpoints, notes, ai", - "Renderer-side trpc client created with the correct IPC link and wrapped in TRPCProvider + QueryClientProvider", - "A health.ping procedure returns 'pong' and is successfully callable from the renderer (verified via console log or React component)", - "Zod imported and used to validate at least one procedure input", - "Typecheck passes" - ], - "priority": 3, - "passes": true, - "notes": "Completed: electron-trpc IPC bridge, appRouter with stub routers for all 7 domains (health, clients, projects, tasks, checkpoints, notes, ai), renderer TRPCProvider+QueryClientProvider, health.ping returns 'pong' displayed in HomePage, Zod validates all procedure inputs" - }, - { - "id": "US-004", - "title": "App shell layout and sidebar navigation", - "description": "As a user, I want a persistent left sidebar so that I can navigate between all sections of the app.", - "acceptanceCriteria": [ - "Sidebar renders at 240px with #fafafa background and 1px right border (#e5e5e5)", - "Nav items render with Lucide icons + labels: Home (house), Timeline (chart-gantt), Tasks (clipboard-check), Projects (folder-kanban)", - "Active route highlights nav item with #f5f5f5 accent background (no extra border)", - "Collapse button at bottom toggles sidebar to icon-only mode (64px wide); state persisted via electron-store", - "TanStack Router renders the correct view component for each nav route: /, /timeline, /tasks, /projects", - "Right-edge vertical rotated label 'keep scrolling for AI' with chevron-down icon is visible in every section as a non-interactive visual affordance", - "Geist font applied globally via @fontsource/geist", - "Global CSS variables set: bg=#ffffff, foreground=#0a0a0a, muted=#737373, border=#e5e5e5, sidebar=#fafafa, sidebar-accent=#f5f5f5, primary=#171717, primary-fg=#fafafa", - "Typecheck passes", - "Verify in browser using dev-browser skill" - ], - "priority": 4, - "passes": true, - "notes": "Completed: electron-store@8 sidebar collapse persistence via settings tRPC router, @fontsource/geist replacing Google Fonts CDN, right-edge 'keep scrolling for AI' label in all views, ESLint fixed with eslint-import-resolver-typescript" - }, - { - "id": "US-005", - "title": "Client tRPC procedures (CRUD)", - "description": "As a developer, I need tRPC procedures for client and sub-client CRUD so that the UI can create, read, update, and delete hierarchical client records.", - "acceptanceCriteria": [ - "clients.list returns all clients ordered by name", - "clients.create accepts { name: string, parentId?: string, industry?: string } and inserts with UUID and createdAt timestamp", - "clients.update accepts { id: string, name?: string, industry?: string } and updates the record", - "clients.delete accepts { id: string } and returns an error payload if the client has child clients or projects (does not delete)", - "clients.deleteWithCascade accepts { id: string } and deletes the client, all descendant clients, and their projects (nulls projectId on orphaned tasks)", - "All inputs validated with Zod schemas", - "Typecheck passes" - ], - "priority": 5, - "passes": true, - "notes": "Completed: clients.list (ordered by name), clients.create (UUID + createdAt), clients.update (partial), clients.delete (guard returns error payload if children exist), clients.deleteWithCascade (BFS recursive — nulls orphaned tasks.projectId, deletes projects, then clients). All queries use .all()/.run() for drizzle better-sqlite3 sync driver." - }, - { - "id": "US-006", - "title": "Project tRPC procedures (CRUD)", - "description": "As a developer, I need tRPC procedures for project CRUD so that the UI can create, read, update, and archive projects attached to clients or as standalone.", - "acceptanceCriteria": [ - "projects.list accepts optional { clientId?: string, includeArchived?: boolean } and returns matching projects", - "projects.listAll returns all projects (for dropdowns) with id and name only", - "projects.get accepts { id: string } and returns the full project record", - "projects.create accepts { name: string, clientId?: string } and inserts with UUID, status='active', createdAt", - "projects.update accepts { id: string, name?: string, clientId?: string, status?: 'active'|'archived', aiSummary?: string }", - "projects.delete accepts { id: string } and deletes the project (nulls projectId on its tasks)", - "All inputs validated with Zod", - "Typecheck passes" - ], - "priority": 6, - "passes": true, - "notes": "Completed: projects.list (filters by clientId + includeArchived via drizzle and()), projects.listAll (id+name only), projects.get (returns null if not found), projects.create (UUID + status='active' + createdAt), projects.update (partial set object), projects.delete (nulls tasks.projectId then deletes project)" - }, - { - "id": "US-007", - "title": "Task tRPC procedures (CRUD + filtering)", - "description": "As a developer, I need tRPC procedures for task CRUD with search and filter support so that the global Tasks view can query tasks efficiently.", - "acceptanceCriteria": [ - "tasks.list accepts { projectId?: string, status?: 'todo'|'in_progress'|'done', search?: string, orderBy?: 'dueDate'|'priority'|'createdAt' } and returns matching tasks", - "tasks.list joins with projects and clients tables to return breadcrumb fields: projectName, clientName, subClientName", - "tasks.create accepts { title: string, description?: string, status?: string, priority?: string, assignee?: string, dueDate?: number, projectId?: string }", - "tasks.update accepts { id: string, ...partial task fields }", - "tasks.delete accepts { id: string }", - "All inputs validated with Zod", - "Typecheck passes" - ], - "priority": 7, - "passes": true, - "notes": "Completed: tasks.list (LEFT JOIN projects+clients+parentClients for breadcrumb fields, and() filters for projectId/status/search via like(), orderBy CASE for priority), tasks.create (UUID + createdAt + defaults), tasks.update (partial set), tasks.delete. alias() from drizzle-orm/sqlite-core for self-join." - }, - { - "id": "US-008", - "title": "Checkpoint and Note tRPC procedures (CRUD)", - "description": "As a developer, I need tRPC procedures for checkpoints and notes so that the timeline and notes features have a typed backend.", - "acceptanceCriteria": [ - "checkpoints.list accepts { projectId?: string } and returns matching checkpoints ordered by date", - "checkpoints.create accepts { projectId: string, title: string, date: number, isAiSuggested?: number, isApproved?: number }", - "checkpoints.update accepts { id: string, title?: string, date?: number, isApproved?: number }", - "checkpoints.delete accepts { id: string }", - "notes.list accepts { projectId?: string } and returns notes with id, title, createdAt, updatedAt — not content (for performance)", - "notes.get accepts { id: string } and returns the full note including content", - "notes.create accepts { title: string, content: string, projectId?: string }", - "notes.update accepts { id: string, title?: string, content?: string } and always updates updatedAt", - "notes.delete accepts { id: string }", - "All inputs validated with Zod", - "Typecheck passes" - ], - "priority": 8, - "passes": true, - "notes": "Completed: checkpoints.list (ordered by date, optional projectId filter), checkpoints.create (UUID + createdAt + defaults for isAiSuggested/isApproved), checkpoints.update (partial set), checkpoints.delete. notes.list (returns id/projectId/title/createdAt/updatedAt — no content), notes.get (full record or null), notes.create (UUID + createdAt + updatedAt), notes.update (partial set, always updates updatedAt), notes.delete." - }, - { - "id": "US-009", - "title": "Project CRUD UI in Projects sidebar", - "description": "As a user, I want to create, rename, and delete Projects from the Projects sidebar, where each project can optionally belong to a Client or Sub-Client.", - "acceptanceCriteria": [ - "'New Project' button at top of Projects sidebar uses shadcn/ui Button component; creates a new project via clients.create tRPC mutation", - "Each project item has a context menu using shadcn/ui DropdownMenu (triggered by kebab icon) with items: Rename, Delete", - "Rename activates an inline editable field (shadcn/ui Input) replacing the label; pressing Enter or blurring saves via clients.update", - "Delete shows a shadcn/ui AlertDialog confirmation; if the project has sub-projects, warns the user and offers cascade-delete option", - "Each project can optionally be associated with a Client or Sub-Client", - "Tree updates immediately after any mutation without full page reload", - "All interactive elements use shadcn/ui primitives: install via 'npx shadcn@latest add button input dropdown-menu alert-dialog' before implementing", - "Typecheck passes", - "Verify in browser using dev-browser skill" - ], - "priority": 9, - "passes": true, - "notes": "Completed: ProjectSidebar component with New Project button (clients.create), kebab context menu (DropdownMenu with Rename/Delete/New Sub-Project), inline rename (Input with Enter/Escape/blur), AlertDialog delete with cascade-warn flow, collapsible tree with parent-child hierarchy, empty state. All shadcn/ui primitives: Button, Input, DropdownMenu, AlertDialog, Collapsible. Typecheck passes." - }, - { - "id": "US-010", - "title": "Projects sidebar tree view and project detail routing", - "description": "As a user, I want to see all projects in a collapsible tree in the sidebar, optionally grouped by client, and manage them from the Projects section.", - "acceptanceCriteria": [ - "Tree renders projects as a flat list with folder icons; projects with a client show the client name as a grouping header; use shadcn/ui Collapsible for expand/collapse groups", - "Search input at the top of the Projects sidebar uses shadcn/ui Input; filters the tree in real-time by project name", - "Projects with no client appear under an 'Internal / No Client' group", - "Project context menu uses shadcn/ui DropdownMenu with items: Edit (assign/change client), Archive/Unarchive, Delete", - "Archived projects hidden by default; a shadcn/ui Switch or toggle reveals them", - "Clicking a project node loads the Project Detail panel in the right pane", - "Active project highlighted in tree", - "Install shadcn/ui components via 'npx shadcn@latest add collapsible dialog select switch' before implementing", - "Typecheck passes", - "Verify in browser using dev-browser skill" - ], - "priority": 10, - "passes": true, - "notes": "Completed: ProjectSidebar reworked to show projects grouped by client (Collapsible groups), 'Internal / No Client' group for orphan projects, real-time search filter, archive toggle (Switch), context menu (Edit Client via Dialog+Select, Archive/Unarchive, Delete with AlertDialog), click selects project via search params, active project highlighted. ProjectDetail placeholder component. projects.update now accepts nullable clientId. shadcn/ui: dialog, select, switch installed." - }, - { - "id": "US-011", - "title": "Global Tasks view UI", - "description": "As a user, I want a global task list where I can create, filter, search, and update tasks across all projects in one place.", - "acceptanceCriteria": [ - "4 stat cards using shadcn/ui Card (Card, CardHeader, CardTitle, CardContent) at top: Total Tasks, To Do, In Progress, Completed — each with a Lucide icon and count, reactively updated via tasks.list queries", - "Search uses shadcn/ui Input; filters tasks by title or description (case-insensitive, 300ms debounce)", - "Status filter uses shadcn/ui Tabs (Tabs, TabsList, TabsTrigger): All | To Do | In Progress | Completed", - "'Order by' uses shadcn/ui DropdownMenu: Due Date | Priority | Created Date", - "Task rows display: shadcn/ui Checkbox, title (bold 14px), description (muted 14px), priority chip using shadcn/ui Badge (HIGH=destructive variant, MEDIUM=secondary variant, LOW=outline variant with green), due date chip (calendar icon + 'Due Mon DD'), breadcrumb (Client > Sub-Client > Project, chevron-separated via shadcn/ui Breadcrumb if available), assignee (person icon + name)", - "Completed task rows have green-tinted background (#f0fdf4 or similar)", - "Clicking the shadcn/ui Checkbox calls tasks.update to set status='done' (or back to 'todo') immediately", - "'New Task' shadcn/ui Button opens a shadcn/ui Dialog modal: shadcn/ui Input for title (required), Textarea for description, Select for priority, Popover+Calendar for due date, Select for project (optional searchable), Input for assignee (optional)", - "Install shadcn/ui components via 'npx shadcn@latest add card tabs checkbox badge dialog textarea select popover calendar' before implementing", - "Typecheck passes", - "Verify in browser using dev-browser skill" - ], - "priority": 11, - "passes": true, - "notes": "Completed: Global Tasks view with 4 stat cards (Card, CardHeader, CardTitle, CardContent), search with 300ms debounce (Input + Search icon), status filter tabs (Tabs/TabsList/TabsTrigger: All/To Do/In Progress/Completed), Order by dropdown (DropdownMenu: Due Date/Priority/Created Date), task rows with Checkbox toggle (todo↔done), priority Badge (destructive/secondary/outline variants), due date chip, breadcrumb (Client > Sub-Client > Project), assignee. Completed rows green-tinted. NewTaskDialog component with title Input, Textarea description, Select priority/status, Popover+Calendar due date, Select project, Input assignee. All shadcn/ui primitives installed: card, tabs, checkbox, badge, textarea, popover, calendar." - }, - { - "id": "US-012", - "title": "GanttChart SVG component and global Timeline view", - "description": "As a user, I want to view and create timeline checkpoints on a Gantt-style view so that I have full control over project milestones.", - "acceptanceCriteria": [ - "Reusable GanttChart component accepts { checkpoints: Checkpoint[], startDate: Date, endDate: Date } props", - "Component renders a custom SVG: month labels on the X axis, a horizontal baseline , and dots for each checkpoint positioned by date", - "Dot fill: dark (#171717) = isApproved=1 + status todo, green (#16a34a) = done/approved, dashed outline = isApproved=0 (pending AI suggestion)", - "A vertical 'Today' marker line rendered at the current date", - "Component uses ResizeObserver for responsive SVG width", - "Clicking a checkpoint dot opens a shadcn/ui Popover with: title, formatted date, and a shadcn/ui Button (variant=destructive, size=sm) for Delete (calls checkpoints.delete)", - "Global Timeline route (/timeline) renders GanttChart with all checkpoints from all projects, color-coded or grouped by project", - "'+ Add' shadcn/ui Button opens a shadcn/ui Dialog: shadcn/ui Input for title (required), Popover+Calendar for date picker (required), Select for project dropdown (required in global view)", - "Install shadcn/ui components via 'npx shadcn@latest add popover calendar' before implementing (button, dialog, input, select already installed)", - "Typecheck passes", - "Verify in browser using dev-browser skill" - ], - "priority": 12, - "passes": true, - "notes": "Completed: Reusable GanttChart SVG component with month labels, baseline, ResizeObserver responsive width, Today marker (red line), checkpoint dots (dark=#171717 for future/approved, green=#16a34a for past/approved, dashed outline for pending AI suggestions). Popover on dot click shows title, date, project name, delete button. Global Timeline route (/timeline) renders all checkpoints from all projects with project name lookup via Map. AddCheckpointDialog with title Input, Popover+Calendar date, Select project. Legend showing dot types." - }, - { - "id": "US-013", - "title": "Project Detail view — layout, breadcrumb, stat cards, AI summary", - "description": "As a user, I want a project detail panel showing breadcrumb navigation, project name, stat cards, and an AI summary card.", - "acceptanceCriteria": [ - "Right panel renders when a project is selected in the Projects tree", - "Breadcrumb at top uses shadcn/ui Breadcrumb (Breadcrumb, BreadcrumbList, BreadcrumbItem, BreadcrumbSeparator) showing Client > Sub-Client path", - "Project name renders as H1 below the breadcrumb", - "3 stat cards using shadcn/ui Card displayed horizontally: Notes (count from notes.list), Tasks Complete (done/total fraction from tasks.list), Checkpoints (approved/total fraction from checkpoints.list)", - "AI Project Summary card uses shadcn/ui Card with sparkle (sparkles) Lucide icon + placeholder text 'AI summary will appear here' when project.aiSummary is null/empty", - "When project.aiSummary is populated, the card displays the AI-generated text instead", - "Install shadcn/ui components via 'npx shadcn@latest add breadcrumb' before implementing (card already installed)", - "Typecheck passes", - "Verify in browser using dev-browser skill" - ], - "priority": 13, - "passes": true, - "notes": "Completed: Project Detail view with Breadcrumb (Client > Sub-Client path from clients list), H1 project name, 3 stat cards (Notes count, Tasks Complete done/total, Checkpoints approved/total), AI Project Summary card with sparkles icon showing aiSummary or placeholder text. All data fetched via tRPC queries scoped to projectId. shadcn/ui breadcrumb installed." - }, - { - "id": "US-014", - "title": "Kanban board in Project Detail", - "description": "As a user, I want a Kanban board inside the project detail view with drag-and-drop task management between status columns.", - "acceptanceCriteria": [ - "@hello-pangea/dnd installed; DragDropContext wraps 3 Droppable columns: To Do | In Progress | Completed", - "Each task card is a Draggable wrapped in a shadcn/ui Card rendering: title, description (truncated), priority as shadcn/ui Badge, due date chip, assignee string", - "Dragging a card to another column calls tasks.update({ id, status }) via tRPC and the UI updates immediately (optimistic or on success)", - "'+ Add' shadcn/ui Button (variant=ghost, size=sm) in each column header opens the shadcn/ui Dialog new-task modal with the column's status pre-selected", - "Columns show a task count in their header using shadcn/ui Badge (variant=secondary)", - "All card content uses shadcn/ui primitives: Card, Badge, Button (already installed)", - "Typecheck passes", - "Verify in browser using dev-browser skill" - ], - "priority": 14, - "passes": true, - "notes": "Completed: @hello-pangea/dnd installed, KanbanBoard component with DragDropContext wrapping 3 Droppable columns (To Do/In Progress/Completed), each task is a Draggable wrapping the shared TaskRow component (same UI as global Tasks view), drag-and-drop calls tasks.update({id, status}), '+ Add' Button (variant=ghost, size=sm) per column opens NewTaskDialog with defaultStatus pre-selected, Badge (variant=secondary) task count in each column header, EditTaskDialog for context menu editing, tasks.delete for context menu deletion. Typecheck passes." - }, - { - "id": "US-015", - "title": "Inline project timeline and notes list in Project Detail", - "description": "As a user, I want to see the project's Gantt timeline and a list of its notes within the project detail scrollable view.", - "acceptanceCriteria": [ - "Project Detail view includes a 'Project Timeline' section using the GanttChart component (from US-012) scoped to the current project's checkpoints", - "'+ Add' shadcn/ui Button (variant=outline, size=sm) in the timeline section header opens the add-checkpoint shadcn/ui Dialog with the project pre-selected", - "Notes section below Kanban shows a flat list using shadcn/ui Separator between rows: each row has note title + formatted createdAt date", - "'+ Add' shadcn/ui Button in notes header calls notes.create with a default title and navigates to /notes/:noteId", - "Clicking a note title navigates to /notes/:noteId", - "All buttons/dialogs use shadcn/ui components (already installed)", - "Typecheck passes", - "Verify in browser using dev-browser skill" - ], - "priority": 15, - "passes": true, - "notes": "Completed: Project Timeline section using GanttChart (scoped to project checkpoints) with AddCheckpointDialog (defaultProjectId hides project selector), checkpoint delete mutation. Notes section using Item cards (variant=muted) in flex-wrap grid matching Figma design, '+ Add' creates note via notes.create and navigates to /notes/$noteId, clicking note card navigates to /notes/$noteId. Route stub at notes.$noteId.tsx with back button + note title." - }, - { - "id": "US-016", - "title": "Milkdown note editor", - "description": "As a user, I want a full-screen Markdown editor for each note so that I can write rich content without leaving the app.", - "acceptanceCriteria": [ - "@milkdown/react and @milkdown/preset-commonmark installed; Milkdown editor renders at route /notes/:noteId", - "Supported Markdown: headings (H1-H6), bold, italic, inline code, code blocks, bullet lists, ordered lists, blockquotes", - "Note title editable as a shadcn/ui Input (variant borderless/ghost style) at the top of the page (separate from Milkdown content area)", - "Content auto-saves to SQLite via notes.update on Milkdown onChange event, debounced 500ms", - "Unsaved indicator shown using shadcn/ui Badge (variant=secondary, text 'Saving...') next to the title while save is pending", - "Back button uses shadcn/ui Button (variant=ghost, size=icon) with ArrowLeft Lucide icon; navigates to the previous route", - "All UI chrome uses shadcn/ui components (already installed)", - "Typecheck passes", - "Verify in browser using dev-browser skill" - ], - "priority": 16, - "passes": true, - "notes": "Completed: @milkdown/kit + @milkdown/react + @milkdown/theme-nord installed. MilkdownEditor wrapper component at src/renderer/components/notes/MilkdownEditor.tsx using official React recipe (MilkdownProvider + useEditor + Editor.make() with commonmark, listener, history plugins). Route /notes/$noteId rewritten with: editable title (borderless Input, saves on blur), Milkdown editor with auto-save (500ms debounce via useRef+setTimeout, notes.update mutation), 'Saving...' Badge (variant=secondary) shown while save pending, back button (ghost Button + ArrowLeft, window.history.back()). Editor CSS overrides in globals.css using semantic color variables (var(--foreground), var(--muted), var(--border), etc.). Typecheck passes." - }, - { - "id": "US-017", - "title": "Fluid Curtain pull-down animation", - "description": "As a user, I want to pull down from the top of any view to slide the app panel off-screen and reveal the AI chat layer beneath.", - "acceptanceCriteria": [ - "framer-motion useMotionValue + useSpring (stiffness: 300, damping: 30) controls a 'y' CSS transform on the main app panel wrapper", - "Trigger 1: wheel event listener at document level — when the current route's scroll position is at 0 and deltaY < 0 (overscroll up), animate panel y from 0 to viewport height", - "Trigger 2: Cmd/Ctrl+K keyboard shortcut toggles curtain open (y = viewport height) and closed (y = 0)", - "AI chat view is rendered as a fixed full-screen layer behind the sliding panel and becomes fully visible when panel slides down", - "App panel remains mounted during animation (no unmount/remount, no state loss)", - "Returning from chat: wheel event with deltaY > 0 at chat-bottom OR Cmd/Ctrl+K slides panel back to y = 0", - "Right-edge vertical 'keep scrolling for AI' label with chevron-down is visible in every section (non-interactive, visual hint only)", - "Typecheck passes", - "Verify in browser using dev-browser skill" - ], - "priority": 17, - "passes": true, - "notes": "Completed: Fluid Curtain animation using framer-motion useMotionValue + useSpring (stiffness: 300, damping: 30) on a motion.div wrapping the content area inside SidebarInset. Sidebar stays visible. Trigger 1: wheel overscroll-up (deltaY < 0 at scrollTop=0) opens curtain (y → viewport height). Trigger 2: Cmd/Ctrl+K toggles. Closing: deltaY > 0 while open or Cmd/Ctrl+K. AIChatPanel placeholder rendered as absolute z-0 layer behind content. Right-edge label dynamically shows 'scrolling up for Adiuva' (closed) or 'back to app' (open) with matching chevron direction. App panel stays mounted (no state loss). Typecheck passes." - }, - { - "id": "US-018", - "title": "GitHub Copilot SDK setup and keytar token storage", - "description": "As a developer, I need the GitHub Copilot SDK initialized in the main process with secure OS keychain token storage so that AI features can authenticate.", - "acceptanceCriteria": [ - "keytar installed and imported in main process only (not renderer)", - "ai.setToken tRPC mutation accepts { token: string } and stores it via keytar.setPassword('adiuva', 'copilot-token', token)", - "ai.hasToken tRPC query returns a boolean indicating whether a token is stored", - "On app start, main process reads the token from keychain and initializes the GitHub Copilot SDK client", - "Settings dialog uses shadcn/ui Dialog (DialogTrigger as a SidebarMenuButton with Settings/gear icon in the sidebar footer); dialog content uses shadcn/ui Input for token paste + shadcn/ui Button to save via ai.setToken", - "If no token is stored, AI-dependent features display a prompt using shadcn/ui Card with a shadcn/ui Button linking to the Settings dialog instead of throwing an error", - "Typecheck passes" - ], - "priority": 18, - "passes": true, - "notes": "Provider-agnostic architecture: AIProvider interface + registry in src/main/ai/provider.ts allows swapping AI backends (Copilot, OpenAI, Anthropic, etc.) without changing token storage or UI. Token keyed by provider name in OS keychain via keytar." - }, - { - "id": "US-019", - "title": "@Orchestrator agent with intent routing", - "description": "As a developer, I need an Orchestrator agent that receives user messages, reads intent, and routes to the correct specialist agent.", - "acceptanceCriteria": [ - "ai.chat tRPC procedure accepts { message: string, context: { type: 'global' | 'project', projectId?: string } }", - "@Orchestrator system prompt instructs the model to use one of three routing tool calls: route_to_project (when context is a project), route_to_knowledge (for cross-project questions), or route_to_general (for everything else)", - "For project-scoped context, Orchestrator invokes @ProjectAgent logic and returns its response", - "For global context, Orchestrator answers from task/project summaries directly", - "Streaming tokens returned to renderer incrementally (use tRPC subscription or async generator pattern)", - "SDK auth errors and timeouts caught; procedure returns { error: string } for user-facing display", - "Typecheck passes" - ], - "priority": 19, - "passes": true, - "notes": "Completed: LangGraph-based provider-independent orchestrator. StateGraph with classifyIntent node (structured output via withStructuredOutput(zodSchema) for route classification) + 3 specialist agent nodes (projectAgent, knowledgeAgent, generalAgent) connected via addConditionalEdges. LLM factory (src/main/ai/llm.ts) returns BaseChatModel per provider: ChatOpenAI (openai), ChatAnthropic (anthropic), ChatCopilot adapter (copilot). ChatCopilot (src/main/ai/chat-copilot.ts) wraps @github/copilot-sdk in SimpleChatModel with _call() and _streamResponseChunks(). Streaming via LangGraph streamMode:'messages' + IPC side-channel (ai:stream). Context assembly from DB (buildProjectContext/buildGlobalContext), tRPC context with event.sender, auth/timeout error handling. Typecheck passes." - }, - { - "id": "US-020", - "title": "Context-scoped AI chat UI", - "description": "As a user, I want the AI chat (revealed by the Fluid Curtain) to display a context header, support message input, and stream AI responses.", - "acceptanceCriteria": [ - "Chat panel shows a context header using shadcn/ui Badge (variant=outline): 'Chatting about: [Project Name]' when opened from a project detail view, or 'Global workspace' when opened from other sections", - "Chat input box uses shadcn/ui Textarea: white background, border #d4d4d4, shadow-lg, min-height 109px, placeholder 'Ask me anything...'; Send uses shadcn/ui Button (black bg, Send Lucide icon + 'Send' label) anchored bottom-right", - "User messages appear as right-aligned message bubbles using shadcn/ui Card; AI responses as left-aligned Cards", - "Streaming: AI response tokens appended to the current AI bubble as they arrive from ai.chat", - "A loading spinner or pulsing indicator (shadcn/ui Skeleton) shown while waiting for first token", - "If ai.chat returns { error }, display the error message in a shadcn/ui Card with destructive border styling", - "Chat history is session-only — cleared when the curtain closes or the app restarts", - "Install shadcn/ui components via 'npx shadcn@latest add textarea' before implementing (card, badge, button, skeleton already installed)", - "Typecheck passes", - "Verify in browser using dev-browser skill" - ], - "priority": 20, - "passes": true, - "notes": "Completed: Full chat UI in AIChatPanel with context header Badge, user Card bubbles, AI plain text (Sparkles+Adiuva header), streaming via IPC, Skeleton loading, error Cards, session-only history cleared on curtain close" - }, - { - "id": "US-021", - "title": "@ProjectAgent with project action tools", - "description": "As a user, I want the AI to answer project-specific questions and take actions like adding tasks, summarizing the project, and suggesting checkpoints.", - "acceptanceCriteria": [ - "read_project_notes tool: fetches all notes for the scoped projectId from SQLite and returns combined content to the model", - "add_task tool: creates a task in the project via tasks.create and confirms with 'Task added: [title]' in the chat response", - "get_summary tool: calls the SDK to generate a 2-3 sentence summary of the project based on its notes and tasks, then calls projects.update to persist the result in project.aiSummary", - "suggest_checkpoints tool: returns a JSON array of { title: string, date: string } proposed checkpoints based on date-anchored commitments found in notes", - "@Orchestrator routes project-context messages to @ProjectAgent", - "Typecheck passes" - ], - "priority": 21, - "passes": true, - "notes": "Completed: 4 project action tools (read_project_notes, add_task, get_summary, suggest_checkpoints) built per-invocation inside projectAgent using @langchain/core/tools tool() helper. Explicit agent loop with MAX_ITERATIONS=5 safety ceiling. AIMessage.isInstance() type guard for safe tool_calls access. StructuredTool[] return type + non-null assertion for bindTools after runtime guard. Tool dispatch via { ...toolCall, type: 'tool_call' } pattern (matches LangGraph ToolNode convention). Feature-detect bindTools at runtime — Copilot falls back to context-based response. classifyIntent short-circuits to route:'project' when chatContext.type==='project'. get_summary and suggest_checkpoints use nested getLLM().invoke() calls. Typecheck passes." - }, - { - "id": "US-022", - "title": "LanceDB vector store setup and note embedding pipeline", - "description": "As a developer, I need notes embedded into LanceDB so that semantic search across all project notes is possible.", - "acceptanceCriteria": [ - "vectordb (LanceDB Node.js binding) installed and initialized in main process only; vector DB stored at app.getPath('userData')/vectors/", - "After notes.create or notes.update, note content is embedded via the GitHub Copilot SDK embeddings endpoint and upserted in LanceDB with metadata: { noteId, projectId, content }", - "On first app startup, a migration routine checks if the 'notes' LanceDB table exists; if not, embeds all existing SQLite notes and populates LanceDB", - "Embedding errors are caught and logged to console (console.error) but do not reject the notes.update/create promise", - "Typecheck passes" - ], - "priority": 22, - "passes": true, - "notes": "Completed: vectordb npm package installed. src/main/ai/embeddings.ts reads GitHub Copilot OAuth token from ~/.copilot/config.json (falls back to stored OpenAI token) and calls OpenAIEmbeddings with Copilot base URL or standard OpenAI. src/main/db/vectordb.ts: initVectorDb() singleton, upsertNoteEmbedding() with delete-then-add strategy (table created on first upsert from schema inferred record), migrateNotesIfNeeded() embeds all SQLite notes sequentially on first launch. notes.create and notes.update made async with fire-and-forget .catch() embed calls; update re-fetches full note from SQLite to embed current title+content. initVectorDb().then(migrateNotesIfNeeded) chained in app.ready after initAI(). vectordb externalized in vite.main.config.mts. Typecheck passes (tsc --noEmit clean). Note: @github/copilot-sdk has no embeddings API; embeddings use @langchain/openai's OpenAIEmbeddings pointed at https://api.githubcopilot.com with the CLI OAuth token." - }, - { - "id": "US-023", - "title": "@KnowledgeAgent semantic search across all projects", - "description": "As a user, I want to ask questions that retrieve semantically relevant content from all past project notes regardless of project scope.", - "acceptanceCriteria": [ - "vector_search_all tool accepts a query string and performs a LanceDB similarity search returning the top-5 results", - "Each result includes: noteId, note title (joined from SQLite), projectId, project name (joined from SQLite), and matched text excerpt", - "@Orchestrator routes cross-project knowledge queries to @KnowledgeAgent", - "Chat response includes inline citations in the format: 'From: [Project Name] — [Note Title]'", - "Typecheck passes" - ], - "priority": 23, - "passes": true, - "notes": "Completed: searchNotes() in vectordb.ts performs LanceDB similarity search (embedText query → table.search().limit(5).execute()). buildKnowledgeTools() in orchestrator.ts defines vector_search_all tool with Zod schema, joins SQLite for note title + project name, returns formatted results with 'From: [Project Name] — [Note Title]' citations. knowledgeAgent() rewritten as full tool-calling agent loop (mirrors projectAgent pattern: TOOL_CALLING_PROVIDERS check, bindTools, 5-iteration loop with ToolMessage accumulation). makeKnowledgeAgentPrompt() updated with tool docs and citation format instructions. Typecheck passes." - }, - { - "id": "US-024", - "title": "AI checkpoint suggestions UI", - "description": "As a user, I want the AI to suggest timeline checkpoints from my meeting notes, which I can approve or reject directly in the timeline.", - "acceptanceCriteria": [ - "'Suggest checkpoints' shadcn/ui Button (variant=outline, sparkles Lucide icon) in the Project Detail timeline header calls ai.chat with a suggest_checkpoints intent for the current project", - "Suggested checkpoints returned by @ProjectAgent are inserted into the checkpoints table via checkpoints.create with isAiSuggested=1, isApproved=0", - "Pending suggestions appear as shadcn/ui Card components (with dashed border via className 'border-dashed') above or below the GanttChart in the Project Detail timeline section", - "'Approve' shadcn/ui Button (variant=default, size=sm) on each card calls checkpoints.update({ id, isApproved: 1 }); the checkpoint then appears as a normal dot on the Gantt", - "'Reject' shadcn/ui Button (variant=ghost, size=sm) calls checkpoints.delete({ id }) and removes the card", - "All UI uses shadcn/ui components (already installed)", - "Typecheck passes", - "Verify in browser using dev-browser skill" - ], - "priority": 24, - "passes": true, - "notes": "Completed: suggest_checkpoints tool in orchestrator.ts now persists suggestions to DB (isAiSuggested=1, isApproved=0). New suggest_tasks tool added with same pattern. ProjectDetail.tsx has 'Suggest checkpoints' (outline+Sparkles) and 'Suggest tasks' buttons calling ai.chat. Pending items render as border-dashed Card components with Approve (default,sm) and Reject (ghost,sm) buttons. Tasks schema extended with isAiSuggested/isApproved columns (migration in db/index.ts). KanbanBoard and tasks route filter out unapproved AI suggestions. Typecheck passes." - }, - { - "id": "US-025", - "title": "Home dashboard — AI daily brief and suggestion chips", - "description": "As a user, I want the Home screen to greet me with an AI-generated daily brief and pre-populated suggestion chips for quick queries.", - "acceptanceCriteria": [ - "Greeting rendered as '✦ Hello, {name}' in Geist Semibold 30px with -1px letter-spacing; name sourced from electron-store (defaults to 'there' if not set)", - "Top-right corner stat chip uses shadcn/ui Badge (variant=secondary) showing 'N Task due' where N = count of tasks with dueDate on or before end of today", - "On app open, ai.chat called with global context to generate a daily brief paragraph highlighting tasks due today/this week and recent project activity", - "Brief displayed below greeting in a shadcn/ui Card; bold key phrases rendered as (model wraps them in **markdown bold**)", - "4 suggestion chips rendered in a 4-column flex row below the chat box using shadcn/ui Button (variant=outline); each chip has a Lucide icon + short prompt text", - "Clicking a suggestion chip populates the chat input with the chip's prompt text", - "Chat box uses shadcn/ui Textarea: white bg, border #d4d4d4, shadow-lg, min-height 109px, placeholder 'Ask me anything...'; Send uses shadcn/ui Button (black bg) bottom-right", - "All UI uses shadcn/ui components (already installed)", - "Typecheck passes", - "Verify in browser using dev-browser skill" - ], - "priority": 25, - "passes": true, - "notes": "" - } - ] -} diff --git a/.claude/progress.txt b/.claude/progress.txt deleted file mode 100644 index e9ce6de..0000000 --- a/.claude/progress.txt +++ /dev/null @@ -1,591 +0,0 @@ -## Codebase Patterns -- `alias(table, 'alias_name')` from `drizzle-orm/sqlite-core` enables self-joins (e.g., clients → parentClients for hierarchy) -- `sql\`CASE WHEN ... THEN ... ELSE ... END\`` for conditional SELECT fields (e.g., clientName vs subClientName based on parentId) -- `or(like(col1, pattern), like(col2, pattern))` for multi-column search; SQLite LIKE on NULL columns safely returns NULL (falsy) so OR is safe -- Vite configs use `.mts` extension (not `.ts`) to avoid ESM/CJS conflict with electron-forge's externalize-deps plugin -- electron-trpc uses `exposeElectronTRPC()` in preload and `createIPCHandler({ router, windows })` in main; renderer uses `ipcLink()` from `electron-trpc/renderer` -- appRouter lives at `src/main/router/index.ts`; renderer client at `src/renderer/lib/trpc.ts` -- `@/*` path alias maps to `src/renderer/*` (configured in tsconfig.json paths) -- Drizzle ORM with better-sqlite3 (sync driver): SELECT queries MUST end with `.all()` to execute; INSERT/UPDATE/DELETE MUST end with `.run()` -- `inArray(column, values)` works with nullable columns when values is `string[]` (TypeScript covariance allows string[] → (string | null)[]) -- All DB tables use `CREATE TABLE IF NOT EXISTS` for non-destructive migrations -- All IDs are UUIDs generated via `crypto.randomUUID()` -- TypeScript strict mode + noUncheckedIndexedAccess enabled; always account for possible undefined on array access -- electron-store@8 (CJS) used for app settings; use lazy init pattern `getStore()` like `getDb()` to avoid calling before app ready -- ESLint uses `eslint-import-resolver-typescript` to resolve `@/*` aliases; configured in `.eslintrc.json` under `settings.import/resolver` -- App settings (sidebar state, etc.) exposed via `settings` tRPC sub-router for type-safe renderer access -- `z.string().nullable().optional()` in tRPC inputs enables three-state semantics: undefined = don't change, null = clear, string = set value -- NewTaskDialog component at `src/renderer/components/tasks/NewTaskDialog.tsx` accepts `defaultProjectId` and `defaultStatus` props for reuse in Kanban column "+ Add" buttons -- `date-fns` is available as a transitive dependency of `react-day-picker` (shadcn/ui calendar) -- GanttChart component at `src/renderer/components/timeline/GanttChart.tsx` is reusable: accepts `defaultProjectId` to scope to a project (for US-015 inline timeline) -- AddCheckpointDialog at `src/renderer/components/timeline/AddCheckpointDialog.tsx` accepts `defaultProjectId` — hides project select when provided -- TanStack Router `validateSearch` with Zod schema for passing selected-item IDs via URL search params (e.g., `?projectId=...`) ---- - -## 2026-02-19 - US-002 -- Installed `better-sqlite3`, `drizzle-orm` (runtime) and `@types/better-sqlite3`, `drizzle-kit` (dev) -- Created `src/main/db/schema.ts`: 5 tables (clients, projects, tasks, checkpoints, notes) with exported InferSelectModel/InferInsertModel types -- Created `src/main/db/index.ts`: `initDb()` opens/creates `adiuva.db` at `app.getPath('userData')`, runs CREATE TABLE IF NOT EXISTS (non-destructive), enables WAL mode; `getDb()` singleton accessor -- Updated `src/main/index.ts`: call `initDb()` in `app.on('ready')` -- Updated `vite.main.config.mts`: externalized `better-sqlite3` -- Updated `forge.config.ts`: added `AutoUnpackNativesPlugin` -- Added `drizzle.config.ts` for drizzle-kit CLI -- Typecheck: passes with zero errors -- **Learnings for future iterations:** - - better-sqlite3 is CommonJS with native addon; Vite must NOT bundle it — always add to rollupOptions.external - - The CREATE TABLE IF NOT EXISTS approach satisfies "never destructive" and works perfectly in electron without needing migration file resolution - - electron-forge rebuilds native modules automatically on `electron-forge start`; no manual rebuild step needed - - `app.getPath('userData')` is only available after `app.on('ready')` fires — do not call earlier ---- - -## 2026-02-19 - US-008 -- What was implemented: - - Full `checkpointsRouter` replacing stubs in `src/main/router/index.ts` - - Full `notesRouter` replacing stubs in `src/main/router/index.ts` - - Added `checkpoints` and `notes` to the schema import - - `checkpoints.list`: optional `projectId` filter, ordered by `asc(checkpoints.date)` - - `checkpoints.create`: inserts with UUID, createdAt=Date.now(), defaults isAiSuggested/isApproved to 0 - - `checkpoints.update`: partial set for title/date/isApproved - - `checkpoints.delete`: deletes by id, returns `{ success: true }` - - `notes.list`: returns `{ id, projectId, title, createdAt, updatedAt }` only — no content (performance) - - `notes.get`: returns full record or null via `.all()[0] ?? null` pattern - - `notes.create`: inserts with UUID, createdAt=updatedAt=Date.now() - - `notes.update`: partial set, always sets updatedAt=Date.now() regardless of which fields changed - - `notes.delete`: deletes by id, returns `{ success: true }` -- Files changed: `src/main/router/index.ts`, `prd.json`, `progress.txt` -- **Learnings for future iterations:** - - `notes.update` must always set `updatedAt` — build the set object with updatedAt outside the conditional block - - `notes.list` intentionally excludes `content` column for performance; use `notes.get` for full record - - `checkpoints.projectId` is `.notNull()` in schema (unlike tasks.projectId which is nullable) — no null coalescing needed ---- - -## 2026-02-19 - US-003 -- What was implemented: - - Installed: electron-trpc, @trpc/server, @trpc/client, @trpc/react-query, @tanstack/react-query, zod - - Created `src/main/router/index.ts` with full appRouter: stub routers for health, clients, projects, tasks, checkpoints, notes, ai - - Updated `src/preload/index.ts` to call `exposeElectronTRPC()` - - Updated `src/main/index.ts` to call `createIPCHandler({ router: appRouter, windows: [win] })`; `createWindow()` now returns `BrowserWindow` - - Created `src/renderer/lib/trpc.ts` with `createTRPCReact()` - - Updated `src/renderer/index.tsx` to wrap app in `TRPCProvider` + `QueryClientProvider` - - Updated `src/renderer/routes/index.tsx` to call `trpc.health.ping.useQuery()` and display 'tRPC IPC bridge: pong' -- Files changed: package.json, package-lock.json, prd.json, src/main/index.ts, src/main/router/index.ts (new), src/preload/index.ts, src/renderer/index.tsx, src/renderer/lib/trpc.ts (new), src/renderer/routes/index.tsx -- **Learnings for future iterations:** - - electron-trpc `exposeElectronTRPC` is imported from `electron-trpc/main` (not a separate package) - - `ipcLink` is imported from `electron-trpc/renderer` in the renderer process - - `createTRPCReact()` requires importing the AppRouter type from the main process router — this is a type-only import so it doesn't bundle main process code into renderer - - The TRPCProvider must wrap QueryClientProvider (or be a sibling); both need the same queryClient instance - - Stub routers return empty arrays or null — they will be replaced in US-005 through US-008 ---- - -## 2026-02-19 - US-004 -- What was implemented: - - Installed: electron-store@8 (CJS-compatible, for persistent app settings), @fontsource/geist (self-hosted Geist font), eslint-import-resolver-typescript (ESLint path alias fix) - - Created `src/main/store.ts` with lazy `getStore()` pattern using electron-store - - Added `settings` tRPC sub-router with `getSidebarCollapsed` query and `setSidebarCollapsed` mutation - - Updated `src/renderer/components/layout/AppShell.tsx` to: persist sidebar collapse via tRPC, add right-edge 'keep scrolling for AI' vertical label with ChevronDown icon - - Updated `src/renderer/globals.css`: replaced Google Fonts CDN with @fontsource/geist imports (weights 400/500/600) - - Updated `index.html`: removed Google Fonts CDN links - - Updated `.eslintrc.json`: added eslint-import-resolver-typescript to fix @/* alias resolution (fixed all 7 pre-existing lint errors) -- Files changed: .eslintrc.json, index.html, package.json, package-lock.json, src/main/router/index.ts, src/main/store.ts (new), src/renderer/components/layout/AppShell.tsx, src/renderer/globals.css -- **Learnings for future iterations:** - - Use electron-store@8 (not v9+) — v9+ is ESM-only and breaks with CommonJS main process - - electron-store must NOT be initialized at module import time (before app.ready); use lazy `getStore()` like `getDb()` pattern - - For sidebar/UI state loaded from IPC: use `localState ?? queryData ?? default` pattern to avoid flash while query resolves - - @fontsource packages are the npm equivalent of Google Fonts — import weight-specific CSS files (e.g., `@fontsource/geist/400.css`) - - ESLint `import/no-unresolved` requires `eslint-import-resolver-typescript` with `alwaysTryTypes: true` to resolve TypeScript path aliases - - The `writingMode: 'vertical-rl'` + `transform: 'rotate(180deg)'` CSS pattern creates bottom-to-top text for vertical affordance labels ---- - -## 2026-02-19 - US-006 -- What was implemented: - - Full `projectsRouter` replacing stubs in `src/main/router/index.ts` - - Added `and` to drizzle-orm imports - - `projects.list`: uses `and()` with optional conditions for `clientId` filter and archived filter (defaults to active only) - - `projects.listAll`: returns only `{ id, name }` columns for dropdown use - - `projects.get`: `.all()` then `result[0] ?? null` pattern for nullable single-record lookup - - `projects.create`: inserts with UUID, status='active', createdAt=Date.now() - - `projects.update`: partial set object — only sets defined fields - - `projects.delete`: nulls `tasks.projectId` for all tasks in the project, then deletes the project -- Files changed: `src/main/router/index.ts`, `prd.json`, `progress.txt` -- **Learnings for future iterations:** - - `and(...conditions)` from drizzle-orm accepts `(SQL | undefined)[]` — pass `undefined` for optional conditions and drizzle filters them out automatically - - For nullable single-record queries: use `.all()` and `result[0] ?? null` (strict mode forbids `.get()` direct null return without this pattern) - - `and()` returns `SQL | undefined` which `.where()` accepts directly (no extra wrapping needed) - ---- - -## 2026-02-19 - US-005 -- What was implemented: - - Full clients tRPC router replacing stubs in `src/main/router/index.ts` - - Added imports: `eq`, `asc`, `inArray` from `drizzle-orm`; `getDb` from `../db`; `clients`, `projects`, `tasks` from `../db/schema` - - `clients.list`: `db.select().from(clients).orderBy(asc(clients.name)).all()` - - `clients.create`: inserts with `crypto.randomUUID()` + `Date.now()` via `.run()` - - `clients.update`: partial update — only sets fields that are defined in input, skips if no-op - - `clients.delete`: checks for child clients and child projects; returns `{ error: string }` payload if any exist; otherwise deletes and returns `{ success: true }` - - `clients.deleteWithCascade`: BFS loop collects all descendant client IDs, finds their projects, nulls `projectId` on orphaned tasks, deletes projects, then deletes all clients -- Files changed: `src/main/router/index.ts`, `prd.json`, `progress.txt` -- **Learnings for future iterations:** - - Drizzle ORM with better-sqlite3 sync driver: SELECT must call `.all()` to get an array; INSERT/UPDATE/DELETE must call `.run()` to execute — NOT calling these causes TypeScript errors (query builder ≠ result) - - `inArray(nullableColumn, string[])` is TypeScript-safe because `string[]` is assignable to `(string | null)[]` via covariance - - Guard against empty arrays before using `inArray` — while `allClientIds` is never empty (starts with input.id), `projectIds` could be empty; guarded with `if (projectIds.length > 0)` block - - `@typescript-eslint/no-non-null-assertion` is configured as a warning (not error) in this project — `queue.shift()!` is fine after a `length > 0` check ---- - -## 2026-02-19 - US-007 -- What was implemented: - - Full `tasksRouter` replacing stubs in `src/main/router/index.ts` - - Added imports: `or`, `like`, `sql` from `drizzle-orm`; `alias` from `drizzle-orm/sqlite-core` - - `tasks.list`: LEFT JOINs projects → clients → parentClients (alias for self-join); CASE WHEN for clientName/subClientName breadcrumb fields; `and()` with optional conditions for projectId/status/search; `like()` OR search on title+description; CASE expression for priority ordering - - `tasks.create`: inserts with UUID, defaults (status='todo', priority='medium'), createdAt=Date.now() - - `tasks.update`: partial set object — only sets defined fields - - `tasks.delete`: deletes by id, returns `{ success: true }` -- Files changed: `src/main/router/index.ts`, `prd.json`, `progress.txt` -- **Learnings for future iterations:** - - `alias(table, 'alias_name')` is from `drizzle-orm/sqlite-core` (NOT `drizzle-orm`) for SQLite self-joins - - `sql\`CASE WHEN ${col} IS NOT NULL THEN ${alias.col} ELSE ${col} END\`` for conditional field selection using drizzle template literals - - `or(like(col1, pattern), like(col2, pattern))` composes safely — null columns evaluate to NULL (falsy) in WHERE - - For priority ordering: `asc(sql\`CASE ${tasks.priority} WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 ELSE 4 END\`)` puts high priority first ---- - -## 2026-02-19 - US-009 -- What was implemented: - - Verified existing `ProjectSidebar` component at `src/renderer/components/projects/ProjectSidebar.tsx` satisfies all US-009 acceptance criteria - - New Project button at top using shadcn/ui Button + `clients.create` mutation with auto-rename on success - - Kebab context menu (DropdownMenu) with Rename, New Sub-Project, Delete actions - - Inline rename: Input replaces label, Enter saves via `clients.update`, Escape cancels, blur saves - - Delete: AlertDialog with two stages — initial confirm, then cascade-warn if children exist (uses `clients.delete` first, falls back to `clients.deleteWithCascade`) - - Hierarchical tree via `buildTree()` function (parent-child via `clients.parentId`) - - Empty state with EmptyMedia + call-to-action button - - All mutations invalidate `clients.list` query for immediate tree refresh - - Typecheck passes (zero errors), lint passes (1 non-null assertion warning, guarded) -- Files changed: `scripts/ralph/prd.json`, `scripts/ralph/progress.txt` -- **Learnings for future iterations:** - - The ProjectSidebar was built as part of US-004 app shell work but the US-009 story wasn't marked as passing — always check existing code before implementing - - `useCallback` ref pattern (`ref={callbackRef}`) is used for auto-focus + select on mount without useEffect - - The two-stage delete flow (try simple delete first → if error, show cascade option) maps well to the backend's `clients.delete` (guards) + `clients.deleteWithCascade` (force) pattern ---- - -## 2026-02-19 - US-010 -- What was implemented: - - Rewrote `ProjectSidebar` from a client-hierarchy tree to a project-centric sidebar grouped by client - - Projects grouped by `clientId` using Collapsible headers; projects without a client appear under "Internal / No Client" - - Search input filters projects by name in real-time (auto-expands all groups when searching) - - Show/hide archived projects via Switch toggle (queries `projects.list` with `includeArchived`) - - Context menu per project (DropdownMenu): Edit Client (Dialog + Select to assign/change/remove client), Archive/Unarchive, Delete (AlertDialog) - - Clicking a project sets `projectId` in search params → renders ProjectDetail placeholder in right pane - - Active project highlighted with `bg-sidebar-accent` - - Updated `projects.update` tRPC procedure to accept `clientId: z.string().nullable().optional()` (allows unlinking from client) - - Created placeholder `ProjectDetail` component (full implementation deferred to US-013) - - Installed shadcn/ui: dialog, select, switch -- Files changed: `src/renderer/components/projects/ProjectSidebar.tsx`, `src/renderer/routes/projects.tsx`, `src/renderer/components/projects/ProjectDetail.tsx` (new), `src/main/router/index.ts`, `src/renderer/components/ui/dialog.tsx` (new), `src/renderer/components/ui/select.tsx` (new), `src/renderer/components/ui/switch.tsx` (new), `scripts/ralph/prd.json`, `scripts/ralph/progress.txt` -- **Learnings for future iterations:** - - TanStack Router `validateSearch` with Zod schema is the cleanest way to pass selected-item IDs via URL search params without creating nested routes - - `Route.useNavigate()` returns a typed navigate fn; use `void navigate({ search: { ... } })` to avoid unhandled promise warnings - - For project grouping, query both `projects.list` and `clients.list` separately then join in-memory via a Map — avoids complex SQL joins for display-only data - - `projects.update` with `clientId: z.string().nullable().optional()` allows three states: undefined (don't change), null (unlink), string (assign) - - Auto-expanding all groups during search (`effectiveExpanded` computed from grouped keys) gives a better UX than forcing users to manually expand ---- - -## 2026-02-20 - US-011 -- What was implemented: - - Full Global Tasks view UI at `/tasks` route - - 4 stat cards (Total Tasks, To Do, In Progress, Completed) using shadcn/ui Card components with Lucide icons, reactively updated from unfiltered `tasks.list` query - - Search bar with 300ms debounce using shadcn/ui Input + Search icon - - Status filter tabs using shadcn/ui Tabs (All | To Do | In Progress | Completed) - - "Order by" dropdown using shadcn/ui DropdownMenu (Due Date | Priority | Created Date) - - Task rows with: shadcn/ui Checkbox (toggles todo↔done), title (bold 14px), description (muted truncated), priority Badge (HIGH=destructive, MEDIUM=secondary, LOW=outline green), due date chip (calendar icon + formatted date), breadcrumb (Client > Sub-Client > Project with ChevronRight separators), assignee (User icon + name) - - Completed task rows have green-tinted background (`bg-green-50 border-green-200`) - - NewTaskDialog component with: Input for title (required), Textarea for description, Select for priority/status, Popover+Calendar for due date, Select for project (from `projects.listAll`), Input for assignee - - Installed shadcn/ui components: card, tabs, checkbox, badge, textarea, popover, calendar -- Files changed: `src/renderer/routes/tasks.tsx`, `src/renderer/components/tasks/NewTaskDialog.tsx` (new), `src/renderer/components/ui/card.tsx` (new), `src/renderer/components/ui/tabs.tsx` (new), `src/renderer/components/ui/checkbox.tsx` (new), `src/renderer/components/ui/badge.tsx` (new), `src/renderer/components/ui/textarea.tsx` (new), `src/renderer/components/ui/popover.tsx` (new), `src/renderer/components/ui/calendar.tsx` (new), `scripts/ralph/prd.json`, `scripts/ralph/progress.txt` -- **Learnings for future iterations:** - - Use two separate `tasks.list` queries: one unfiltered `{}` for stat card counts, one with filters for the displayed list — ensures stats always reflect total counts - - `date-fns` `format(date, 'PPP')` produces "February 20th, 2026" style dates — already installed as a dependency of react-day-picker (shadcn/ui calendar) - - shadcn/ui Select with an empty string value (`No project`) works as a "none" option for optional fields - - The Popover+Calendar date picker pattern is standard shadcn/ui: Popover wraps a Button trigger showing the formatted date, PopoverContent contains the Calendar - - Electron app runs at `http://localhost:5173` in dev mode but only within the Electron BrowserWindow — Playwright browser testing requires the Electron-specific test harness, not direct URL navigation ---- - -## 2026-02-20 - US-012 -- What was implemented: - - Reusable `GanttChart` SVG component at `src/renderer/components/timeline/GanttChart.tsx` - - Accepts `{ checkpoints: GanttCheckpoint[], startDate: Date, endDate: Date, onDelete? }` props - - Custom SVG rendering: month labels on X axis, horizontal baseline ``, `` dots for checkpoints positioned by date - - Dot fill logic: dark (#171717) for future approved checkpoints, green (#16a34a) for past approved, dashed outline (#737373) for pending AI suggestions (isApproved=0) - - Vertical red "Today" marker line at current date - - ResizeObserver for responsive SVG width - - foreignObject + shadcn/ui Popover on each dot click: shows title, formatted date, project name, and destructive Delete button - - `AddCheckpointDialog` component at `src/renderer/components/timeline/AddCheckpointDialog.tsx`: title Input (required), Popover+Calendar date (required), Select project dropdown (required in global view, hidden when `defaultProjectId` provided) - - Global Timeline route (`/timeline`) renders GanttChart with all checkpoints, project name lookup via Map from `projects.listAll` - - Legend showing dot types, empty state message when no checkpoints -- Files changed: `src/renderer/components/timeline/GanttChart.tsx` (new), `src/renderer/components/timeline/AddCheckpointDialog.tsx` (new), `src/renderer/routes/timeline.tsx` -- **Learnings for future iterations:** - - `foreignObject` inside SVG is the cleanest way to embed React components (like Popover) on SVG elements — set `overflow-visible` class to prevent clipping - - Checkpoints don't have a `status` field; use `isApproved=1` + `date < now` heuristic for "completed" vs "todo" dot color - - Date range for the Gantt is computed dynamically: 1 month before earliest date, 2 months after latest date — ensures comfortable visual padding - - GanttChart is designed for reuse: the `defaultProjectId` prop on AddCheckpointDialog pre-selects the project and hides the dropdown (for per-project timeline in US-015) - - `trpc.projects.listAll.useQuery(undefined, { enabled: showProjectSelect })` prevents unnecessary queries when project is already known ---- - -## 2026-02-21 - US-014 -- What was implemented: - - Installed `@hello-pangea/dnd` for drag-and-drop support - - Created `KanbanBoard` component at `src/renderer/components/projects/KanbanBoard.tsx` - - `DragDropContext` wraps 3 `Droppable` columns: To Do (`todo`), In Progress (`in_progress`), Completed (`done`) - - Each task is a `Draggable` wrapping the shared `TaskRow` component (same UI as global Tasks view) - - Drag-and-drop between columns calls `tasks.update({ id, status })` via tRPC mutation - - Each column header shows: status label, `Badge` (variant=secondary) with task count, `Button` (variant=ghost, size=sm) with "+ Add" - - "+ Add" opens `NewTaskDialog` with `defaultProjectId` and `defaultStatus` pre-set to the column's status - - `EditTaskDialog` integrated for right-click context menu editing - - `tasks.delete` integrated for right-click context menu deletion - - Added "Tasks" section with `KanbanBoard` to `ProjectDetail.tsx` below the AI summary card - - Tasks with unknown status values fall back to the "To Do" column - - Drop zones highlight with `bg-muted/50` when dragging over -- Files changed: `src/renderer/components/projects/KanbanBoard.tsx` (new), `src/renderer/components/projects/ProjectDetail.tsx`, `prd.json`, `progress.txt`, `package.json`, `package-lock.json` -- **Learnings for future iterations:** - - `@hello-pangea/dnd` ships its own TypeScript declarations — no `@types/` package needed - - `TaskRow` component from the global Tasks view is fully reusable inside Kanban `Draggable` wrappers — its `ContextMenu` (Edit/Delete) still works correctly inside drag-and-drop contexts - - `NewTaskDialog` accepts `defaultStatus` prop which resets correctly on close via `resetAndClose()` — ideal for column-specific "+ Add" buttons - - When grouping tasks by status for Kanban columns, always handle unknown/null status values with a fallback to prevent tasks from disappearing - - `DragDropContext.onDragEnd` provides `draggableId` which maps directly to `task.id` — no need to look up the task object for status updates ---- - -## 2026-02-22 - US-015 -- What was implemented: - - Added Project Timeline section to `ProjectDetail.tsx` between AI Summary and Tasks Kanban - - Reused `GanttChart` component (from US-012) scoped to current project's checkpoints - - "+ Add" Button (variant=outline, size=sm) opens `AddCheckpointDialog` with `defaultProjectId={projectId}` (hides project selector) - - Wired `checkpoints.delete` mutation with `onDelete` prop for checkpoint dot deletion - - Computed `ganttStart`/`ganttEnd` dynamically from checkpoint dates with 1-month padding (fallback ±2 months if empty) - - Added Notes section below Tasks Kanban using `Item` component (variant=muted) in a flex-wrap grid layout matching Figma design - - Each note card shows `SquareDashed` icon + title + formatted createdAt date, clickable to navigate to `/notes/$noteId` - - "+ Add" Button calls `notes.create({ title: 'Untitled Note', content: '', projectId })` then navigates to the new note - - Created route stub at `src/renderer/routes/notes.$noteId.tsx` with back button + note title placeholder (full editor deferred to US-016) -- Files changed: `src/renderer/components/projects/ProjectDetail.tsx`, `src/renderer/routes/notes.$noteId.tsx` (new), `prd.json`, `progress.txt` -- **Learnings for future iterations:** - - GanttChart + AddCheckpointDialog are designed for reuse: `defaultProjectId` prop scopes the dialog to a project and hides the project select dropdown - - Figma notes section uses a card grid layout (flex-wrap with Item cards), not a flat list with Separators — always cross-reference Figma when acceptance criteria text diverges - - `trpc.useUtils()` provides `invalidate()` for cache busting after mutations — use at the component level, not inside mutation callbacks - - `notes.create` returns `{ id }` which can be used directly for navigation in the `onSuccess` callback - - TanStack Router file-based routing: `notes.$noteId.tsx` generates `/notes/:noteId` route automatically — `Route.useParams()` provides typed `{ noteId }` ---- - -## 2026-02-22 - US-016 -- What was implemented: - - Installed `@milkdown/kit`, `@milkdown/react`, `@milkdown/theme-nord` (following official Milkdown installation guide) - - Created `MilkdownEditor` wrapper component at `src/renderer/components/notes/MilkdownEditor.tsx` - - Uses official React recipe: `MilkdownProvider` + `useEditor` hook with `Editor.make()` configuring `commonmark`, `listener`, `history` plugins - - `listenerCtx.markdownUpdated()` fires onChange callback via stable `useRef` (avoids editor re-creation) - - `defaultValueCtx` sets initial markdown content from SQLite - - Rewrote `src/renderer/routes/notes.$noteId.tsx` with full editor page: - - Editable title: borderless shadcn/ui `Input` (border-0, shadow-none, focus-visible:ring-0), saves on blur via `notes.update({ id, title })` - - Auto-save: `onChange` from Milkdown triggers 500ms debounced `notes.update({ id, content })` via `useRef` + `setTimeout`/`clearTimeout` - - "Saving..." indicator: shadcn/ui `Badge` (variant=secondary) shown while debounce is pending, hidden on mutation `onSettled` - - Back button: shadcn/ui `Button` (variant=ghost, size=icon) with `ArrowLeft` Lucide icon, `window.history.back()` - - Loading/not-found states handled - - Added Milkdown/ProseMirror CSS overrides in `src/renderer/globals.css` using semantic color variables (`var(--foreground)`, `var(--muted)`, `var(--border)`, `var(--muted-foreground)`, `var(--primary)`) - - Typecheck passes (zero errors) -- Files changed: `src/renderer/components/notes/MilkdownEditor.tsx` (new), `src/renderer/routes/notes.$noteId.tsx`, `src/renderer/globals.css`, `package.json`, `package-lock.json`, `prd.json`, `progress.txt` -- **Learnings for future iterations:** - - `@milkdown/kit` is the recommended all-in-one package — it bundles core, preset-commonmark, plugin-listener, plugin-history, and utilities under sub-paths like `@milkdown/kit/core`, `@milkdown/kit/preset/commonmark`, etc. - - The `useEditor` hook from `@milkdown/react` takes `(root) => Editor.make()...` — the `root` param is the DOM element Milkdown manages, set via `ctx.set(rootCtx, root)` - - Use `useRef` for the onChange callback passed to `listenerCtx.markdownUpdated()` — this avoids re-creating the editor instance when the callback identity changes - - `listenerCtx.markdownUpdated((_ctx, markdown, prevMarkdown))` provides both current and previous markdown — compare them to avoid firing on no-op updates - - For debounced auto-save: `useRef>` + `clearTimeout`/`setTimeout` is simpler than external debounce libraries; cleanup in `useEffect` return prevents stale saves - - Nord theme (`@milkdown/theme-nord`) provides base ProseMirror structure; override with CSS using the app's semantic color variables for consistent theming - - Import both `@milkdown/theme-nord/style.css` and `@milkdown/kit/prose/view/style/prosemirror.css` for proper base styling ---- - -## 2026-02-22 - US-017 -- What was implemented: - - Fluid Curtain pull-down animation in `AppShell.tsx` - - `framer-motion` `useMotionValue(0)` + `useSpring(y, { stiffness: 300, damping: 30 })` controls `y` CSS transform on a `motion.div` wrapping the content area inside `SidebarInset` - - Sidebar stays visible at all times — only content area slides down - - Trigger 1: `document` wheel event — `findScrollableAncestor()` walks DOM to detect nearest scrollable element; if `scrollTop === 0` and `deltaY < 0`, opens curtain (`y.set(window.innerHeight)`) - - Trigger 2: `Cmd/Ctrl+K` keyboard shortcut toggles curtain open/closed - - Closing: `deltaY > 0` while curtain is open, or `Cmd/Ctrl+K` - - `AIChatPanel` placeholder component at `src/renderer/components/ai/AIChatPanel.tsx` — absolute `z-0` layer behind the sliding content panel - - Right-edge label dynamically changes: `"scrolling up for Adiuva"` + `ChevronUp` when closed, `"back to app"` + `ChevronDown` when open - - App panel remains mounted during animation (no unmount/remount, no state loss) - - `curtainOpenRef` (useRef) keeps event handlers in sync without re-registering effects -- Files changed: `src/renderer/components/layout/AppShell.tsx`, `src/renderer/components/ai/AIChatPanel.tsx` (new), `prd.json`, `progress.txt` -- **Learnings for future iterations:** - - `SidebarInset` already has `relative` in its base classes — adding `overflow-hidden` via className prop is sufficient to clip the sliding `motion.div` - - `useMotionValue` + `useSpring` pattern: `y.set(target)` immediately sets the spring target; `springY` (from `useSpring`) animates toward it — apply `springY` to `style={{ y: springY }}`, not `y` directly - - `findScrollableAncestor()` DOM walk is needed because `body` and `#root` both have `overflow: hidden` — scroll detection must target inner route containers (e.g., `overflow-y-auto` divs in projects/tasks) - - `useRef` for curtain open state avoids stale closures in `useEffect` wheel/keyboard handlers — the boolean ref is updated synchronously alongside `useState` setter - - `{ passive: true }` on wheel listener is correct when not calling `preventDefault()` — avoids Chrome console warnings ---- - -## 2026-02-23 - US-018 -- What was implemented: - - Installed `keytar` native module for OS keychain token storage; added to `vite.main.config.mts` externals - - Created provider-agnostic AI architecture under `src/main/ai/`: - - `token.ts` — keychain CRUD via keytar with safeStorage fallback (handles missing libsecret on WSL/Linux), keyed by provider name - - `provider.ts` — `AIProvider` interface with `name`, `displayName`, `initialize(token)`, `isReady()` methods; provider registry (`Map`), `initAI()` startup function, `saveTokenAndInit()`, `hasActiveToken()` - - `copilot.ts` — GitHub Copilot provider implementation (initial default); registers via `registerProvider()` on import - - Updated `src/main/store.ts` — added `aiProvider: string` and `encryptedTokens: Record` to `AppSettings` - - Updated `src/main/index.ts` — made `app.on('ready')` async, calls `await initAI()` after `initDb()` - - Updated `src/main/router/index.ts` — `ai.setToken` mutation calls `saveTokenAndInit(input.token)`, `ai.hasToken` query calls `hasActiveToken()` - - Created `/settings` route at `src/renderer/routes/settings.tsx`: - - Full settings page with left nav sidebar (shadcn pattern) + content area - - "AI Provider" section with password Input for token, Save Button, green "Saved" feedback - - Shows "A token is currently stored" indicator when `ai.hasToken` returns true - - Invalidates `ai.hasToken` query cache on successful save - - Updated `src/renderer/components/layout/AppShell.tsx`: - - Settings `SidebarMenuButton` with gear icon in sidebar footer links to `/settings` route - - Removed Dialog-based settings in favor of full route page - - Clean separation: no settings state lifted to AppShell - - Updated `src/renderer/components/ai/AIChatPanel.tsx`: - - Calls `trpc.ai.hasToken.useQuery()`; if `false`, renders `Card` with `KeyRound` icon + "AI provider not configured" + Link to `/settings` - - If `true`, shows existing "AI Chat — coming soon" placeholder - - Typecheck passes (zero errors) -- Files changed: `package.json`, `package-lock.json`, `vite.main.config.mts`, `src/main/ai/token.ts` (new), `src/main/ai/provider.ts` (new), `src/main/ai/copilot.ts` (new), `src/main/store.ts`, `src/main/index.ts`, `src/main/router/index.ts`, `src/renderer/routes/settings.tsx` (new), `src/renderer/components/layout/AppShell.tsx`, `src/renderer/components/ai/AIChatPanel.tsx`, `src/renderer/routeTree.gen.ts` (auto-generated), `prd.json`, `progress.txt` -- **Learnings for future iterations:** - - `keytar` requires `libsecret` on Linux (system dependency) — on WSL it's often missing. Use try/catch lazy require + Electron `safeStorage` as fallback to keep the app functional everywhere - - `safeStorage.encryptString()` returns a Buffer; store as base64 in electron-store. `safeStorage.decryptString()` takes a Buffer back. - - Provider-agnostic pattern: `AIProvider` interface + `Map` registry + `electron-store` for active provider name = swap providers by adding a new implementation + `registerProvider()` call - - `initAI()` uses dynamic `await import('./copilot')` to trigger side-effect registration before reading the provider from the registry - - Settings as a full route (not a Dialog) is better for extensibility — left nav allows adding sections (Appearance, Notifications, etc.) without cluttering the sidebar - - TanStack Router route tree must be regenerated after adding a new route file: `npx @tanstack/router-cli generate` or just `npm start` (Vite plugin does it) - - Token is stored per-provider so multiple providers can have tokens stored simultaneously; switching providers just changes which key is read - - `electron-store` dot-notation access (`store.get('a.b')`) works but loses type safety; prefer `store.get('encryptedTokens')` then access the nested key on the result object ---- - -## 2026-02-23 - US-019 -- What was implemented: - - Installed `@github/copilot-sdk` (v0.1.25) — official GitHub Copilot SDK with CopilotClient for programmatic CLI control via JSON-RPC - - Updated `src/main/ai/copilot.ts` — CopilotClient singleton created via dynamic `import()` (SDK is ESM-only), `initialize()` starts the client with `githubToken`, `getCopilotClient()` exported, clean shutdown on `app.before-quit` - - Added `@github/copilot-sdk` and `@github/copilot` to `vite.main.config.mts` externals (native CLI binary + prebuilds must stay in node_modules) - - Created IPC streaming side-channel: - - `src/preload/trpc.ts` — exposed `window.electronAI.onStreamChunk(cb)` via contextBridge on `ai:stream` IPC channel - - `src/main/ipc.ts` — added `TRPCContext` type with optional `sender: Electron.WebContents`, passed `event.sender` into tRPC context - - `src/renderer/lib/ipcLink.ts` — added `Window.electronAI` type declaration - - Updated `src/main/router/index.ts` — `initTRPC.context().create()`, `ai.chat` mutation now calls `orchestrate()` with streaming + error handling - - Created `src/main/ai/orchestrator.ts` (new) — core Orchestrator agent: - - System prompt instructs model to use exactly one routing tool per message - - 3 routing tools: `route_to_project`, `route_to_knowledge`, `route_to_general` (each with JSON schema params + handler callback) - - `buildProjectContext(projectId)` — fetches project, tasks, checkpoints, notes from DB - - `buildGlobalContext()` — fetches active projects, task counts, upcoming tasks due this week - - Two-phase orchestration: (1) Orchestrator session classifies intent via tool call, (2) Specialist session generates streamed response - - Specialist agent prompts: @ProjectAgent (scoped project data), @KnowledgeAgent (stub — LanceDB pending US-023), @GeneralAgent (workspace summary) - - Streaming via `session.on('assistant.message_delta')` → `sender.send('ai:stream', { token, done })` - - Error classification: auth (401/403) → friendly message, timeout → retry prompt, generic → error message - - Typecheck passes (zero errors), no new lint errors -- Files changed: `package.json`, `package-lock.json`, `vite.main.config.mts`, `src/main/ai/copilot.ts`, `src/main/ai/orchestrator.ts` (new), `src/main/ipc.ts`, `src/preload/trpc.ts`, `src/main/router/index.ts`, `src/renderer/lib/ipcLink.ts`, `prd.json`, `progress.txt` -- **Learnings for future iterations:** - - `@github/copilot-sdk` is ESM-only (`"type": "module"`) — cannot `require()` from CJS Electron main process. Use `await import('@github/copilot-sdk')` inside async functions - - The SDK depends on `@github/copilot` (the CLI binary, ~400MB) which includes native prebuilds, ripgrep, tree-sitter WASM, etc. Both must be externalized in Vite config - - `CopilotClient` manages a CLI subprocess via JSON-RPC. `autoStart: true` + `autoRestart: true` handles lifecycle. Call `client.stop()` on `app.before-quit` - - SDK tools use a `handler: async (args) => ToolResult` callback pattern — the SDK calls your handler when the model invokes the tool. This is different from OpenAI's function-calling (where you check tool_calls in the response) - - `session.sendAndWait()` blocks until `session.idle`, returns `AssistantMessageEvent | undefined`. Default timeout is 60s - - `session.on('assistant.message_delta', cb)` fires for each streaming chunk with `{ messageId, deltaContent }`. Returns an unsubscribe function - - `SessionConfig.availableTools` controls which tools are exposed to the model. Set to only your custom tool names to disable all built-in Copilot tools - - `SystemMessageConfig` has `mode: 'replace'` to fully replace the SDK's default system prompt — necessary for agent specialization - - For IPC streaming: extend the preload to expose a separate channel (`ai:stream`) via `contextBridge.exposeInMainWorld()`, and use `event.sender.send()` from the main process. This avoids touching the tRPC request-response infrastructure - - `TRPCContext` with optional `sender` field preserves backward compatibility — non-AI procedures get `sender` but don't use it ---- - -## 2026-02-23 - US-019 Refactor: LangGraph Provider-Independent Orchestration -- What was implemented: - - Installed `@langchain/langgraph`, `@langchain/core`, `@langchain/openai`, `@langchain/anthropic` for provider-independent agent orchestration - - Created `src/main/ai/llm.ts` (new) — LLM factory returning `BaseChatModel` for the active provider: - - `openai` → `ChatOpenAI` (gpt-4o-mini, streaming) - - `anthropic` → `ChatAnthropic` (claude-sonnet, streaming) - - `copilot` → `ChatCopilot` adapter wrapping the Copilot SDK - - Created `src/main/ai/chat-copilot.ts` (new) — LangChain-compatible `SimpleChatModel` adapter for GitHub Copilot SDK: - - `_call()` creates a Copilot session, sends messages, returns response text - - `_streamResponseChunks()` yields `ChatGenerationChunk` tokens via `assistant.message_delta` event listener + async generator pattern - - Refactored `src/main/ai/orchestrator.ts` — replaced Copilot SDK sessions with LangGraph `StateGraph`: - - `OrchestratorState` annotation: `userMessage`, `chatContext`, `route`, `messages`, `response` - - `classifyIntent` node: uses `llm.withStructuredOutput(RouteSchema)` for intent classification (project/knowledge/general) - - `projectAgent`, `knowledgeAgent`, `generalAgent` nodes: each gets context from DB + invokes LLM - - `addConditionalEdges` routes from classifier to specialist based on `state.route` - - Streaming via LangGraph `streamMode: 'messages'` — tokens from specialist nodes forwarded to renderer via IPC - - Graph is compiled once (singleton) and reused across calls - - Updated `vite.main.config.mts` — added all `@langchain/*` packages to externals - - Context assembly functions (`buildProjectContext`, `buildGlobalContext`) and system prompts preserved unchanged - - Typecheck passes (zero errors), no new lint errors -- Files changed: `package.json`, `package-lock.json`, `vite.main.config.mts`, `src/main/ai/llm.ts` (new), `src/main/ai/chat-copilot.ts` (new), `src/main/ai/orchestrator.ts`, `progress.txt` -- **Learnings for future iterations:** - - LangGraph packages ship dual ESM/CJS (`require` + `import` in exports map) — no dynamic import needed unlike `@github/copilot-sdk` - - `SimpleChatModel` from `@langchain/core` only requires implementing `_call()` and `_llmType()` — much simpler than full `BaseChatModel` - - `withStructuredOutput(zodSchema)` on a `BaseChatModel` forces the LLM to return JSON matching the schema — ideal for intent routing without custom tool handlers - - LangGraph `streamMode: 'messages'` yields `[chunk, metadata]` tuples; `metadata.langgraph_node` identifies which graph node produced each token — use this to filter out classifier tokens - - `Annotation.Root({})` with a `reducer` function enables append-only message arrays in state — matches LangGraph's immutable state update pattern - - The graph is compiled once via `buildGraph()` singleton — no per-request overhead for graph construction - - Architecture: agent logic (LangGraph) is now fully decoupled from the LLM provider. Adding a new provider only requires a new factory function in `llm.ts` ---- - -## 2026-02-23 - US-020 -- What was implemented: - - Full context-scoped AI chat UI in `AIChatPanel` component, replacing the "coming soon" placeholder - - Two-mode layout: empty state (centered input) and chat state (messages + pinned bottom input) - - Context header: `Badge` (variant=outline) showing "Chatting about: [Project Name]" or "Global workspace" - - Context derived in AppShell from `currentPath` + `searchObj['projectId']`; project name fetched via `trpc.projects.get` query - - User messages: right-aligned `Card` components - - AI messages: left-aligned plain text (no Card) with `Sparkles` icon + bold "Adiuva" header line - - Streaming: subscribes to `window.electronAI.onStreamChunk` IPC channel before firing `trpc.ai.chat.mutate()`; tokens accumulate in `streamingContent` state via `useRef` pattern - - Loading indicator: `Skeleton` lines (w-48 + w-32) shown below Adiuva header while waiting for first token - - Error handling: mutation errors and `{ error }` responses display in `Card` with `border-destructive` styling - - Session-only history: `useEffect` on `curtainOpen` prop clears all messages, input, and streaming state when curtain closes - - Scroll behavior: after user sends, scrolls user message to top of visible area; does NOT auto-scroll during AI streaming - - Input: `Textarea` matching Figma (white bg, border #d4d4d4, shadow-lg, min-h 109px, "Ask me anything..."), Send `Button` (default variant, Send icon + label) absolute bottom-right - - Enter sends, Shift+Enter for newline - - Extracted `ChatInput` sub-component for reuse between empty and chat states - - Typecheck passes (zero errors), no new lint errors introduced -- Files changed: `src/renderer/components/ai/AIChatPanel.tsx`, `src/renderer/components/layout/AppShell.tsx`, `prd.json`, `progress.txt` -- **Learnings for future iterations:** - - `window.electronAI.onStreamChunk` returns an unsubscribe function — subscribe before firing the mutation, unsubscribe in error/completion handlers - - Use `useRef` for accumulating streaming content (`streamingContentRef.current += token`) to avoid stale closure issues in the stream callback, then sync to state with `setStreamingContent(streamingContentRef.current)` - - `trpc.ai.chat.mutate()` returns `{ response, error? }` — the `error` field is `string | undefined`, so must narrow before passing to typed state (assign to a `const` first) - - `trpc.projects.get` query with `enabled: !!projectId` + `id: projectId ?? ''` avoids both the non-null assertion lint warning and unnecessary queries - - For scroll-to-user-message UX: track the last user message with a ref and use `scrollIntoView({ behavior: 'smooth', block: 'start' })` — do NOT auto-scroll on AI streaming to let the user read from the top ---- - -## 2026-02-24 - US-021 -- What was implemented: - - 4 project action tools in `src/main/ai/orchestrator.ts` using `@langchain/core/tools` `tool()` helper, via new `buildProjectTools(projectId)` factory function: - - `read_project_notes`: fetches full note content from SQLite (no 500-char truncation unlike buildProjectContext) - - `add_task`: inserts task via `db.insert(tasks).run()`, returns `'Task added: [title]'` - - `get_summary`: calls nested `getLLM().invoke()` to generate 2-3 sentence summary, persists via `db.update(projects).set({ aiSummary })` - - `suggest_checkpoints`: calls nested `getLLM().invoke()` with structured prompt, returns JSON array `[{ title, date }]` with regex extraction fallback - - `classifyIntent` short-circuits: `chatContext.type === 'project' && chatContext.projectId` → immediately returns `{ route: 'project' }` (saves one LLM round-trip, prevents misrouting) - - `projectAgent` rewritten with agent loop (max 5 iterations): - - `supportsTools` runtime guard: `'bindTools' in llm && typeof llm.bindTools === 'function'` - - Copilot path (no bindTools): direct `llm.invoke()` with full context prompt - - OpenAI/Anthropic path: `llm.bindTools!(projectTools)` → agent loop with `AIMessage.isInstance()` type guard for `tool_calls` access - - Tool dispatch via `matched.invoke({ ...toolCall, type: 'tool_call' as const })` — StructuredTool.invoke() detects ToolCall object and extracts args via internal `_isToolCall()` check - - `ToolMessage` appended per tool call with `tool_call_id`; `messageHistory` accumulated across iterations - - `makeProjectAgentPrompt` updated to describe all 4 available tools and usage guidance - - Streaming unaffected: tool-calling rounds produce empty `chunk.content` (falsy), filtered by existing guard; final text response streams normally -- Files changed: `src/main/ai/orchestrator.ts`, `prd.json`, `progress.txt` -- **Learnings for future iterations:** - - `tool()` returns `DynamicStructuredTool<...>` — use `StructuredTool[]` as the function return type and cast with `as StructuredTool[]` to avoid generic type variance errors in strict mode - - `llm.bindTools` is typed as optional on `BaseChatModel` — even after a runtime `typeof === 'function'` guard, TypeScript still reports TS18048 ("possibly undefined"); use `llm.bindTools!()` with an eslint-disable comment after the guard - - `AIMessage.isInstance(response)` is the zero-unsafe-cast way to access `tool_calls` on a `BaseMessage` — avoids `as any` - - LangGraph `streamMode: 'messages'` naturally skips tool-calling rounds because `chunk.content` is `''` (falsy) for AIMessageChunks that have tool call deltas - - Nested `getLLM().invoke()` calls inside tool handlers (for `get_summary`, `suggest_checkpoints`) do NOT stream tokens to the IPC channel — they execute synchronously within the tool handler, outside LangGraph's stream interceptor - - Short-circuiting `classifyIntent` for project context saves cost and prevents misrouting when user asks general questions from within a project view - - Empty Zod schema `z.object({})` infers TypeScript type `{}` — use `Record` as the handler parameter type to be explicit about intent in strict mode ---- - -## 2026-02-24 - US-021 bugfix -- Bug: `` XML appeared in chat and tasks weren't actually created -- Root cause: `ChatCopilot` extends `SimpleChatModel` which inherits `bindTools()` from `BaseChatModel` — so `'bindTools' in llm` returned TRUE. But `ChatCopilot._call()` ignores bound tools (no kwargs plumbing to the Copilot SDK). The model received tool descriptions in the system prompt but NOT via the API, so it hallucinated `{"name":"sql",...}` freeform text. `tool_calls` on the response was empty → tool not executed → fake success text streamed to UI -- Fix: Replaced runtime `'bindTools' in llm` check with provider-name whitelist (`TOOL_CALLING_PROVIDERS = new Set(['openai', 'anthropic'])`). Imported `getActiveProviderName` from `./provider` -- Fix: Copilot fallback path now uses `makeProjectAgentPrompt(contextData, false)` — no tool section in system prompt — preventing hallucinated tool calls text -- Files changed: `src/main/ai/orchestrator.ts` -- **Learnings for future iterations:** - - `BaseChatModel.bindTools()` exists as a default inherited method in modern LangChain — `'bindTools' in llm` is ALWAYS true. You CANNOT use this to detect actual tool calling support; must know the provider - - The safe check is provider-name based: only 'openai' and 'anthropic' have real bindTools support in this codebase - - When a model receives tool descriptions IN THE SYSTEM PROMPT but NOT via the API tool calling mechanism, it may hallucinate tool calls as freeform text (especially models trained on ReAct/ToolBench data) - - Remove tool mentions from system prompt when NOT using API-level tool calling ---- - -## 2026-02-24 - US-021 classifyIntent short-circuit bugfix -- Bug: After US-021, GitHub Copilot chat broke entirely with "Failed to list models" SDK error -- Root cause: The `classifyIntent` short-circuit (added in US-021 for project context) removed the FIRST LLM call from the graph. The Copilot SDK requires at least one prior `sendAndWait()` call to initialize its internal model list cache before a subsequent call succeeds. Without `classifyIntent`'s LLM invocation acting as warm-up, the cold `projectAgent` call triggered `runAgenticLoop → listModels` which failed -- Fix: Removed the short-circuit from `classifyIntent` entirely. The node always calls the LLM for routing (matching pre-US-021 behavior). The `metadata.langgraph_node !== 'classifyIntent'` check in the streaming loop already prevents the routing token from appearing in chat -- Files changed: `src/main/ai/orchestrator.ts` -- **Learnings for future iterations:** - - The Copilot SDK needs a "warm-up" LLM call before it can successfully process the main request. Never eliminate the first LLM call in the graph when Copilot is the provider - - Short-circuit optimizations that skip LLM nodes are only safe for providers where the SDK has no internal state to initialize (OpenAI, Anthropic) - - If you want to restore the short-circuit as an OpenAI/Anthropic optimization, gate it: `if (TOOL_CALLING_PROVIDERS.has(getActiveProviderName()) && state.chatContext.type === 'project')` ---- - -## [2026-02-24] - US-022 -- What was implemented: - - Installed `vectordb` (LanceDB Node.js binding v0.21.2) as a project dependency - - Created `src/main/ai/embeddings.ts`: reads GitHub Copilot OAuth token from `~/.copilot/config.json` (via `copilot_tokens` map), falls back to stored OpenAI token via `getToken('openai')`. Uses `@langchain/openai` `OpenAIEmbeddings` with `baseURL: 'https://api.githubcopilot.com'` for Copilot path, or standard OpenAI API for fallback. Exposes `embedText(text): Promise` - - Created `src/main/db/vectordb.ts`: LanceDB singleton (`initVectorDb()` / `getConn()`), `upsertNoteEmbedding(noteId, projectId, content)` with delete-then-add upsert strategy (first call auto-creates table with schema inferred from first record), `migrateNotesIfNeeded()` that checks table existence on startup and bulk-embeds all SQLite notes sequentially with per-note error isolation - - Modified `src/main/router/index.ts`: imported `upsertNoteEmbedding`, made `notes.create` and `notes.update` async, added fire-and-forget embedding calls with `.catch(console.error)` in both handlers; `notes.update` re-fetches the full note from SQLite after the write to embed current title+content - - Modified `src/main/index.ts`: imported `initVectorDb` + `migrateNotesIfNeeded`, added `initVectorDb().then(() => migrateNotesIfNeeded()).catch(...)` chain in `app.on('ready')` - - Modified `vite.main.config.mts`: added `'vectordb'` to the `external` array so ViteRollup doesn't try to bundle the NAPI-RS binary -- Files changed: - - `package.json` (vectordb dependency added) - - `vite.main.config.mts` - - `src/main/index.ts` - - `src/main/router/index.ts` - - `src/main/ai/embeddings.ts` (new) - - `src/main/db/vectordb.ts` (new) -- **Learnings for future iterations:** - - `@github/copilot-sdk` has **no embeddings API** — it is a pure chat/session SDK. The `CopilotClient` type definitions contain zero mention of "embedding". Do not assume any LLM provider SDK supports embeddings - - GitHub Copilot CLI stores OAuth tokens in `~/.copilot/config.json` under `copilot_tokens["{host}:{login}"]`. The token format is `gho_*` (GitHub OAuth). These tokens work with the GitHub Copilot REST API (`https://api.githubcopilot.com`) which is OpenAI-compatible — including embeddings - - `@langchain/openai`'s `OpenAIEmbeddings` accepts a `configuration.baseURL` option that makes it work against any OpenAI-compatible endpoint - - `vectordb` (v0.21.2): deprecated but functional. The new package name is `@lancedb/lancedb`. `vectordb` requires at least one data record for `createTable()` (cannot create an empty table — schema is inferred from the first record). Use delete-then-add for upsert since there's no native upsert API at this version - - When using dynamic `import('@langchain/openai')`, TypeScript cannot infer the exact return type of `embedDocuments()` — it resolves to `{}` instead of `number[][]`. Fix: cast explicitly `as number[][]` - - tRPC mutation handlers support both sync and async functions transparently — making a mutation `async` does not break the renderer-side interface - - `notes.update` allows partial field updates (title or content can be omitted). Always re-fetch the full note from SQLite after the update write to get the correct combined text for embedding - - `vectordb`'s `table.delete(where)` accepts a raw SQL WHERE clause string. UUID v4 IDs are safe to interpolate directly (only `[0-9a-f-]` characters) ---- - -## 2026-02-24 - US-023 -- Implemented @KnowledgeAgent semantic search across all projects -- Added `searchNotes()` to `src/main/db/vectordb.ts`: embeds query via `embedText()`, performs `table.search(vector).limit(k).execute()` on LanceDB notes table, returns `SearchResult[]` with id, projectId, content, _distance -- Added `buildKnowledgeTools()` to `src/main/ai/orchestrator.ts`: defines `vector_search_all` tool that calls `searchNotes()`, joins SQLite for note title and project name, returns formatted results with `From: [Project Name] — [Note Title]` citation headers -- Rewrote `knowledgeAgent()` from simple LLM stub to full tool-calling agent loop (mirrors projectAgent/generalAgent pattern: TOOL_CALLING_PROVIDERS check, bindTools, 5-iteration MAX_ITERATIONS loop with ToolMessage accumulation, fallback for providers without tool support) -- Updated `makeKnowledgeAgentPrompt()` with `withTools` parameter, tool documentation for `vector_search_all`, and citation format instructions -- Files changed: `src/main/db/vectordb.ts`, `src/main/ai/orchestrator.ts` -- **Learnings for future iterations:** - - `openTable('name')` in vectordb makes `search()` expect `T` as input type — omit the generic when using `search()` with a raw vector array - - The agent loop pattern (check TOOL_CALLING_PROVIDERS → buildTools → bindTools → iterate with ToolMessage) is now used consistently across all three agents (project, knowledge, general) — follow this pattern for any new agent - - LanceDB `table.search(vector).limit(k).execute()` returns objects with all stored fields plus `_distance` (L2 distance, lower = more similar) - - The `SearchResult` type is exported from `vectordb.ts` for reuse in the orchestrator — keep vector DB types in the DB module, not the AI module ---- - -## 2026-02-24 - US-024 -- Implemented AI checkpoint suggestions UI with approve/reject flow -- Extended to also support AI task suggestions (user-requested scope expansion) -- Modified suggest_checkpoints tool in orchestrator to persist suggestions to DB (isAiSuggested=1, isApproved=0) -- Created new suggest_tasks tool with same pattern (analyzes notes, extracts actionable tasks, persists to DB) -- Added isAiSuggested/isApproved columns to tasks table schema + ALTER TABLE migration for existing databases -- Updated tasks.create/update router to accept new fields -- Added TaskItem type fields for isAiSuggested/isApproved -- ProjectDetail.tsx: "Suggest checkpoints" button (outline+Sparkles) in timeline header, "Suggest tasks" button in tasks header -- Pending suggestions render as border-dashed Card components below GanttChart / above KanbanBoard -- Approve (variant=default, size=sm) calls update with isApproved=1; Reject (variant=ghost, size=sm) calls delete -- KanbanBoard and workspace tasks route filter out unapproved AI suggestions -- Files changed: - - src/main/db/schema.ts (added isAiSuggested + isApproved to tasks) - - src/main/db/index.ts (ALTER TABLE migration + updated CREATE TABLE) - - src/main/router/index.ts (tasks.create/update/list updated) - - src/main/ai/orchestrator.ts (persist checkpoint suggestions + new suggest_tasks tool + updated system prompt) - - src/renderer/components/projects/ProjectDetail.tsx (suggest buttons + pending cards for both) - - src/renderer/components/projects/KanbanBoard.tsx (filter out pending AI suggestions) - - src/renderer/components/tasks/TaskRow.tsx (TaskItem type extended) - - src/renderer/routes/tasks.tsx (filter out pending AI suggestions) -- **Learnings for future iterations:** - - Tasks table defaults isApproved=1 (unlike checkpoints which default=0) so existing/manually-created tasks remain visible - - SQLite has no ADD COLUMN IF NOT EXISTS — use try/catch around ALTER TABLE statements - - The suggest_checkpoints/suggest_tasks tools persist directly via db.insert() in the tool handler, then query invalidation on the frontend picks up new records - - TaskItem type in TaskRow.tsx is manually defined (not auto-inferred from tRPC) — must be updated when adding columns to the tasks select - - The ai.chat mutation can be instantiated multiple times for independent suggest flows (suggestCheckpoints vs suggestTasks) ---- - -## 2026-02-25 - US-025 -- Implemented Home dashboard with AI daily brief and suggestion chips -- AIChatPanel enhanced to serve as both home page (initial greeting/brief/chips state) and curtain chat -- Added `userName` setting to electron-store (defaults to 'there'), with `getUserName`/`setUserName` tRPC procedures -- Added `tasks.dueToday` query returning tasks with dueDate on or before end of today (status != done) -- AppShell hides content panel on home route, revealing AIChatPanel directly -- Home initial state: greeting "✦ Hello, {name}" (30px, -1px tracking) + Badge (variant=secondary) with due task count -- Daily brief auto-fires via ai.chat on mount, streams into a Card below greeting -- Chat input: white bg, border #d4d4d4, shadow-lg, min-height 109px, send button with foreground/background colors -- 4 suggestion chips (ListTodo, TrendingUp, AlertCircle, Lightbulb) below chat input, populate input on click -- On first user message: greeting + chips disappear, brief card persists, chat messages flow below -- Responsive: flex-wrap on chips for narrow screens, max-w-3xl container -- Files changed: - - src/main/store.ts (added userName field) - - src/main/router/index.ts (getUserName, setUserName, dueToday) - - src/renderer/components/ai/AIChatPanel.tsx (main change — home mode with greeting, brief, chips) - - src/renderer/components/layout/AppShell.tsx (hide content panel on home, pass isHomePage prop) - - src/renderer/routes/index.tsx (simplified to null component) - - prd.json (passes: true) -- **Learnings for future iterations:** - - AIChatPanel is the single source of truth for all AI chat UIs — home page is just a different initial state, not a separate component - - The curtain is disabled on home (`currentPath !== '/'`), so AppShell conditionally hides the motion.div content panel to reveal AIChatPanel - - Stream interleaving prevention: disable chat input while dailyBrief is loading (both use the same `ai:stream` IPC channel) - - `hasFiredBrief` ref prevents double-fire in React strict mode for the auto-fire daily brief effect - - electron-store settings don't need schema migrations — just add to the AppSettings interface with a default ----