aboutsummaryrefslogtreecommitdiffstats
path: root/plan.md
diff options
context:
space:
mode:
Diffstat (limited to 'plan.md')
-rw-r--r--plan.md1684
1 files changed, 0 insertions, 1684 deletions
diff --git a/plan.md b/plan.md
deleted file mode 100644
index 9f74c1c..0000000
--- a/plan.md
+++ /dev/null
@@ -1,1684 +0,0 @@
1# Plan: Claude Flow Implementation
2
3## Overview
4
5A document-centric coding assistant that enforces a structured workflow: **Research → Plan → Implement**.
6
7The primary UI is a **markdown document viewer/editor** with a **chat sidebar**. The workflow is driven by the document, not the chat.
8
9---
10
11## User Flow
12
13### 1. Project Setup
14- Add a project by selecting a folder
15- Start a new session within the project
16
17### 2. Research Phase
18- Chat dialogue: Claude asks what to research and what you want to build
19- You provide direction via chat
20- Claude generates `research.md` → displayed as rendered markdown
21- You edit the document (add comments, adjustments)
22- App detects changes → enables **[Review]** and **[Submit]** buttons
23- **Review**: Claude reads your changes and adjusts the document
24- **Submit**: Move to Plan phase
25
26### 3. Plan Phase
27- Claude generates `plan.md` based on research
28- Displayed as rendered markdown
29- You edit, iterate with **[Review]**
30- **Submit**: Kicks off Implementation
31
32### 4. Implement Phase
33- Claude executes the plan
34- Marks tasks complete as it goes
35- Chat shows progress and tool usage
36
37---
38
39## UI Layout
40
41```
42┌──────────────────────────────────────────────────────────────┐
43│ ┌─────────────────┐ ┌─────────────────┐ [Research ● ───]│
44│ │ Project ▾ │ │ Session ▾ │ [Plan ○────────]│
45│ └─────────────────┘ └─────────────────┘ [Implement ○───]│
46├────────────────────────────────────────┬─────────────────────┤
47│ │ │
48│ ┌────────────────────────────────┐ │ Chat Dialogue │
49│ │ │ │ │
50│ │ # Research Findings │ │ ┌───────────────┐ │
51│ │ │ │ │ What areas │ │
52│ │ ## Authentication System │ │ │ should I │ │
53│ │ │ │ │ research? │ │
54│ │ The auth module uses JWT... │ │ └───────────────┘ │
55│ │ │ │ │
56│ │ // REVIEW: check OAuth too │ │ ┌───────────────┐ │
57│ │ │ │ │ Research the │ │
58│ │ │ │ │ auth system, │ │
59│ │ │ │ │ I want OAuth │ │
60│ └────────────────────────────────┘ │ └───────────────┘ │
61│ │ │
62│ 42k / 200k tokens ████░░░░░░░ │ │
63├────────────────────────────────────────┼─────────────────────┤
64│ [Review] [Submit →] │ [____________] ⏎ │
65└────────────────────────────────────────┴─────────────────────┘
66```
67
68### Key UI Elements
69
70| Element | Behavior |
71|---------|----------|
72| **Project dropdown** | Select/create projects |
73| **Session dropdown** | Select/create sessions within project |
74| **Phase indicator** | Shows current phase (Research → Plan → Implement) |
75| **Document pane** | Rendered markdown, editable |
76| **Chat pane** | Dialogue with Claude |
77| **Review button** | Disabled until document edited. Triggers Claude to read changes. |
78| **Submit button** | Advances to next phase |
79| **Token indicator** | Shows context usage |
80
81---
82
83## Directory Structure
84
85```
86claude-flow/
87├── src/main/
88│ ├── index.ts # App lifecycle, window management
89│ ├── preload.ts # IPC bridge
90│ ├── db/
91│ │ ├── index.ts # Database connection singleton
92│ │ ├── schema.ts # Table definitions + migrations
93│ │ ├── projects.ts # Project CRUD
94│ │ └── sessions.ts # Session CRUD
95│ ├── claude/
96│ │ ├── index.ts # Claude SDK wrapper
97│ │ ├── phases.ts # Phase configs (prompts, tools, permissions)
98│ │ └── hooks.ts # Custom hooks for phase enforcement
99│ └── ipc/
100│ └── handlers.ts # All IPC handlers
101├── renderer/
102│ ├── index.html
103│ └── src/
104│ ├── main.tsx
105│ ├── App.tsx
106│ ├── components/
107│ │ ├── Header.tsx # Project/session dropdowns + phase indicator
108│ │ ├── DocumentPane.tsx # Markdown viewer/editor
109│ │ ├── ChatPane.tsx # Chat dialogue
110│ │ ├── ActionBar.tsx # Review/Submit buttons + token indicator
111│ │ └── Message.tsx # Single chat message
112│ ├── lib/
113│ │ ├── api.ts # Typed IPC wrapper
114│ │ └── markdown.ts # Markdown rendering utilities
115│ ├── types.ts
116│ └── styles/
117│ └── globals.css
118├── package.json
119├── tsconfig.json
120└── vite.config.ts
121```
122
123---
124
125## Phase 1: Database Layer
126
127### 1.1 Schema (`src/main/db/schema.ts`)
128
129```typescript
130import Database from "better-sqlite3";
131
132export function initSchema(db: Database.Database) {
133 db.exec(`
134 CREATE TABLE IF NOT EXISTS projects (
135 id TEXT PRIMARY KEY,
136 name TEXT NOT NULL,
137 path TEXT NOT NULL,
138 created_at INTEGER NOT NULL DEFAULT (unixepoch()),
139 updated_at INTEGER NOT NULL DEFAULT (unixepoch())
140 );
141
142 CREATE TABLE IF NOT EXISTS sessions (
143 id TEXT PRIMARY KEY,
144 project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
145 name TEXT NOT NULL,
146 phase TEXT NOT NULL DEFAULT 'research',
147 claude_session_id TEXT,
148 permission_mode TEXT NOT NULL DEFAULT 'acceptEdits',
149 created_at INTEGER NOT NULL DEFAULT (unixepoch()),
150 updated_at INTEGER NOT NULL DEFAULT (unixepoch())
151 );
152
153 CREATE TABLE IF NOT EXISTS messages (
154 id TEXT PRIMARY KEY,
155 session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
156 role TEXT NOT NULL,
157 content TEXT NOT NULL,
158 created_at INTEGER NOT NULL DEFAULT (unixepoch())
159 );
160
161 CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_id);
162 CREATE INDEX IF NOT EXISTS idx_messages_session ON messages(session_id);
163 `);
164}
165```
166
167**Notes:**
168- `phase` is one of: `research`, `plan`, `implement`
169- `claude_session_id` stores the SDK's session ID for resuming
170- `permission_mode`: `acceptEdits` or `bypassPermissions` (user toggle in implement phase)
171
172### 1.2 Database Connection (`src/main/db/index.ts`)
173
174```typescript
175import Database from "better-sqlite3";
176import { app } from "electron";
177import path from "node:path";
178import fs from "node:fs";
179import { initSchema } from "./schema";
180
181let db: Database.Database | null = null;
182
183export function getDb(): Database.Database {
184 if (db) return db;
185
186 const dbDir = app.getPath("userData");
187 if (!fs.existsSync(dbDir)) {
188 fs.mkdirSync(dbDir, { recursive: true });
189 }
190
191 const dbPath = path.join(dbDir, "claude-flow.db");
192 db = new Database(dbPath);
193 db.pragma("journal_mode = WAL");
194 db.pragma("foreign_keys = ON");
195
196 initSchema(db);
197 return db;
198}
199
200export function closeDb() {
201 if (db) {
202 db.close();
203 db = null;
204 }
205}
206```
207
208### 1.3 Project CRUD (`src/main/db/projects.ts`)
209
210```typescript
211import { getDb } from "./index";
212import { v4 as uuid } from "uuid";
213
214export interface Project {
215 id: string;
216 name: string;
217 path: string;
218 created_at: number;
219 updated_at: number;
220}
221
222export function listProjects(): Project[] {
223 return getDb()
224 .prepare("SELECT * FROM projects ORDER BY updated_at DESC")
225 .all() as Project[];
226}
227
228export function getProject(id: string): Project | undefined {
229 return getDb()
230 .prepare("SELECT * FROM projects WHERE id = ?")
231 .get(id) as Project | undefined;
232}
233
234export function createProject(name: string, projectPath: string): Project {
235 const db = getDb();
236 const id = uuid();
237 const now = Math.floor(Date.now() / 1000);
238
239 db.prepare(
240 "INSERT INTO projects (id, name, path, created_at, updated_at) VALUES (?, ?, ?, ?, ?)"
241 ).run(id, name, projectPath, now, now);
242
243 return { id, name, path: projectPath, created_at: now, updated_at: now };
244}
245
246export function deleteProject(id: string): void {
247 getDb().prepare("DELETE FROM projects WHERE id = ?").run(id);
248}
249```
250
251### 1.4 Session CRUD (`src/main/db/sessions.ts`)
252
253```typescript
254import { getDb } from "./index";
255import { v4 as uuid } from "uuid";
256
257export type Phase = "research" | "plan" | "implement";
258export type PermissionMode = "acceptEdits" | "bypassPermissions";
259
260export interface Session {
261 id: string;
262 project_id: string;
263 name: string;
264 phase: Phase;
265 claude_session_id: string | null;
266 permission_mode: PermissionMode;
267 created_at: number;
268 updated_at: number;
269}
270
271export interface Message {
272 id: string;
273 session_id: string;
274 role: "user" | "assistant";
275 content: string;
276 created_at: number;
277}
278
279export function listSessions(projectId: string): Session[] {
280 return getDb()
281 .prepare("SELECT * FROM sessions WHERE project_id = ? ORDER BY updated_at DESC")
282 .all(projectId) as Session[];
283}
284
285export function getSession(id: string): Session | undefined {
286 return getDb()
287 .prepare("SELECT * FROM sessions WHERE id = ?")
288 .get(id) as Session | undefined;
289}
290
291export function createSession(projectId: string, name: string): Session {
292 const db = getDb();
293 const id = uuid();
294 const now = Math.floor(Date.now() / 1000);
295
296 db.prepare(
297 `INSERT INTO sessions (id, project_id, name, phase, permission_mode, created_at, updated_at)
298 VALUES (?, ?, ?, 'research', 'acceptEdits', ?, ?)`
299 ).run(id, projectId, name, now, now);
300
301 return {
302 id,
303 project_id: projectId,
304 name,
305 phase: "research",
306 claude_session_id: null,
307 permission_mode: "acceptEdits",
308 created_at: now,
309 updated_at: now,
310 };
311}
312
313export function updateSession(
314 id: string,
315 updates: Partial<Pick<Session, "name" | "phase" | "claude_session_id" | "permission_mode">>
316): void {
317 const db = getDb();
318 const sets: string[] = [];
319 const values: any[] = [];
320
321 for (const [key, value] of Object.entries(updates)) {
322 if (value !== undefined) {
323 sets.push(`${key} = ?`);
324 values.push(value);
325 }
326 }
327
328 if (sets.length > 0) {
329 sets.push("updated_at = ?");
330 values.push(Math.floor(Date.now() / 1000));
331 values.push(id);
332 db.prepare(`UPDATE sessions SET ${sets.join(", ")} WHERE id = ?`).run(...values);
333 }
334}
335
336export function deleteSession(id: string): void {
337 getDb().prepare("DELETE FROM sessions WHERE id = ?").run(id);
338}
339
340// Messages
341export function listMessages(sessionId: string): Message[] {
342 return getDb()
343 .prepare("SELECT * FROM messages WHERE session_id = ? ORDER BY created_at ASC")
344 .all(sessionId) as Message[];
345}
346
347export function addMessage(sessionId: string, role: Message["role"], content: string): Message {
348 const db = getDb();
349 const id = uuid();
350 const now = Math.floor(Date.now() / 1000);
351
352 db.prepare(
353 "INSERT INTO messages (id, session_id, role, content, created_at) VALUES (?, ?, ?, ?, ?)"
354 ).run(id, sessionId, role, content, now);
355
356 db.prepare("UPDATE sessions SET updated_at = ? WHERE id = ?").run(now, sessionId);
357
358 return { id, session_id: sessionId, role, content, created_at: now };
359}
360```
361
362---
363
364## Phase 2: Claude Integration
365
366### 2.1 Phase Configs (`src/main/claude/phases.ts`)
367
368```typescript
369import { Phase, PermissionMode } from "../db/sessions";
370
371export interface PhaseConfig {
372 systemPrompt: string;
373 allowedTools: string[];
374 permissionMode: "plan" | PermissionMode;
375 initialMessage: string; // What Claude says when entering this phase
376}
377
378export const phaseConfigs: Record<Phase, PhaseConfig> = {
379 research: {
380 permissionMode: "plan",
381 allowedTools: ["Read", "Glob", "Grep", "WebSearch", "WebFetch", "Write"],
382 initialMessage: "What areas of the codebase should I research? What are you trying to build?",
383 systemPrompt: `You are in RESEARCH mode.
384
385Your job is to deeply understand the codebase before any changes are made.
386
387When the user tells you what to research:
3881. Read files thoroughly — understand all intricacies
3892. Write your findings to .claude-flow/research.md
3903. Format it as clear, readable markdown
391
392Rules:
393- DO NOT make any code changes
394- DO NOT modify any files except .claude-flow/research.md
395- Be thorough — surface-level reading is not acceptable
396
397When the user clicks "Review", read .claude-flow/research.md for their annotations and update accordingly.
398When the user clicks "Submit", they're ready to move to planning.`,
399 },
400
401 plan: {
402 permissionMode: "plan",
403 allowedTools: ["Read", "Glob", "Grep", "Write"],
404 initialMessage: "I'll create a detailed implementation plan based on my research. Give me a moment...",
405 systemPrompt: `You are in PLANNING mode.
406
407Based on the research in .claude-flow/research.md, write a detailed implementation plan.
408
409Write the plan to .claude-flow/plan.md with:
410- Detailed explanation of the approach
411- Specific code snippets showing proposed changes
412- File paths that will be modified
413- Trade-offs and considerations
414- A granular TODO list with checkboxes
415
416Rules:
417- DO NOT implement anything
418- DO NOT modify any source files
419- Only write to .claude-flow/plan.md
420
421The plan should be detailed enough that implementation becomes mechanical.
422
423When the user clicks "Review", read .claude-flow/plan.md for their annotations and update accordingly.
424When the user clicks "Submit", begin implementation.`,
425 },
426
427 implement: {
428 permissionMode: "acceptEdits", // Will be overridden by user setting
429 allowedTools: ["Read", "Write", "Edit", "Bash", "Glob", "Grep"],
430 initialMessage: "Starting implementation. I'll follow the plan exactly and mark tasks complete as I go.",
431 systemPrompt: `You are in IMPLEMENTATION mode. The plan has been approved.
432
433Read .claude-flow/plan.md and execute it:
434- Follow the plan exactly
435- Mark tasks complete (- [x]) as you finish them
436- Run typecheck/lint continuously if available
437- Do not add unnecessary comments
438- Do not stop until all tasks are complete
439
440If you encounter issues not covered by the plan, stop and ask.`,
441 },
442};
443
444export function getPhaseConfig(phase: Phase, userPermissionMode?: PermissionMode): PhaseConfig {
445 const config = { ...phaseConfigs[phase] };
446 if (phase === "implement" && userPermissionMode) {
447 config.permissionMode = userPermissionMode;
448 }
449 return config;
450}
451```
452
453### 2.2 Claude Wrapper (`src/main/claude/index.ts`)
454
455```typescript
456import { query, SDKMessage } from "@anthropic-ai/claude-agent-sdk";
457import { Session, Phase, updateSession, getSession } from "../db/sessions";
458import { getPhaseConfig } from "./phases";
459import { getProject } from "../db/projects";
460import fs from "node:fs";
461import path from "node:path";
462
463const activeQueries = new Map<string, ReturnType<typeof query>>();
464
465function ensureArtifactDir(projectPath: string): void {
466 const dir = path.join(projectPath, ".claude-flow");
467 if (!fs.existsSync(dir)) {
468 fs.mkdirSync(dir, { recursive: true });
469 }
470}
471
472export interface SendMessageOptions {
473 session: Session;
474 message: string;
475 onMessage: (msg: SDKMessage) => void;
476}
477
478export async function sendMessage({ session, message, onMessage }: SendMessageOptions): Promise<void> {
479 const project = getProject(session.project_id);
480 if (!project) throw new Error("Project not found");
481
482 ensureArtifactDir(project.path);
483
484 const phaseConfig = getPhaseConfig(session.phase, session.permission_mode);
485
486 const q = query({
487 prompt: message,
488 options: {
489 cwd: project.path,
490 resume: session.claude_session_id ?? undefined,
491 systemPrompt: phaseConfig.systemPrompt,
492 allowedTools: phaseConfig.allowedTools,
493 permissionMode: phaseConfig.permissionMode,
494 },
495 });
496
497 activeQueries.set(session.id, q);
498
499 try {
500 for await (const msg of q) {
501 if (msg.type === "system" && msg.subtype === "init") {
502 if (!session.claude_session_id) {
503 updateSession(session.id, { claude_session_id: msg.session_id });
504 }
505 }
506 onMessage(msg);
507 }
508 } finally {
509 activeQueries.delete(session.id);
510 }
511}
512
513export function interruptSession(sessionId: string): void {
514 const q = activeQueries.get(sessionId);
515 if (q) {
516 q.close();
517 activeQueries.delete(sessionId);
518 }
519}
520
521// Trigger review: Claude reads the document and addresses annotations
522export async function triggerReview(session: Session, onMessage: (msg: SDKMessage) => void): Promise<void> {
523 const docName = session.phase === "research" ? "research.md" : "plan.md";
524 const message = `I've updated .claude-flow/${docName} with annotations. Read the file, find all my inline notes (marked with // REVIEW:, // NOTE:, TODO:, or similar), address each one, and update the document accordingly. Do not implement anything yet.`;
525
526 await sendMessage({ session, message, onMessage });
527}
528
529// Advance to next phase
530export function advancePhase(session: Session): Phase | null {
531 const nextPhase: Record<Phase, Phase | null> = {
532 research: "plan",
533 plan: "implement",
534 implement: null,
535 };
536
537 const next = nextPhase[session.phase];
538 if (next) {
539 updateSession(session.id, { phase: next });
540 }
541 return next;
542}
543
544// Read artifact file
545export function readArtifact(projectPath: string, filename: string): string | null {
546 const filePath = path.join(projectPath, ".claude-flow", filename);
547 if (fs.existsSync(filePath)) {
548 return fs.readFileSync(filePath, "utf-8");
549 }
550 return null;
551}
552
553// Write artifact file (for user edits)
554export function writeArtifact(projectPath: string, filename: string, content: string): void {
555 const dir = path.join(projectPath, ".claude-flow");
556 if (!fs.existsSync(dir)) {
557 fs.mkdirSync(dir, { recursive: true });
558 }
559 fs.writeFileSync(path.join(dir, filename), content, "utf-8");
560}
561```
562
563---
564
565## Phase 3: IPC Layer
566
567### 3.1 Preload (`src/main/preload.ts`)
568
569```typescript
570import { contextBridge, ipcRenderer, IpcRendererEvent } from "electron";
571
572contextBridge.exposeInMainWorld("api", {
573 // Projects
574 listProjects: () => ipcRenderer.invoke("projects:list"),
575 createProject: (name: string, path: string) => ipcRenderer.invoke("projects:create", name, path),
576 deleteProject: (id: string) => ipcRenderer.invoke("projects:delete", id),
577
578 // Sessions
579 listSessions: (projectId: string) => ipcRenderer.invoke("sessions:list", projectId),
580 createSession: (projectId: string, name: string) => ipcRenderer.invoke("sessions:create", projectId, name),
581 deleteSession: (id: string) => ipcRenderer.invoke("sessions:delete", id),
582 getSession: (id: string) => ipcRenderer.invoke("sessions:get", id),
583
584 // Chat
585 sendMessage: (sessionId: string, message: string) => ipcRenderer.invoke("chat:send", sessionId, message),
586 interruptSession: (sessionId: string) => ipcRenderer.invoke("chat:interrupt", sessionId),
587
588 // Workflow
589 triggerReview: (sessionId: string) => ipcRenderer.invoke("workflow:review", sessionId),
590 advancePhase: (sessionId: string) => ipcRenderer.invoke("workflow:advance", sessionId),
591 setPermissionMode: (sessionId: string, mode: string) => ipcRenderer.invoke("workflow:setPermissionMode", sessionId, mode),
592
593 // Artifacts
594 readArtifact: (projectPath: string, filename: string) => ipcRenderer.invoke("artifact:read", projectPath, filename),
595 writeArtifact: (projectPath: string, filename: string, content: string) =>
596 ipcRenderer.invoke("artifact:write", projectPath, filename, content),
597
598 // Events
599 onClaudeMessage: (callback: (sessionId: string, message: any) => void) => {
600 const handler = (_: IpcRendererEvent, sessionId: string, message: any) => callback(sessionId, message);
601 ipcRenderer.on("claude:message", handler);
602 return () => ipcRenderer.removeListener("claude:message", handler);
603 },
604
605 // Dialogs
606 selectDirectory: () => ipcRenderer.invoke("dialog:selectDirectory"),
607});
608```
609
610### 3.2 IPC Handlers (`src/main/ipc/handlers.ts`)
611
612```typescript
613import { ipcMain, dialog, BrowserWindow } from "electron";
614import * as projects from "../db/projects";
615import * as sessions from "../db/sessions";
616import * as claude from "../claude";
617
618export function registerIpcHandlers(mainWindow: BrowserWindow) {
619 // Projects
620 ipcMain.handle("projects:list", () => projects.listProjects());
621 ipcMain.handle("projects:create", (_, name: string, path: string) => projects.createProject(name, path));
622 ipcMain.handle("projects:delete", (_, id: string) => projects.deleteProject(id));
623
624 // Sessions
625 ipcMain.handle("sessions:list", (_, projectId: string) => sessions.listSessions(projectId));
626 ipcMain.handle("sessions:create", (_, projectId: string, name: string) => sessions.createSession(projectId, name));
627 ipcMain.handle("sessions:delete", (_, id: string) => sessions.deleteSession(id));
628 ipcMain.handle("sessions:get", (_, id: string) => sessions.getSession(id));
629
630 // Chat
631 ipcMain.handle("chat:send", async (_, sessionId: string, message: string) => {
632 const session = sessions.getSession(sessionId);
633 if (!session) throw new Error("Session not found");
634
635 sessions.addMessage(sessionId, "user", message);
636
637 await claude.sendMessage({
638 session,
639 message,
640 onMessage: (msg) => {
641 mainWindow.webContents.send("claude:message", sessionId, msg);
642
643 if (msg.type === "assistant") {
644 const content = msg.message.content
645 .filter((c: any) => c.type === "text")
646 .map((c: any) => c.text)
647 .join("\n");
648 if (content) {
649 sessions.addMessage(sessionId, "assistant", content);
650 }
651 }
652 },
653 });
654 });
655
656 ipcMain.handle("chat:interrupt", (_, sessionId: string) => {
657 claude.interruptSession(sessionId);
658 });
659
660 // Workflow
661 ipcMain.handle("workflow:review", async (_, sessionId: string) => {
662 const session = sessions.getSession(sessionId);
663 if (!session) throw new Error("Session not found");
664
665 await claude.triggerReview(session, (msg) => {
666 mainWindow.webContents.send("claude:message", sessionId, msg);
667 });
668 });
669
670 ipcMain.handle("workflow:advance", (_, sessionId: string) => {
671 const session = sessions.getSession(sessionId);
672 if (!session) throw new Error("Session not found");
673 return claude.advancePhase(session);
674 });
675
676 ipcMain.handle("workflow:setPermissionMode", (_, sessionId: string, mode: string) => {
677 sessions.updateSession(sessionId, { permission_mode: mode as sessions.PermissionMode });
678 });
679
680 // Artifacts
681 ipcMain.handle("artifact:read", (_, projectPath: string, filename: string) => {
682 return claude.readArtifact(projectPath, filename);
683 });
684
685 ipcMain.handle("artifact:write", (_, projectPath: string, filename: string, content: string) => {
686 claude.writeArtifact(projectPath, filename, content);
687 });
688
689 // Dialogs
690 ipcMain.handle("dialog:selectDirectory", async () => {
691 const result = await dialog.showOpenDialog(mainWindow, { properties: ["openDirectory"] });
692 return result.canceled ? null : result.filePaths[0];
693 });
694}
695```
696
697### 3.3 Main Entry (`src/main/index.ts`)
698
699```typescript
700import { app, BrowserWindow } from "electron";
701import path from "node:path";
702import { getDb, closeDb } from "./db";
703import { registerIpcHandlers } from "./ipc/handlers";
704
705const isDev = !app.isPackaged;
706let mainWindow: BrowserWindow | null = null;
707
708function createWindow() {
709 mainWindow = new BrowserWindow({
710 width: 1400,
711 height: 900,
712 minWidth: 1000,
713 minHeight: 600,
714 show: false,
715 titleBarStyle: "hiddenInset",
716 webPreferences: {
717 contextIsolation: true,
718 nodeIntegration: false,
719 preload: path.join(__dirname, "preload.js"),
720 },
721 });
722
723 registerIpcHandlers(mainWindow);
724
725 if (isDev) {
726 const url = process.env.VITE_DEV_SERVER_URL ?? "http://localhost:5173";
727 mainWindow.loadURL(url).finally(() => {
728 mainWindow!.show();
729 mainWindow!.webContents.openDevTools({ mode: "detach" });
730 });
731 } else {
732 const indexHtml = path.join(app.getAppPath(), "renderer", "dist", "index.html");
733 mainWindow.loadFile(indexHtml).finally(() => mainWindow!.show());
734 }
735}
736
737app.whenReady().then(() => {
738 getDb();
739 createWindow();
740 app.on("activate", () => {
741 if (BrowserWindow.getAllWindows().length === 0) createWindow();
742 });
743});
744
745app.on("window-all-closed", () => {
746 closeDb();
747 if (process.platform !== "darwin") app.quit();
748});
749```
750
751---
752
753## Phase 4: React UI
754
755### 4.1 Types (`renderer/src/types.ts`)
756
757```typescript
758export interface Project {
759 id: string;
760 name: string;
761 path: string;
762 created_at: number;
763 updated_at: number;
764}
765
766export interface Session {
767 id: string;
768 project_id: string;
769 name: string;
770 phase: Phase;
771 claude_session_id: string | null;
772 permission_mode: PermissionMode;
773 created_at: number;
774 updated_at: number;
775}
776
777export type Phase = "research" | "plan" | "implement";
778export type PermissionMode = "acceptEdits" | "bypassPermissions";
779
780export interface Message {
781 id: string;
782 session_id: string;
783 role: "user" | "assistant";
784 content: string;
785 created_at: number;
786}
787
788export interface TokenUsage {
789 inputTokens: number;
790 outputTokens: number;
791 cacheHits?: number;
792}
793```
794
795### 4.2 App Component (`renderer/src/App.tsx`)
796
797```typescript
798import React, { useState, useEffect, useCallback } from "react";
799import { Header } from "./components/Header";
800import { DocumentPane } from "./components/DocumentPane";
801import { ChatPane } from "./components/ChatPane";
802import { ActionBar } from "./components/ActionBar";
803import type { Project, Session, Message, TokenUsage } from "./types";
804import "./styles/globals.css";
805
806const api = window.api;
807
808export function App() {
809 const [projects, setProjects] = useState<Project[]>([]);
810 const [sessions, setSessions] = useState<Session[]>([]);
811 const [selectedProject, setSelectedProject] = useState<Project | null>(null);
812 const [selectedSession, setSelectedSession] = useState<Session | null>(null);
813 const [messages, setMessages] = useState<Message[]>([]);
814 const [documentContent, setDocumentContent] = useState<string>("");
815 const [originalContent, setOriginalContent] = useState<string>("");
816 const [isLoading, setIsLoading] = useState(false);
817 const [tokenUsage, setTokenUsage] = useState<TokenUsage>({ inputTokens: 0, outputTokens: 0 });
818
819 const hasChanges = documentContent !== originalContent;
820
821 // Load projects on mount
822 useEffect(() => {
823 api.listProjects().then(setProjects);
824 }, []);
825
826 // Load sessions when project changes
827 useEffect(() => {
828 if (selectedProject) {
829 api.listSessions(selectedProject.id).then(setSessions);
830 } else {
831 setSessions([]);
832 }
833 }, [selectedProject]);
834
835 // Load artifact when session/phase changes
836 useEffect(() => {
837 if (selectedSession && selectedProject) {
838 const filename = selectedSession.phase === "research" ? "research.md" : "plan.md";
839 api.readArtifact(selectedProject.path, filename).then((content) => {
840 const text = content || "";
841 setDocumentContent(text);
842 setOriginalContent(text);
843 });
844 }
845 }, [selectedSession?.id, selectedSession?.phase, selectedProject]);
846
847 // Subscribe to Claude messages
848 useEffect(() => {
849 const unsubscribe = api.onClaudeMessage((sessionId, msg) => {
850 if (sessionId !== selectedSession?.id) return;
851
852 if (msg.type === "result") {
853 setIsLoading(false);
854 if (msg.usage) {
855 setTokenUsage({
856 inputTokens: msg.usage.input_tokens,
857 outputTokens: msg.usage.output_tokens,
858 cacheHits: msg.usage.cache_read_input_tokens,
859 });
860 }
861 // Reload artifact after Claude updates it
862 if (selectedProject) {
863 const filename = selectedSession.phase === "research" ? "research.md" : "plan.md";
864 api.readArtifact(selectedProject.path, filename).then((content) => {
865 const text = content || "";
866 setDocumentContent(text);
867 setOriginalContent(text);
868 });
869 }
870 }
871
872 if (msg.type === "assistant") {
873 const content = msg.message.content
874 .filter((c: any) => c.type === "text")
875 .map((c: any) => c.text)
876 .join("\n");
877 if (content) {
878 setMessages((prev) => {
879 const last = prev[prev.length - 1];
880 if (last?.role === "assistant") {
881 return [...prev.slice(0, -1), { ...last, content }];
882 }
883 return [...prev, { id: crypto.randomUUID(), session_id: sessionId, role: "assistant", content, created_at: Date.now() / 1000 }];
884 });
885 }
886 }
887 });
888
889 return unsubscribe;
890 }, [selectedSession?.id, selectedSession?.phase, selectedProject]);
891
892 const handleSendMessage = async (message: string) => {
893 if (!selectedSession) return;
894 setIsLoading(true);
895 setMessages((prev) => [...prev, { id: crypto.randomUUID(), session_id: selectedSession.id, role: "user", content: message, created_at: Date.now() / 1000 }]);
896 await api.sendMessage(selectedSession.id, message);
897 };
898
899 const handleReview = async () => {
900 if (!selectedSession || !selectedProject) return;
901 // Save user edits first
902 const filename = selectedSession.phase === "research" ? "research.md" : "plan.md";
903 await api.writeArtifact(selectedProject.path, filename, documentContent);
904 setOriginalContent(documentContent);
905 setIsLoading(true);
906 await api.triggerReview(selectedSession.id);
907 };
908
909 const handleSubmit = async () => {
910 if (!selectedSession || !selectedProject) return;
911 // Save any pending edits
912 const filename = selectedSession.phase === "research" ? "research.md" : "plan.md";
913 await api.writeArtifact(selectedProject.path, filename, documentContent);
914
915 const nextPhase = await api.advancePhase(selectedSession.id);
916 if (nextPhase) {
917 setSelectedSession({ ...selectedSession, phase: nextPhase });
918 // Trigger initial message for next phase
919 setIsLoading(true);
920 const initialMsg = nextPhase === "plan"
921 ? "Create a detailed implementation plan based on the research."
922 : "Begin implementing the plan.";
923 await api.sendMessage(selectedSession.id, initialMsg);
924 }
925 };
926
927 const handleCreateProject = async () => {
928 const path = await api.selectDirectory();
929 if (!path) return;
930 const name = path.split("/").pop() || "New Project";
931 const project = await api.createProject(name, path);
932 setProjects((prev) => [project, ...prev]);
933 setSelectedProject(project);
934 };
935
936 const handleCreateSession = async () => {
937 if (!selectedProject) return;
938 const name = `Session ${sessions.length + 1}`;
939 const session = await api.createSession(selectedProject.id, name);
940 setSessions((prev) => [session, ...prev]);
941 setSelectedSession(session);
942 setMessages([]);
943 setDocumentContent("");
944 setOriginalContent("");
945 };
946
947 return (
948 <div className="app">
949 <Header
950 projects={projects}
951 sessions={sessions}
952 selectedProject={selectedProject}
953 selectedSession={selectedSession}
954 onSelectProject={setSelectedProject}
955 onSelectSession={setSelectedSession}
956 onCreateProject={handleCreateProject}
957 onCreateSession={handleCreateSession}
958 />
959
960 <div className="main-content">
961 <DocumentPane
962 content={documentContent}
963 onChange={setDocumentContent}
964 phase={selectedSession?.phase || "research"}
965 disabled={!selectedSession || selectedSession.phase === "implement"}
966 />
967
968 <ChatPane
969 messages={messages}
970 onSend={handleSendMessage}
971 isLoading={isLoading}
972 disabled={!selectedSession}
973 placeholder={selectedSession ? `Chat with Claude (${selectedSession.phase})...` : "Select a session to start"}
974 />
975 </div>
976
977 <ActionBar
978 phase={selectedSession?.phase || "research"}
979 hasChanges={hasChanges}
980 isLoading={isLoading}
981 tokenUsage={tokenUsage}
982 permissionMode={selectedSession?.permission_mode || "acceptEdits"}
983 onReview={handleReview}
984 onSubmit={handleSubmit}
985 onPermissionModeChange={(mode) => {
986 if (selectedSession) {
987 api.setPermissionMode(selectedSession.id, mode);
988 setSelectedSession({ ...selectedSession, permission_mode: mode });
989 }
990 }}
991 disabled={!selectedSession}
992 />
993 </div>
994 );
995}
996```
997
998### 4.3 Header (`renderer/src/components/Header.tsx`)
999
1000```typescript
1001import React from "react";
1002import type { Project, Session, Phase } from "../types";
1003
1004interface HeaderProps {
1005 projects: Project[];
1006 sessions: Session[];
1007 selectedProject: Project | null;
1008 selectedSession: Session | null;
1009 onSelectProject: (project: Project | null) => void;
1010 onSelectSession: (session: Session | null) => void;
1011 onCreateProject: () => void;
1012 onCreateSession: () => void;
1013}
1014
1015const phaseLabels: Record<Phase, string> = {
1016 research: "Research",
1017 plan: "Plan",
1018 implement: "Implement",
1019};
1020
1021export function Header({
1022 projects, sessions, selectedProject, selectedSession,
1023 onSelectProject, onSelectSession, onCreateProject, onCreateSession,
1024}: HeaderProps) {
1025 return (
1026 <header className="header">
1027 <div className="header-left">
1028 <select
1029 value={selectedProject?.id || ""}
1030 onChange={(e) => {
1031 const project = projects.find((p) => p.id === e.target.value);
1032 onSelectProject(project || null);
1033 onSelectSession(null);
1034 }}
1035 >
1036 <option value="">Select Project...</option>
1037 {projects.map((p) => (
1038 <option key={p.id} value={p.id}>{p.name}</option>
1039 ))}
1040 </select>
1041 <button onClick={onCreateProject}>+ Project</button>
1042
1043 {selectedProject && (
1044 <>
1045 <select
1046 value={selectedSession?.id || ""}
1047 onChange={(e) => {
1048 const session = sessions.find((s) => s.id === e.target.value);
1049 onSelectSession(session || null);
1050 }}
1051 >
1052 <option value="">Select Session...</option>
1053 {sessions.map((s) => (
1054 <option key={s.id} value={s.id}>{s.name}</option>
1055 ))}
1056 </select>
1057 <button onClick={onCreateSession}>+ Session</button>
1058 </>
1059 )}
1060 </div>
1061
1062 <div className="header-right">
1063 {selectedSession && (
1064 <div className="phase-indicator">
1065 {(["research", "plan", "implement"] as Phase[]).map((phase) => (
1066 <span
1067 key={phase}
1068 className={`phase-step ${selectedSession.phase === phase ? "active" : ""} ${
1069 (["research", "plan", "implement"].indexOf(phase) <
1070 ["research", "plan", "implement"].indexOf(selectedSession.phase)) ? "complete" : ""
1071 }`}
1072 >
1073 {phaseLabels[phase]}
1074 </span>
1075 ))}
1076 </div>
1077 )}
1078 </div>
1079 </header>
1080 );
1081}
1082```
1083
1084### 4.4 DocumentPane (`renderer/src/components/DocumentPane.tsx`)
1085
1086```typescript
1087import React, { useMemo } from "react";
1088import type { Phase } from "../types";
1089
1090interface DocumentPaneProps {
1091 content: string;
1092 onChange: (content: string) => void;
1093 phase: Phase;
1094 disabled: boolean;
1095}
1096
1097// Simple markdown renderer (can be replaced with a library like react-markdown)
1098function renderMarkdown(md: string): string {
1099 return md
1100 // Headers
1101 .replace(/^### (.*$)/gm, '<h3>$1</h3>')
1102 .replace(/^## (.*$)/gm, '<h2>$1</h2>')
1103 .replace(/^# (.*$)/gm, '<h1>$1</h1>')
1104 // Bold/italic
1105 .replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>')
1106 .replace(/\*([^*]+)\*/g, '<em>$1</em>')
1107 // Code blocks
1108 .replace(/```(\w*)\n([\s\S]*?)```/g, '<pre><code class="language-$1">$2</code></pre>')
1109 .replace(/`([^`]+)`/g, '<code>$1</code>')
1110 // Lists
1111 .replace(/^\- \[x\] (.*$)/gm, '<li class="task done">☑ $1</li>')
1112 .replace(/^\- \[ \] (.*$)/gm, '<li class="task">☐ $1</li>')
1113 .replace(/^\- (.*$)/gm, '<li>$1</li>')
1114 // Review comments (highlight them)
1115 .replace(/(\/\/ REVIEW:.*$)/gm, '<mark class="review">$1</mark>')
1116 .replace(/(\/\/ NOTE:.*$)/gm, '<mark class="note">$1</mark>')
1117 // Paragraphs
1118 .replace(/\n\n/g, '</p><p>')
1119 .replace(/^(.+)$/gm, '<p>$1</p>')
1120 // Clean up
1121 .replace(/<p><\/p>/g, '')
1122 .replace(/<p>(<h[1-3]>)/g, '$1')
1123 .replace(/(<\/h[1-3]>)<\/p>/g, '$1');
1124}
1125
1126export function DocumentPane({ content, onChange, phase, disabled }: DocumentPaneProps) {
1127 const [isEditing, setIsEditing] = React.useState(false);
1128 const renderedHtml = useMemo(() => renderMarkdown(content), [content]);
1129
1130 if (phase === "implement") {
1131 // In implement phase, show read-only rendered view
1132 return (
1133 <div className="document-pane">
1134 <div className="document-header">
1135 <span>plan.md</span>
1136 <span className="badge">Implementing...</span>
1137 </div>
1138 <div
1139 className="document-content rendered"
1140 dangerouslySetInnerHTML={{ __html: renderedHtml }}
1141 />
1142 </div>
1143 );
1144 }
1145
1146 const filename = phase === "research" ? "research.md" : "plan.md";
1147
1148 return (
1149 <div className="document-pane">
1150 <div className="document-header">
1151 <span>{filename}</span>
1152 <button onClick={() => setIsEditing(!isEditing)}>
1153 {isEditing ? "Preview" : "Edit"}
1154 </button>
1155 </div>
1156
1157 {isEditing ? (
1158 <textarea
1159 className="document-content editing"
1160 value={content}
1161 onChange={(e) => onChange(e.target.value)}
1162 disabled={disabled}
1163 placeholder={`${filename} will appear here...`}
1164 />
1165 ) : (
1166 <div
1167 className="document-content rendered"
1168 dangerouslySetInnerHTML={{ __html: renderedHtml || '<p class="empty">Document will appear here after Claude generates it...</p>' }}
1169 onClick={() => !disabled && setIsEditing(true)}
1170 />
1171 )}
1172 </div>
1173 );
1174}
1175```
1176
1177### 4.5 ChatPane (`renderer/src/components/ChatPane.tsx`)
1178
1179```typescript
1180import React, { useState, useRef, useEffect } from "react";
1181import type { Message } from "../types";
1182
1183interface ChatPaneProps {
1184 messages: Message[];
1185 onSend: (message: string) => void;
1186 isLoading: boolean;
1187 disabled: boolean;
1188 placeholder: string;
1189}
1190
1191export function ChatPane({ messages, onSend, isLoading, disabled, placeholder }: ChatPaneProps) {
1192 const [input, setInput] = useState("");
1193 const messagesEndRef = useRef<HTMLDivElement>(null);
1194
1195 useEffect(() => {
1196 messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
1197 }, [messages]);
1198
1199 const handleSend = () => {
1200 if (!input.trim() || isLoading || disabled) return;
1201 onSend(input.trim());
1202 setInput("");
1203 };
1204
1205 return (
1206 <div className="chat-pane">
1207 <div className="chat-messages">
1208 {messages.map((msg) => (
1209 <div key={msg.id} className={`message ${msg.role}`}>
1210 <div className="message-content">{msg.content}</div>
1211 </div>
1212 ))}
1213 {isLoading && (
1214 <div className="message assistant loading">
1215 <div className="message-content">Thinking...</div>
1216 </div>
1217 )}
1218 <div ref={messagesEndRef} />
1219 </div>
1220
1221 <div className="chat-input">
1222 <input
1223 type="text"
1224 value={input}
1225 onChange={(e) => setInput(e.target.value)}
1226 onKeyDown={(e) => e.key === "Enter" && !e.shiftKey && handleSend()}
1227 placeholder={placeholder}
1228 disabled={disabled || isLoading}
1229 />
1230 <button onClick={handleSend} disabled={disabled || isLoading || !input.trim()}>
1231 Send
1232 </button>
1233 </div>
1234 </div>
1235 );
1236}
1237```
1238
1239### 4.6 ActionBar (`renderer/src/components/ActionBar.tsx`)
1240
1241```typescript
1242import React from "react";
1243import type { Phase, PermissionMode, TokenUsage } from "../types";
1244
1245interface ActionBarProps {
1246 phase: Phase;
1247 hasChanges: boolean;
1248 isLoading: boolean;
1249 tokenUsage: TokenUsage;
1250 permissionMode: PermissionMode;
1251 onReview: () => void;
1252 onSubmit: () => void;
1253 onPermissionModeChange: (mode: PermissionMode) => void;
1254 disabled: boolean;
1255}
1256
1257export function ActionBar({
1258 phase, hasChanges, isLoading, tokenUsage, permissionMode,
1259 onReview, onSubmit, onPermissionModeChange, disabled,
1260}: ActionBarProps) {
1261 const totalTokens = tokenUsage.inputTokens + tokenUsage.outputTokens;
1262 const maxTokens = 200000;
1263 const usagePercent = Math.min((totalTokens / maxTokens) * 100, 100);
1264
1265 const getBarColor = () => {
1266 if (usagePercent > 80) return "#ef4444";
1267 if (usagePercent > 50) return "#f59e0b";
1268 return "#10b981";
1269 };
1270
1271 return (
1272 <div className="action-bar">
1273 <div className="action-bar-left">
1274 <div className="token-indicator">
1275 <div className="token-bar">
1276 <div className="token-fill" style={{ width: `${usagePercent}%`, backgroundColor: getBarColor() }} />
1277 </div>
1278 <span className="token-label">
1279 {(totalTokens / 1000).toFixed(1)}k / 200k
1280 </span>
1281 </div>
1282
1283 {phase === "implement" && (
1284 <label className="permission-toggle">
1285 <input
1286 type="checkbox"
1287 checked={permissionMode === "bypassPermissions"}
1288 onChange={(e) => onPermissionModeChange(e.target.checked ? "bypassPermissions" : "acceptEdits")}
1289 disabled={disabled}
1290 />
1291 Bypass Permissions
1292 </label>
1293 )}
1294 </div>
1295
1296 <div className="action-bar-right">
1297 {phase !== "implement" && (
1298 <>
1299 <button
1300 onClick={onReview}
1301 disabled={disabled || isLoading || !hasChanges}
1302 className="btn-secondary"
1303 >
1304 Review
1305 </button>
1306 <button
1307 onClick={onSubmit}
1308 disabled={disabled || isLoading}
1309 className="btn-primary"
1310 >
1311 Submit →
1312 </button>
1313 </>
1314 )}
1315 {phase === "implement" && isLoading && (
1316 <span className="implementing-status">Implementing...</span>
1317 )}
1318 </div>
1319 </div>
1320 );
1321}
1322```
1323
1324### 4.7 Styles (`renderer/src/styles/globals.css`)
1325
1326```css
1327* {
1328 box-sizing: border-box;
1329 margin: 0;
1330 padding: 0;
1331}
1332
1333:root {
1334 --bg-primary: #1a1a1a;
1335 --bg-secondary: #252525;
1336 --bg-tertiary: #333;
1337 --border: #444;
1338 --text-primary: #e0e0e0;
1339 --text-secondary: #888;
1340 --accent: #3b82f6;
1341 --accent-hover: #2563eb;
1342 --success: #10b981;
1343 --warning: #f59e0b;
1344 --danger: #ef4444;
1345}
1346
1347body {
1348 font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
1349 background: var(--bg-primary);
1350 color: var(--text-primary);
1351 overflow: hidden;
1352}
1353
1354.app {
1355 display: flex;
1356 flex-direction: column;
1357 height: 100vh;
1358}
1359
1360/* Header */
1361.header {
1362 display: flex;
1363 justify-content: space-between;
1364 align-items: center;
1365 padding: 12px 16px;
1366 background: var(--bg-secondary);
1367 border-bottom: 1px solid var(--border);
1368 -webkit-app-region: drag;
1369}
1370
1371.header-left, .header-right {
1372 display: flex;
1373 align-items: center;
1374 gap: 8px;
1375 -webkit-app-region: no-drag;
1376}
1377
1378.header select, .header button {
1379 padding: 6px 12px;
1380 background: var(--bg-tertiary);
1381 border: 1px solid var(--border);
1382 border-radius: 4px;
1383 color: var(--text-primary);
1384 cursor: pointer;
1385}
1386
1387.header button:hover {
1388 background: var(--border);
1389}
1390
1391.phase-indicator {
1392 display: flex;
1393 gap: 4px;
1394}
1395
1396.phase-step {
1397 padding: 4px 12px;
1398 font-size: 12px;
1399 border-radius: 4px;
1400 background: var(--bg-tertiary);
1401 color: var(--text-secondary);
1402}
1403
1404.phase-step.active {
1405 background: var(--accent);
1406 color: white;
1407}
1408
1409.phase-step.complete {
1410 background: var(--success);
1411 color: white;
1412}
1413
1414/* Main Content */
1415.main-content {
1416 flex: 1;
1417 display: flex;
1418 overflow: hidden;
1419}
1420
1421/* Document Pane */
1422.document-pane {
1423 flex: 1;
1424 display: flex;
1425 flex-direction: column;
1426 border-right: 1px solid var(--border);
1427}
1428
1429.document-header {
1430 display: flex;
1431 justify-content: space-between;
1432 align-items: center;
1433 padding: 8px 16px;
1434 background: var(--bg-secondary);
1435 border-bottom: 1px solid var(--border);
1436 font-size: 14px;
1437 color: var(--text-secondary);
1438}
1439
1440.document-header button {
1441 padding: 4px 8px;
1442 background: var(--bg-tertiary);
1443 border: 1px solid var(--border);
1444 border-radius: 4px;
1445 color: var(--text-primary);
1446 cursor: pointer;
1447 font-size: 12px;
1448}
1449
1450.document-content {
1451 flex: 1;
1452 overflow-y: auto;
1453 padding: 24px;
1454}
1455
1456.document-content.editing {
1457 font-family: "SF Mono", Monaco, "Cascadia Code", monospace;
1458 font-size: 14px;
1459 line-height: 1.6;
1460 background: var(--bg-primary);
1461 border: none;
1462 resize: none;
1463 color: var(--text-primary);
1464}
1465
1466.document-content.rendered {
1467 line-height: 1.7;
1468}
1469
1470.document-content.rendered h1 { font-size: 28px; margin: 24px 0 16px; }
1471.document-content.rendered h2 { font-size: 22px; margin: 20px 0 12px; color: var(--text-secondary); }
1472.document-content.rendered h3 { font-size: 18px; margin: 16px 0 8px; }
1473.document-content.rendered p { margin: 8px 0; }
1474.document-content.rendered code { background: var(--bg-tertiary); padding: 2px 6px; border-radius: 4px; font-size: 13px; }
1475.document-content.rendered pre { background: var(--bg-tertiary); padding: 16px; border-radius: 8px; overflow-x: auto; margin: 16px 0; }
1476.document-content.rendered pre code { background: none; padding: 0; }
1477.document-content.rendered li { margin-left: 24px; margin-bottom: 4px; }
1478.document-content.rendered li.task { list-style: none; margin-left: 0; }
1479.document-content.rendered li.task.done { color: var(--success); }
1480.document-content.rendered mark.review { background: var(--warning); color: black; padding: 2px 4px; border-radius: 2px; }
1481.document-content.rendered mark.note { background: var(--accent); color: white; padding: 2px 4px; border-radius: 2px; }
1482.document-content.rendered .empty { color: var(--text-secondary); font-style: italic; }
1483
1484.badge {
1485 background: var(--accent);
1486 color: white;
1487 padding: 2px 8px;
1488 border-radius: 4px;
1489 font-size: 11px;
1490}
1491
1492/* Chat Pane */
1493.chat-pane {
1494 width: 380px;
1495 display: flex;
1496 flex-direction: column;
1497 background: var(--bg-secondary);
1498}
1499
1500.chat-messages {
1501 flex: 1;
1502 overflow-y: auto;
1503 padding: 16px;
1504}
1505
1506.message {
1507 margin-bottom: 12px;
1508 padding: 10px 14px;
1509 border-radius: 8px;
1510 max-width: 90%;
1511 font-size: 14px;
1512 line-height: 1.5;
1513}
1514
1515.message.user {
1516 background: var(--accent);
1517 margin-left: auto;
1518}
1519
1520.message.assistant {
1521 background: var(--bg-tertiary);
1522}
1523
1524.message.loading {
1525 color: var(--text-secondary);
1526 font-style: italic;
1527}
1528
1529.chat-input {
1530 display: flex;
1531 gap: 8px;
1532 padding: 12px;
1533 border-top: 1px solid var(--border);
1534}
1535
1536.chat-input input {
1537 flex: 1;
1538 padding: 10px 14px;
1539 background: var(--bg-tertiary);
1540 border: 1px solid var(--border);
1541 border-radius: 8px;
1542 color: var(--text-primary);
1543}
1544
1545.chat-input button {
1546 padding: 10px 16px;
1547 background: var(--accent);
1548 border: none;
1549 border-radius: 8px;
1550 color: white;
1551 cursor: pointer;
1552}
1553
1554.chat-input button:disabled {
1555 opacity: 0.5;
1556 cursor: not-allowed;
1557}
1558
1559/* Action Bar */
1560.action-bar {
1561 display: flex;
1562 justify-content: space-between;
1563 align-items: center;
1564 padding: 12px 16px;
1565 background: var(--bg-secondary);
1566 border-top: 1px solid var(--border);
1567}
1568
1569.action-bar-left, .action-bar-right {
1570 display: flex;
1571 align-items: center;
1572 gap: 16px;
1573}
1574
1575.token-indicator {
1576 display: flex;
1577 align-items: center;
1578 gap: 8px;
1579}
1580
1581.token-bar {
1582 width: 100px;
1583 height: 6px;
1584 background: var(--bg-tertiary);
1585 border-radius: 3px;
1586 overflow: hidden;
1587}
1588
1589.token-fill {
1590 height: 100%;
1591 transition: width 0.3s ease;
1592}
1593
1594.token-label {
1595 font-size: 12px;
1596 color: var(--text-secondary);
1597}
1598
1599.permission-toggle {
1600 display: flex;
1601 align-items: center;
1602 gap: 6px;
1603 font-size: 13px;
1604 color: var(--text-secondary);
1605 cursor: pointer;
1606}
1607
1608.btn-secondary {
1609 padding: 8px 16px;
1610 background: var(--bg-tertiary);
1611 border: 1px solid var(--border);
1612 border-radius: 6px;
1613 color: var(--text-primary);
1614 cursor: pointer;
1615}
1616
1617.btn-secondary:disabled {
1618 opacity: 0.5;
1619 cursor: not-allowed;
1620}
1621
1622.btn-primary {
1623 padding: 8px 20px;
1624 background: var(--accent);
1625 border: none;
1626 border-radius: 6px;
1627 color: white;
1628 cursor: pointer;
1629 font-weight: 500;
1630}
1631
1632.btn-primary:hover:not(:disabled) {
1633 background: var(--accent-hover);
1634}
1635
1636.btn-primary:disabled {
1637 opacity: 0.5;
1638 cursor: not-allowed;
1639}
1640
1641.implementing-status {
1642 color: var(--success);
1643 font-size: 14px;
1644}
1645```
1646
1647---
1648
1649## TODO List
1650
1651### Phase 1: Database Layer
1652- [x] Create `src/main/db/` directory
1653- [x] Implement `src/main/db/schema.ts`
1654- [x] Implement `src/main/db/index.ts`
1655- [x] Implement `src/main/db/projects.ts`
1656- [x] Implement `src/main/db/sessions.ts`
1657
1658### Phase 2: Claude Integration
1659- [x] Create `src/main/claude/` directory
1660- [x] Implement `src/main/claude/phases.ts`
1661- [x] Implement `src/main/claude/index.ts`
1662- [x] Add `@anthropic-ai/claude-agent-sdk` dependency
1663
1664### Phase 3: IPC Layer
1665- [x] Implement `src/main/preload.ts`
1666- [x] Implement `src/main/ipc/handlers.ts`
1667- [x] Update `src/main/index.ts`
1668
1669### Phase 4: React UI
1670- [x] Create `renderer/src/types.ts`
1671- [x] Create `renderer/src/lib/api.ts` (declare window.api types)
1672- [x] Implement `renderer/src/App.tsx`
1673- [x] Implement `renderer/src/components/Header.tsx`
1674- [x] Implement `renderer/src/components/DocumentPane.tsx`
1675- [x] Implement `renderer/src/components/ChatPane.tsx`
1676- [x] Implement `renderer/src/components/ActionBar.tsx`
1677- [x] Create `renderer/src/styles/globals.css`
1678- [x] Update `renderer/src/main.tsx`
1679
1680### Phase 5: Integration & Polish
1681- [x] Add `uuid` dependency
1682- [x] Test full workflow: Research → Plan → Implement
1683- [x] Add error handling and loading states
1684- [x] Add keyboard shortcuts