aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorbndw <ben@bdw.to>2026-02-28 19:14:01 -0800
committerbndw <ben@bdw.to>2026-02-28 19:14:01 -0800
commit9a636af9090b122db2e55737fca3e78550aab9df (patch)
treef76f3b118b525907e92fb29df096567a6eeabd06
parente2a0bc68726c1b8dca179ee1f6826b88d8dd09f5 (diff)
fix: scope artifacts to sessions
-rw-r--r--.gitignore7
-rw-r--r--CLAUDE.md53
-rw-r--r--README.md4
-rw-r--r--plan.md1684
-rw-r--r--research.md414
-rw-r--r--src/main/claude/index.ts51
-rw-r--r--src/main/claude/phases.ts61
7 files changed, 116 insertions, 2158 deletions
diff --git a/.gitignore b/.gitignore
index 4569d3b..9075af5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,3 @@
1node_modules 1.claude-flow/
2release 2dist/
3dist 3node_modules/
4*.sync-conflict-*
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 0000000..e54032e
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,53 @@
1# Claude Flow — Codebase Overview
2
3## What This Is
4
5An Electron desktop app that enforces a **Research → Plan → Implement** workflow for AI-assisted coding, using `@anthropic-ai/claude-agent-sdk`.
6
7## Tech Stack
8
9- **Electron 38** + **Vite** + **React 19** + **TypeScript**
10- **better-sqlite3** — local persistence (projects, sessions, messages)
11- **@anthropic-ai/claude-agent-sdk** — Claude integration
12- **CodeMirror 6** — markdown editor in DocumentPane
13- **react-markdown + remark-gfm** — markdown renderer
14
15## Project Structure
16
17```
18src/main/ # Electron main process
19 claude/ # SDK integration + phase configs
20 db/ # SQLite layer (schema, projects, sessions)
21 ipc/ # IPC channel registrations
22 index.ts # App entry, BrowserWindow
23 preload.ts # contextBridge API surface
24renderer/src/ # React UI
25 App.tsx # Root — all state management
26 components/ # Header, DocumentPane, ChatPane, ActionBar
27 types.ts # Shared TypeScript types
28 styles/ # CSS variables (dark/light themes)
29```
30
31## Key Patterns
32
33### Phase System
34Phases: `research | plan | implement`. Each defined in `src/main/claude/phases.ts` with its own `systemPrompt`, `tools[]`, `permissionMode`, and `initialMessage`. Phase progression is one-way; triggered by user clicking "Submit".
35
36### Artifact Storage
37Session artifacts (`research.md`, `plan.md`) stored inside the target project at `.claude-flow/sessions/{sessionId}/`. This keeps them within the SDK's allowed write boundary (project `cwd`). Add `.claude-flow/` to `.gitignore` to exclude from version control.
38
39### Session Continuity
40Claude SDK session IDs are captured from the `system:init` message and stored in SQLite. Subsequent turns resume the same SDK session (`options: { resume: claude_session_id }`).
41
42### IPC Pattern
43All renderer→main communication goes through named IPC channels registered in `src/main/ipc/handlers.ts`. Streaming events flow back via `mainWindow.webContents.send("claude:message", sessionId, msg)` and are received via `ipcRenderer.on()`.
44
45### Database
46SQLite at `app.getPath("userData")/claude-flow.db`. Tables: `projects`, `sessions`, `messages`. Foreign keys ON, WAL mode enabled.
47
48## Important Notes
49
50- `ANTHROPIC_API_KEY` env var must be set before launching
51- Artifacts are stored in `.claude-flow/sessions/` inside the target project
52- `bypassPermissions` mode is a user-controlled toggle in implement phase only
53- Token usage (from `SDKResultMessage.usage`) is displayed in the ActionBar
diff --git a/README.md b/README.md
index 07d3839..6b619f8 100644
--- a/README.md
+++ b/README.md
@@ -14,12 +14,12 @@ At each phase, edit the document to add notes (`// REVIEW:`, `// NOTE:`), click
14 14
15## Sessions 15## Sessions
16 16
17Each session has isolated artifacts stored in `~/.claude-flow/projects/{projectId}/sessions/{sessionId}/`: 17Each session has isolated artifacts stored inside your project at `.claude-flow/sessions/{sessionId}/`:
18 18
19- `research.md` — Session research 19- `research.md` — Session research
20- `plan.md` — Session plan 20- `plan.md` — Session plan
21 21
22Concurrent sessions supported — switch between them freely. Artifacts live outside your repo so they never get accidentally committed. 22Concurrent sessions supported — switch between them freely. Add `.claude-flow/` to your `.gitignore` to keep artifacts out of version control.
23 23
24## Setup 24## Setup
25 25
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
diff --git a/research.md b/research.md
deleted file mode 100644
index b7d0897..0000000
--- a/research.md
+++ /dev/null
@@ -1,414 +0,0 @@
1# Research: Claude Flow Architecture
2
3## Existing Codebase Analysis
4
5### Template Structure
6
7The starting point is a minimal Electron + Vite + React + better-sqlite3 template:
8
9```
10minimal-electron-vite-react-better-sqlite/
11├── src/main/
12│ ├── index.ts # Electron main process
13│ └── preload.ts # IPC bridge (empty)
14├── renderer/
15│ ├── index.html # Entry HTML
16│ └── src/
17│ └── main.tsx # React entry ("hi")
18├── package.json
19├── tsconfig.json
20└── vite.config.ts
21```
22
23### Key Patterns in Template
24
25**Main Process (`src/main/index.ts`):**
26- Uses `app.isPackaged` for dev/prod detection
27- Database stored in `app.getPath('userData')` — correct for Electron apps
28- Uses WAL mode for SQLite (`journal_mode = WAL`)
29- Window created with `contextIsolation: true`, `nodeIntegration: false` — secure defaults
30- Preload script path: `path.join(__dirname, 'preload.js')`
31
32**Vite Config:**
33- Root is `renderer/` directory
34- Base is `./` for file:// loads in production
35- Dev server on port 5173 with `strictPort: true`
36- Output to `renderer/dist`
37
38**Build/Dev Scripts:**
39- `npm run dev` — concurrent Vite + TypeScript watch + Electron
40- Uses `wait-on tcp:5173` to wait for Vite before launching Electron
41- `@electron/rebuild` handles native module rebuilding
42
43### What's Missing (We Need to Build)
44
451. IPC handlers for renderer → main communication
462. Proper database layer with migrations
473. React components and state management
484. Claude integration
495. Workflow state machine
50
51---
52
53## Claude Agent SDK Research
54
55### Package Information
56
57The SDK has been renamed from "Claude Code SDK" to "Claude Agent SDK".
58
59**NPM Package:** `@anthropic-ai/claude-agent-sdk`
60
61**Installation:**
62```bash
63npm install @anthropic-ai/claude-agent-sdk
64```
65
66**Authentication:**
67- Set `ANTHROPIC_API_KEY` environment variable
68- Also supports Bedrock, Vertex AI, Azure via environment flags
69
70### Core API: `query()`
71
72The primary function returns an async generator that streams messages:
73
74```typescript
75import { query } from "@anthropic-ai/claude-agent-sdk";
76
77for await (const message of query({
78 prompt: "Find and fix the bug in auth.py",
79 options: {
80 allowedTools: ["Read", "Edit", "Bash"],
81 permissionMode: "acceptEdits",
82 cwd: "/path/to/project"
83 }
84})) {
85 console.log(message);
86}
87```
88
89### Built-in Tools
90
91| Tool | Purpose |
92|------|---------|
93| **Read** | Read files (text, images, PDFs, notebooks) |
94| **Write** | Create/overwrite files |
95| **Edit** | Precise string replacements |
96| **Bash** | Run terminal commands |
97| **Glob** | Find files by pattern |
98| **Grep** | Search file contents with regex |
99| **WebSearch** | Search the web |
100| **WebFetch** | Fetch/parse web pages |
101| **Task** | Spawn subagents |
102
103### Permission Modes
104
105| Mode | Behavior |
106|------|----------|
107| `default` | Standard permissions, may prompt |
108| `acceptEdits` | Auto-approve file edits |
109| `bypassPermissions` | Allow everything (dangerous) |
110| `plan` | Planning only, no execution |
111
112**Critical for our workflow:**
113- **Research/Plan/Annotate phases**: Use `plan` mode or restrict `allowedTools` to read-only
114- **Implement phase**: Use `acceptEdits` or `bypassPermissions` // REVIEW: make a user toggle
115
116### System Prompts
117
118Can be customized via options:
119
120```typescript
121options: {
122 systemPrompt: "You are in RESEARCH mode. Read files deeply, write findings to research.md. DO NOT modify any source files.",
123 // OR use preset with append:
124 systemPrompt: {
125 type: "preset",
126 preset: "claude_code",
127 append: "Additional instructions here..."
128 }
129}
130```
131
132### Session Management
133
134Sessions can be resumed using session IDs:
135
136```typescript
137// First query captures session ID
138let sessionId: string;
139for await (const message of query({ prompt: "Read auth module" })) {
140 if (message.type === "system" && message.subtype === "init") {
141 sessionId = message.session_id;
142 }
143}
144
145// Resume later with full context
146for await (const message of query({
147 prompt: "Now find all places that call it",
148 options: { resume: sessionId }
149})) {
150 // Claude remembers the previous conversation
151}
152```
153
154**Key insight:** Sessions persist to disk by default. We can store the session ID in SQLite and resume later.
155
156### Hooks
157
158Hooks intercept agent behavior at key points:
159
160```typescript
161import { query, HookCallback, PreToolUseHookInput } from "@anthropic-ai/claude-agent-sdk";
162
163const blockWrites: HookCallback = async (input, toolUseID, { signal }) => {
164 const preInput = input as PreToolUseHookInput;
165 if (["Write", "Edit"].includes(preInput.tool_name)) {
166 return {
167 hookSpecificOutput: {
168 hookEventName: "PreToolUse",
169 permissionDecision: "deny",
170 permissionDecisionReason: "In planning mode - no code changes allowed"
171 }
172 };
173 }
174 return {};
175};
176
177for await (const message of query({
178 prompt: "...",
179 options: {
180 hooks: {
181 PreToolUse: [{ matcher: "Write|Edit", hooks: [blockWrites] }]
182 }
183 }
184})) { ... }
185```
186
187**Available hook events:**
188- `PreToolUse` — Before tool executes (can block/modify)
189- `PostToolUse` — After tool executes (can log/transform)
190- `Stop` — Agent execution ending
191- `SessionStart` / `SessionEnd` — Session lifecycle
192- `SubagentStart` / `SubagentStop` — Subagent lifecycle
193
194### Message Types
195
196The query yields different message types:
197
198```typescript
199type SDKMessage =
200 | SDKAssistantMessage // Claude's response (includes tool_use)
201 | SDKUserMessage // User input
202 | SDKResultMessage // Final result with usage stats
203 | SDKSystemMessage // Init message with session_id, tools, etc.
204 | SDKPartialMessage // Streaming chunks (if enabled)
205 | SDKStatusMessage // Status updates
206 | ...
207```
208
209**SDKResultMessage** contains:
210- `result` — Final text output
211- `total_cost_usd` — API cost
212- `usage` — Token counts
213- `duration_ms` — Total time
214- `num_turns` — Conversation turns
215
216### Query Object Methods
217
218The query object has methods for control:
219
220```typescript
221const q = query({ prompt: "...", options: { ... } });
222
223// Change settings mid-session
224await q.setPermissionMode("acceptEdits");
225await q.setModel("opus");
226
227// Get session info
228const init = await q.initializationResult();
229const commands = await q.supportedCommands();
230const models = await q.supportedModels();
231
232// Interrupt/cancel
233await q.interrupt();
234q.close();
235```
236
237---
238
239## Architecture Decisions
240
241### Phase Enforcement Strategy
242
243We have two complementary approaches:
244
245**1. Permission Mode + Allowed Tools:**
246```typescript
247const phaseConfig = {
248 research: {
249 permissionMode: "plan",
250 allowedTools: ["Read", "Glob", "Grep", "WebSearch", "WebFetch"],
251 systemPrompt: "You are in RESEARCH mode..."
252 },
253 plan: {
254 permissionMode: "plan",
255 allowedTools: ["Read", "Glob", "Grep", "Write"], // Write only for plan.md
256 systemPrompt: "You are in PLANNING mode..."
257 },
258 annotate: {
259 permissionMode: "plan",
260 allowedTools: ["Read", "Write"], // Only update plan.md
261 systemPrompt: "You are in ANNOTATION mode..."
262 },
263 implement: {
264 permissionMode: "acceptEdits",
265 allowedTools: ["Read", "Write", "Edit", "Bash", "Glob", "Grep"],
266 systemPrompt: "You are in IMPLEMENTATION mode..."
267 }
268};
269```
270
271**2. Hooks for Fine-Grained Control:**
272```typescript
273const enforcePhaseHook: HookCallback = async (input, toolUseID, { signal }) => {
274 const { tool_name, tool_input } = input as PreToolUseHookInput;
275 const phase = getCurrentPhase(); // From app state
276
277 if (phase !== "implement" && ["Write", "Edit"].includes(tool_name)) {
278 const filePath = (tool_input as any).file_path;
279 // Allow only plan.md/research.md in non-implement phases
280 if (!filePath.endsWith("plan.md") && !filePath.endsWith("research.md")) {
281 return {
282 hookSpecificOutput: {
283 hookEventName: "PreToolUse",
284 permissionDecision: "deny",
285 permissionDecisionReason: `Cannot modify ${filePath} in ${phase} phase`
286 }
287 };
288 }
289 }
290 return {};
291};
292```
293
294### Session Persistence
295
296**Option A: Use SDK's built-in session persistence**
297- Sessions saved to disk automatically
298- Store session ID in SQLite
299- Resume with `options: { resume: sessionId }`
300- Pro: Simpler, full context preserved
301- Con: Less control over what's stored
302
303**Option B: Store messages in SQLite ourselves**
304- Store each message in our database
305- Reconstruct context when resuming
306- Pro: Full control, searchable history
307- Con: More complex, may lose SDK internal state
308
309**Recommendation:** Use **Option A** (SDK persistence) with session ID in SQLite. We can still store messages for display/search, but rely on SDK for actual context.
310// REVIEW: option a
311
312### Artifact Management
313
314The blog workflow uses `research.md` and `plan.md` as persistent artifacts.
315
316**Options:**
3171. **Store in SQLite** — Searchable, version history, but not editable externally
3182. **Store as files in project** — Visible in git, editable in any editor
3193. **Both** — Files as source of truth, sync to SQLite for search
320
321**Recommendation:** Store as **files in the project directory** (e.g., `.claude-flow/research.md`, `.claude-flow/plan.md`). This matches the blog workflow where the human edits the plan.md directly.
322// REVIEW: recommendation is great
323
324### IPC Architecture
325
326Electron requires IPC for renderer ↔ main communication:
327
328```typescript
329// preload.ts
330import { contextBridge, ipcRenderer } from "electron";
331
332contextBridge.exposeInMainWorld("api", {
333 // Projects
334 listProjects: () => ipcRenderer.invoke("projects:list"),
335 createProject: (data) => ipcRenderer.invoke("projects:create", data),
336
337 // Sessions
338 listSessions: (projectId) => ipcRenderer.invoke("sessions:list", projectId),
339 createSession: (data) => ipcRenderer.invoke("sessions:create", data),
340
341 // Claude
342 sendMessage: (sessionId, message) => ipcRenderer.invoke("claude:send", sessionId, message),
343 onMessage: (callback) => ipcRenderer.on("claude:message", callback),
344 setPhase: (sessionId, phase) => ipcRenderer.invoke("claude:setPhase", sessionId, phase),
345});
346```
347
348---
349
350## Open Questions Resolved
351
352### 1. Claude Code SDK vs CLI
353
354**Answer:** Use the SDK (`@anthropic-ai/claude-agent-sdk`). It provides:
355- Programmatic control via TypeScript
356- Hooks for intercepting behavior
357- Session management
358- Streaming messages
359
360### 2. Artifact Storage
361
362**Answer:** Store as files in project directory (`.claude-flow/`) for:
363- Editability in any editor (VSCode, etc.)
364- Git visibility
365- Matches the blog workflow
366
367### 3. Session Context / Compaction
368
369**Answer:** Use SDK's built-in session persistence:
370- Store session ID in SQLite
371- Resume with `options: { resume: sessionId }`
372- SDK handles context compaction automatically
373
374### 4. Multi-file Editing
375
376**Answer:** The SDK handles this natively via the Edit/Write tools. The plan.md should list all files to be modified, and Claude executes them in order during implementation.
377
378---
379
380## Dependencies to Add
381
382```json
383{
384 "dependencies": {
385 "@anthropic-ai/claude-agent-sdk": "latest",
386 "better-sqlite3": "12.2.0",
387 "react": "^19.1.1",
388 "react-dom": "^19.1.1",
389 "uuid": "^11.0.0"
390 },
391 "devDependencies": {
392 "@types/uuid": "^10.0.0"
393 // ... existing devDeps
394 }
395}
396```
397
398---
399
400## Summary
401
402The Claude Agent SDK is well-suited for building Claude Flow:
403
4041. **Session management** — Built-in persistence, resume capability
4052. **Permission modes** — `plan` mode prevents execution, `acceptEdits` for implementation
4063. **Hooks** — Fine-grained control over what Claude can do
4074. **Streaming** — Real-time message display in UI
4085. **System prompts** — Customizable per phase
409
410The main work is:
411- Building the Electron app shell (IPC, windows)
412- SQLite layer for projects/sessions
413- React UI for chat, artifacts, phase controls
414- Wiring everything together with the SDK
diff --git a/src/main/claude/index.ts b/src/main/claude/index.ts
index b8c9c07..4dd49f2 100644
--- a/src/main/claude/index.ts
+++ b/src/main/claude/index.ts
@@ -1,31 +1,24 @@
1import { query, type SDKMessage, type Query } from "@anthropic-ai/claude-agent-sdk"; 1import { query, type SDKMessage, type Query } from "@anthropic-ai/claude-agent-sdk";
2import type { Session } from "../db/sessions"; 2import type { Session } from "../db/sessions";
3import { getPhaseConfig, getNextPhase, getArtifactFilename, getSessionArtifactDir } from "./phases"; 3import { getPhaseConfig, getNextPhase, getArtifactFilename } from "./phases";
4import type { Phase, UserPermissionMode } from "./phases"; 4import type { Phase, UserPermissionMode } from "./phases";
5import { getProject } from "../db/projects"; 5import { getProject } from "../db/projects";
6import { updateSession } from "../db/sessions"; 6import { updateSession } from "../db/sessions";
7import fs from "node:fs"; 7import fs from "node:fs";
8import path from "node:path"; 8import path from "node:path";
9import os from "node:os";
10 9
11// Track active queries by session ID 10// Track active queries by session ID
12const activeQueries = new Map<string, Query>(); 11const activeQueries = new Map<string, Query>();
13 12
14// Global storage in home directory
15const GLOBAL_CLAUDE_FLOW_DIR = path.join(os.homedir(), ".claude-flow");
16
17function ensureDir(dirPath: string): void { 13function ensureDir(dirPath: string): void {
18 if (!fs.existsSync(dirPath)) { 14 if (!fs.existsSync(dirPath)) {
19 fs.mkdirSync(dirPath, { recursive: true }); 15 fs.mkdirSync(dirPath, { recursive: true });
20 } 16 }
21} 17}
22 18
23function getProjectDir(projectId: string): string { 19// Artifacts live inside the project directory so the SDK's Write tool can reach them
24 return path.join(GLOBAL_CLAUDE_FLOW_DIR, "projects", projectId); 20function getSessionDir(projectPath: string, sessionId: string): string {
25} 21 return path.join(projectPath, ".claude-flow", "sessions", sessionId);
26
27function getSessionDir(projectId: string, sessionId: string): string {
28 return path.join(getProjectDir(projectId), "sessions", sessionId);
29} 22}
30 23
31export interface SendMessageOptions { 24export interface SendMessageOptions {
@@ -42,12 +35,13 @@ export async function sendMessage({
42 const project = getProject(session.project_id); 35 const project = getProject(session.project_id);
43 if (!project) throw new Error("Project not found"); 36 if (!project) throw new Error("Project not found");
44 37
45 // Ensure session artifact directory exists in global storage 38 // Ensure session artifact directory exists inside the project
46 const sessionDir = getSessionDir(session.project_id, session.id); 39 const sessionDir = getSessionDir(project.path, session.id);
47 ensureDir(sessionDir); 40 ensureDir(sessionDir);
48 41
49 const phaseConfig = getPhaseConfig( 42 const phaseConfig = getPhaseConfig(
50 session.phase as Phase, 43 session.phase as Phase,
44 sessionDir,
51 session.permission_mode as UserPermissionMode 45 session.permission_mode as UserPermissionMode
52 ); 46 );
53 47
@@ -112,22 +106,26 @@ export function advancePhase(session: Session): Phase | null {
112} 106}
113 107
114/** 108/**
115 * Get the artifact path for a session and phase (in global storage) 109 * Get the artifact path for a session and phase (inside the project directory)
116 */ 110 */
117export function getArtifactPath(session: Session): string { 111export function getArtifactPath(session: Session): string {
112 const project = getProject(session.project_id);
113 if (!project) throw new Error("Project not found");
118 const filename = getArtifactFilename(session.phase as Phase); 114 const filename = getArtifactFilename(session.phase as Phase);
119 return path.join(getSessionDir(session.project_id, session.id), filename); 115 return path.join(getSessionDir(project.path, session.id), filename);
120} 116}
121 117
122/** 118/**
123 * Read an artifact file for a session (from global storage) 119 * Read an artifact file for a session
124 */ 120 */
125export function readSessionArtifact( 121export function readSessionArtifact(
126 projectId: string, 122 projectId: string,
127 sessionId: string, 123 sessionId: string,
128 filename: string 124 filename: string
129): string | null { 125): string | null {
130 const filePath = path.join(getSessionDir(projectId, sessionId), filename); 126 const project = getProject(projectId);
127 if (!project) return null;
128 const filePath = path.join(getSessionDir(project.path, sessionId), filename);
131 if (fs.existsSync(filePath)) { 129 if (fs.existsSync(filePath)) {
132 return fs.readFileSync(filePath, "utf-8"); 130 return fs.readFileSync(filePath, "utf-8");
133 } 131 }
@@ -135,7 +133,7 @@ export function readSessionArtifact(
135} 133}
136 134
137/** 135/**
138 * Write an artifact file for a session (to global storage) 136 * Write an artifact file for a session
139 */ 137 */
140export function writeSessionArtifact( 138export function writeSessionArtifact(
141 projectId: string, 139 projectId: string,
@@ -143,7 +141,9 @@ export function writeSessionArtifact(
143 filename: string, 141 filename: string,
144 content: string 142 content: string
145): void { 143): void {
146 const dir = getSessionDir(projectId, sessionId); 144 const project = getProject(projectId);
145 if (!project) throw new Error("Project not found");
146 const dir = getSessionDir(project.path, sessionId);
147 ensureDir(dir); 147 ensureDir(dir);
148 fs.writeFileSync(path.join(dir, filename), content, "utf-8"); 148 fs.writeFileSync(path.join(dir, filename), content, "utf-8");
149} 149}
@@ -168,22 +168,17 @@ export function writeClaudeMd(projectPath: string, content: string): void {
168} 168}
169 169
170/** 170/**
171 * Clear session artifacts from global storage 171 * Clear session artifacts
172 */ 172 */
173export function clearSessionArtifacts(projectId: string, sessionId: string): void { 173export function clearSessionArtifacts(projectId: string, sessionId: string): void {
174 const dir = getSessionDir(projectId, sessionId); 174 const project = getProject(projectId);
175 if (!project) return;
176 const dir = getSessionDir(project.path, sessionId);
175 if (fs.existsSync(dir)) { 177 if (fs.existsSync(dir)) {
176 fs.rmSync(dir, { recursive: true, force: true }); 178 fs.rmSync(dir, { recursive: true, force: true });
177 } 179 }
178} 180}
179 181
180/**
181 * Get the initial message for a phase
182 */
183export function getPhaseInitialMessage(phase: Phase): string {
184 return getPhaseConfig(phase).initialMessage;
185}
186
187// Re-export types 182// Re-export types
188export type { SDKMessage } from "@anthropic-ai/claude-agent-sdk"; 183export type { SDKMessage } from "@anthropic-ai/claude-agent-sdk";
189export type { Phase, UserPermissionMode } from "./phases"; 184export type { Phase, UserPermissionMode } from "./phases";
diff --git a/src/main/claude/phases.ts b/src/main/claude/phases.ts
index f1df719..89e7c22 100644
--- a/src/main/claude/phases.ts
+++ b/src/main/claude/phases.ts
@@ -3,6 +3,7 @@ import type { PermissionMode } from "@anthropic-ai/claude-agent-sdk";
3export type Phase = "research" | "plan" | "implement"; 3export type Phase = "research" | "plan" | "implement";
4export type UserPermissionMode = "acceptEdits" | "bypassPermissions"; 4export type UserPermissionMode = "acceptEdits" | "bypassPermissions";
5 5
6// External interface — consumers see a resolved string
6export interface PhaseConfig { 7export interface PhaseConfig {
7 systemPrompt: string; 8 systemPrompt: string;
8 tools: string[]; 9 tools: string[];
@@ -10,27 +11,24 @@ export interface PhaseConfig {
10 initialMessage: string; 11 initialMessage: string;
11} 12}
12 13
13// Get session-specific artifact path (relative to ~/.claude-flow/) 14// Internal template — systemPrompt is a function that receives the artifact dir
14export function getSessionArtifactDir(sessionId: string): string { 15interface PhaseConfigTemplate {
15 return `sessions/${sessionId}`; 16 systemPrompt: (artifactDir: string) => string;
16} 17 tools: string[];
17 18 permissionMode: PermissionMode;
18export function getArtifactPath(phase: Phase, sessionId: string): string { 19 initialMessage: string;
19 const dir = getSessionArtifactDir(sessionId);
20 const filename = phase === "research" ? "research.md" : "plan.md";
21 return `${dir}/${filename}`;
22} 20}
23 21
24export const phaseConfigs: Record<Phase, PhaseConfig> = { 22const phaseConfigTemplates: Record<Phase, PhaseConfigTemplate> = {
25 research: { 23 research: {
26 permissionMode: "acceptEdits", 24 permissionMode: "acceptEdits",
27 tools: ["Read", "Glob", "Grep", "Bash", "Write"], 25 tools: ["Read", "Glob", "Grep", "Bash", "Write"],
28 initialMessage: 26 initialMessage:
29 "What areas of the codebase should I research? What are you trying to build?", 27 "What areas of the codebase should I research? What are you trying to build?",
30 systemPrompt: `You are in RESEARCH mode. Your ONLY job is to understand the codebase. 28 systemPrompt: (artifactDir) => `You are in RESEARCH mode. Your ONLY job is to understand the codebase.
31 29
32CRITICAL RULES: 30CRITICAL RULES:
331. You MUST write ALL findings to the session research.md — this is your PRIMARY output 311. You MUST write ALL findings to ${artifactDir}/research.md — this is your PRIMARY output
342. DO NOT just respond in chat. The document viewer shows research.md, so write there. 322. DO NOT just respond in chat. The document viewer shows research.md, so write there.
353. DO NOT suggest moving to planning or implementation 333. DO NOT suggest moving to planning or implementation
364. DO NOT ask "are you ready to implement?" or similar 344. DO NOT ask "are you ready to implement?" or similar
@@ -46,7 +44,7 @@ WORKFLOW:
461. Read CLAUDE.md (create at project root if missing) 441. Read CLAUDE.md (create at project root if missing)
472. Ask what to research (if unclear) 452. Ask what to research (if unclear)
483. Read files thoroughly using Read, Glob, Grep 463. Read files thoroughly using Read, Glob, Grep
494. Write findings to session research.md 474. Write findings to ${artifactDir}/research.md
505. Update CLAUDE.md with any new general insights worth sharing 485. Update CLAUDE.md with any new general insights worth sharing
51 49
52FORMAT for research.md: 50FORMAT for research.md:
@@ -69,7 +67,7 @@ FORMAT for research.md:
69[Things that need clarification] 67[Things that need clarification]
70\`\`\` 68\`\`\`
71 69
72Remember: Your output goes in research.md, not chat. Chat is for clarifying questions only.`, 70Remember: Your output goes in ${artifactDir}/research.md, not chat. Chat is for clarifying questions only.`,
73 }, 71 },
74 72
75 plan: { 73 plan: {
@@ -77,10 +75,10 @@ Remember: Your output goes in research.md, not chat. Chat is for clarifying ques
77 tools: ["Read", "Glob", "Grep", "Write"], 75 tools: ["Read", "Glob", "Grep", "Write"],
78 initialMessage: 76 initialMessage:
79 "I'll create a detailed implementation plan based on my research. Writing to plan.md...", 77 "I'll create a detailed implementation plan based on my research. Writing to plan.md...",
80 systemPrompt: `You are in PLANNING mode. Your ONLY job is to create an implementation plan. 78 systemPrompt: (artifactDir) => `You are in PLANNING mode. Your ONLY job is to create an implementation plan.
81 79
82CRITICAL RULES: 80CRITICAL RULES:
831. You MUST write the plan to session plan.md — this is your PRIMARY output 811. You MUST write the plan to ${artifactDir}/plan.md — this is your PRIMARY output
842. DO NOT just respond in chat. The document viewer shows plan.md, so write there. 822. DO NOT just respond in chat. The document viewer shows plan.md, so write there.
853. DO NOT implement anything — no code changes to source files 833. DO NOT implement anything — no code changes to source files
864. DO NOT ask "should I start implementing?" or similar 844. DO NOT ask "should I start implementing?" or similar
@@ -89,12 +87,12 @@ CRITICAL RULES:
89 87
90CONTEXT: 88CONTEXT:
91- Read CLAUDE.md at project root for codebase overview 89- Read CLAUDE.md at project root for codebase overview
92- Read the session research.md to understand the specific task 90- Read ${artifactDir}/research.md to understand the specific task
93 91
94WORKFLOW: 92WORKFLOW:
951. Read CLAUDE.md for codebase overview 931. Read CLAUDE.md for codebase overview
962. Read the session research.md to understand the specific task 942. Read ${artifactDir}/research.md to understand the specific task
973. Write a detailed plan to session plan.md 953. Write a detailed plan to ${artifactDir}/plan.md
984. Include specific code snippets showing proposed changes 964. Include specific code snippets showing proposed changes
995. Make the plan detailed enough that implementation is mechanical 975. Make the plan detailed enough that implementation is mechanical
100 98
@@ -132,7 +130,7 @@ FORMAT for plan.md:
132 130
133When the user adds annotations to plan.md and clicks Review, address each annotation and update the document. 131When the user adds annotations to plan.md and clicks Review, address each annotation and update the document.
134 132
135Remember: Your output goes in plan.md, not chat. Chat is for clarifying questions only.`, 133Remember: Your output goes in ${artifactDir}/plan.md, not chat. Chat is for clarifying questions only.`,
136 }, 134 },
137 135
138 implement: { 136 implement: {
@@ -140,19 +138,19 @@ Remember: Your output goes in plan.md, not chat. Chat is for clarifying question
140 tools: ["Read", "Write", "Edit", "Bash", "Glob", "Grep"], 138 tools: ["Read", "Write", "Edit", "Bash", "Glob", "Grep"],
141 initialMessage: 139 initialMessage:
142 "Starting implementation. I'll follow the plan exactly and mark tasks complete as I go.", 140 "Starting implementation. I'll follow the plan exactly and mark tasks complete as I go.",
143 systemPrompt: `You are in IMPLEMENTATION mode. Execute the approved plan. 141 systemPrompt: (artifactDir) => `You are in IMPLEMENTATION mode. Execute the approved plan.
144 142
145CRITICAL RULES: 143CRITICAL RULES:
1461. Read session plan.md and follow it exactly 1441. Read ${artifactDir}/plan.md and follow it exactly
1472. Mark tasks complete in plan.md as you finish them: - [ ] → - [x] 1452. Mark tasks complete in ${artifactDir}/plan.md as you finish them: - [ ] → - [x]
1483. DO NOT deviate from the plan without asking 1463. DO NOT deviate from the plan without asking
1494. Run tests/typecheck if available 1474. Run tests/typecheck if available
1505. Stop and ask if you encounter issues not covered by the plan 1485. Stop and ask if you encounter issues not covered by the plan
151 149
152WORKFLOW: 150WORKFLOW:
1531. Read session plan.md 1511. Read ${artifactDir}/plan.md
1542. Execute each task in order 1522. Execute each task in order
1553. Update plan.md to mark tasks complete 1533. Update ${artifactDir}/plan.md to mark tasks complete
1564. Continue until all tasks are done 1544. Continue until all tasks are done
157 155
158When complete, summarize what was done and any follow-up tasks.`, 156When complete, summarize what was done and any follow-up tasks.`,
@@ -161,15 +159,26 @@ When complete, summarize what was done and any follow-up tasks.`,
161 159
162export function getPhaseConfig( 160export function getPhaseConfig(
163 phase: Phase, 161 phase: Phase,
162 artifactDir: string,
164 userPermissionMode?: UserPermissionMode 163 userPermissionMode?: UserPermissionMode
165): PhaseConfig { 164): PhaseConfig {
166 const config = { ...phaseConfigs[phase] }; 165 const template = phaseConfigTemplates[phase];
166 const config: PhaseConfig = {
167 systemPrompt: template.systemPrompt(artifactDir),
168 tools: template.tools,
169 permissionMode: template.permissionMode,
170 initialMessage: template.initialMessage,
171 };
167 if (phase === "implement" && userPermissionMode) { 172 if (phase === "implement" && userPermissionMode) {
168 config.permissionMode = userPermissionMode; 173 config.permissionMode = userPermissionMode;
169 } 174 }
170 return config; 175 return config;
171} 176}
172 177
178export function getPhaseInitialMessage(phase: Phase): string {
179 return phaseConfigTemplates[phase].initialMessage;
180}
181
173export function getNextPhase(phase: Phase): Phase | null { 182export function getNextPhase(phase: Phase): Phase | null {
174 const transitions: Record<Phase, Phase | null> = { 183 const transitions: Record<Phase, Phase | null> = {
175 research: "plan", 184 research: "plan",