aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorClawd <ai@clawd.bot>2026-02-27 21:44:59 -0800
committerClawd <ai@clawd.bot>2026-02-27 21:44:59 -0800
commit34ba41851752da108fbede66997cda0f814eb714 (patch)
tree1a23b9c36759984bd40eb16f78dc5de2547f1a95
parent91412701e162b46a76673eacea976319e4f21a26 (diff)
Add detailed implementation plan with code snippets and TODO list
-rw-r--r--plan.md1487
1 files changed, 1487 insertions, 0 deletions
diff --git a/plan.md b/plan.md
new file mode 100644
index 0000000..ff71487
--- /dev/null
+++ b/plan.md
@@ -0,0 +1,1487 @@
1# Plan: Claude Flow Implementation
2
3## Overview
4
5Build an Electron app that wraps the Claude Agent SDK with an opinionated workflow: Research → Plan → Annotate → Implement.
6
7---
8
9## Directory Structure (Target)
10
11```
12claude-flow/
13├── src/main/
14│ ├── index.ts # App lifecycle, window management
15│ ├── preload.ts # IPC bridge
16│ ├── db/
17│ │ ├── index.ts # Database connection singleton
18│ │ ├── schema.ts # Table definitions + migrations
19│ │ ├── projects.ts # Project CRUD
20│ │ └── sessions.ts # Session CRUD
21│ ├── claude/
22│ │ ├── index.ts # Claude SDK wrapper
23│ │ ├── phases.ts # Phase configs (prompts, tools, permissions)
24│ │ └── hooks.ts # Custom hooks for phase enforcement
25│ └── ipc/
26│ ├── index.ts # Register all handlers
27│ ├── projects.ts # Project IPC handlers
28│ ├── sessions.ts # Session IPC handlers
29│ └── claude.ts # Claude IPC handlers
30├── renderer/
31│ ├── index.html
32│ └── src/
33│ ├── main.tsx # React entry
34│ ├── App.tsx # Main app component
35│ ├── components/
36│ │ ├── Sidebar.tsx # Project/session tree
37│ │ ├── SessionList.tsx # Sessions for a project
38│ │ ├── Chat.tsx # Message thread
39│ │ ├── ChatInput.tsx # Input box
40│ │ ├── Message.tsx # Single message bubble
41│ │ ├── ArtifactPane.tsx # Markdown editor for plan/research
42│ │ ├── PhaseBar.tsx # Phase indicator + controls
43│ │ └── Settings.tsx # API key, preferences
44│ ├── hooks/
45│ │ ├── useProjects.ts
46│ │ ├── useSessions.ts
47│ │ └── useChat.ts
48│ ├── lib/
49│ │ └── api.ts # Typed IPC wrapper
50│ ├── types.ts # Shared types
51│ └── styles/
52│ └── globals.css
53├── package.json
54├── tsconfig.json
55└── vite.config.ts
56```
57
58---
59
60## Phase 1: Database Layer
61
62### 1.1 Schema (`src/main/db/schema.ts`)
63
64```typescript
65import Database from "better-sqlite3";
66
67export function initSchema(db: Database.Database) {
68 db.exec(`
69 CREATE TABLE IF NOT EXISTS projects (
70 id TEXT PRIMARY KEY,
71 name TEXT NOT NULL,
72 path TEXT NOT NULL,
73 created_at INTEGER NOT NULL DEFAULT (unixepoch()),
74 updated_at INTEGER NOT NULL DEFAULT (unixepoch())
75 );
76
77 CREATE TABLE IF NOT EXISTS sessions (
78 id TEXT PRIMARY KEY,
79 project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
80 name TEXT NOT NULL,
81 phase TEXT NOT NULL DEFAULT 'research',
82 claude_session_id TEXT,
83 permission_mode TEXT NOT NULL DEFAULT 'acceptEdits',
84 created_at INTEGER NOT NULL DEFAULT (unixepoch()),
85 updated_at INTEGER NOT NULL DEFAULT (unixepoch())
86 );
87
88 CREATE TABLE IF NOT EXISTS messages (
89 id TEXT PRIMARY KEY,
90 session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
91 role TEXT NOT NULL,
92 content TEXT NOT NULL,
93 tool_use TEXT,
94 created_at INTEGER NOT NULL DEFAULT (unixepoch())
95 );
96
97 CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_id);
98 CREATE INDEX IF NOT EXISTS idx_messages_session ON messages(session_id);
99 `);
100}
101```
102
103**Notes:**
104- `claude_session_id` stores the SDK's session ID for resuming
105- `permission_mode` is user toggle: `acceptEdits` or `bypassPermissions`
106- `phase` is one of: `research`, `plan`, `annotate`, `implement`
107- Messages stored for display; actual context in SDK session
108
109### 1.2 Database Connection (`src/main/db/index.ts`)
110
111```typescript
112import Database from "better-sqlite3";
113import { app } from "electron";
114import path from "node:path";
115import fs from "node:fs";
116import { initSchema } from "./schema";
117
118let db: Database.Database | null = null;
119
120export function getDb(): Database.Database {
121 if (db) return db;
122
123 const dbDir = app.getPath("userData");
124 if (!fs.existsSync(dbDir)) {
125 fs.mkdirSync(dbDir, { recursive: true });
126 }
127
128 const dbPath = path.join(dbDir, "claude-flow.db");
129 db = new Database(dbPath);
130 db.pragma("journal_mode = WAL");
131 db.pragma("foreign_keys = ON");
132
133 initSchema(db);
134 return db;
135}
136
137export function closeDb() {
138 if (db) {
139 db.close();
140 db = null;
141 }
142}
143```
144
145### 1.3 Project CRUD (`src/main/db/projects.ts`)
146
147```typescript
148import { getDb } from "./index";
149import { v4 as uuid } from "uuid";
150
151export interface Project {
152 id: string;
153 name: string;
154 path: string;
155 created_at: number;
156 updated_at: number;
157}
158
159export function listProjects(): Project[] {
160 const db = getDb();
161 return db.prepare("SELECT * FROM projects ORDER BY updated_at DESC").all() as Project[];
162}
163
164export function getProject(id: string): Project | undefined {
165 const db = getDb();
166 return db.prepare("SELECT * FROM projects WHERE id = ?").get(id) as Project | undefined;
167}
168
169export function createProject(name: string, projectPath: string): Project {
170 const db = getDb();
171 const id = uuid();
172 const now = Math.floor(Date.now() / 1000);
173
174 db.prepare(
175 "INSERT INTO projects (id, name, path, created_at, updated_at) VALUES (?, ?, ?, ?, ?)"
176 ).run(id, name, projectPath, now, now);
177
178 return { id, name, path: projectPath, created_at: now, updated_at: now };
179}
180
181export function updateProject(id: string, updates: Partial<Pick<Project, "name" | "path">>): void {
182 const db = getDb();
183 const sets: string[] = [];
184 const values: any[] = [];
185
186 if (updates.name !== undefined) {
187 sets.push("name = ?");
188 values.push(updates.name);
189 }
190 if (updates.path !== undefined) {
191 sets.push("path = ?");
192 values.push(updates.path);
193 }
194
195 if (sets.length > 0) {
196 sets.push("updated_at = ?");
197 values.push(Math.floor(Date.now() / 1000));
198 values.push(id);
199
200 db.prepare(`UPDATE projects SET ${sets.join(", ")} WHERE id = ?`).run(...values);
201 }
202}
203
204export function deleteProject(id: string): void {
205 const db = getDb();
206 db.prepare("DELETE FROM projects WHERE id = ?").run(id);
207}
208```
209
210### 1.4 Session CRUD (`src/main/db/sessions.ts`)
211
212```typescript
213import { getDb } from "./index";
214import { v4 as uuid } from "uuid";
215
216export type Phase = "research" | "plan" | "annotate" | "implement";
217export type PermissionMode = "acceptEdits" | "bypassPermissions";
218
219export interface Session {
220 id: string;
221 project_id: string;
222 name: string;
223 phase: Phase;
224 claude_session_id: string | null;
225 permission_mode: PermissionMode;
226 created_at: number;
227 updated_at: number;
228}
229
230export interface Message {
231 id: string;
232 session_id: string;
233 role: "user" | "assistant" | "system";
234 content: string;
235 tool_use: string | null;
236 created_at: number;
237}
238
239export function listSessions(projectId: string): Session[] {
240 const db = getDb();
241 return db
242 .prepare("SELECT * FROM sessions WHERE project_id = ? ORDER BY updated_at DESC")
243 .all(projectId) as Session[];
244}
245
246export function getSession(id: string): Session | undefined {
247 const db = getDb();
248 return db.prepare("SELECT * FROM sessions WHERE id = ?").get(id) as Session | undefined;
249}
250
251export function createSession(projectId: string, name: string): Session {
252 const db = getDb();
253 const id = uuid();
254 const now = Math.floor(Date.now() / 1000);
255
256 db.prepare(
257 `INSERT INTO sessions (id, project_id, name, phase, permission_mode, created_at, updated_at)
258 VALUES (?, ?, ?, 'research', 'acceptEdits', ?, ?)`
259 ).run(id, projectId, name, now, now);
260
261 return {
262 id,
263 project_id: projectId,
264 name,
265 phase: "research",
266 claude_session_id: null,
267 permission_mode: "acceptEdits",
268 created_at: now,
269 updated_at: now,
270 };
271}
272
273export function updateSession(
274 id: string,
275 updates: Partial<Pick<Session, "name" | "phase" | "claude_session_id" | "permission_mode">>
276): void {
277 const db = getDb();
278 const sets: string[] = [];
279 const values: any[] = [];
280
281 for (const [key, value] of Object.entries(updates)) {
282 if (value !== undefined) {
283 sets.push(`${key} = ?`);
284 values.push(value);
285 }
286 }
287
288 if (sets.length > 0) {
289 sets.push("updated_at = ?");
290 values.push(Math.floor(Date.now() / 1000));
291 values.push(id);
292
293 db.prepare(`UPDATE sessions SET ${sets.join(", ")} WHERE id = ?`).run(...values);
294 }
295}
296
297export function deleteSession(id: string): void {
298 const db = getDb();
299 db.prepare("DELETE FROM sessions WHERE id = ?").run(id);
300}
301
302// Messages
303export function listMessages(sessionId: string): Message[] {
304 const db = getDb();
305 return db
306 .prepare("SELECT * FROM messages WHERE session_id = ? ORDER BY created_at ASC")
307 .all(sessionId) as Message[];
308}
309
310export function addMessage(
311 sessionId: string,
312 role: Message["role"],
313 content: string,
314 toolUse?: string
315): Message {
316 const db = getDb();
317 const id = uuid();
318 const now = Math.floor(Date.now() / 1000);
319
320 db.prepare(
321 "INSERT INTO messages (id, session_id, role, content, tool_use, created_at) VALUES (?, ?, ?, ?, ?, ?)"
322 ).run(id, sessionId, role, content, toolUse ?? null, now);
323
324 // Touch session updated_at
325 db.prepare("UPDATE sessions SET updated_at = ? WHERE id = ?").run(now, sessionId);
326
327 return { id, session_id: sessionId, role, content, tool_use: toolUse ?? null, created_at: now };
328}
329```
330
331---
332
333## Phase 2: Claude Integration
334
335### 2.1 Phase Configs (`src/main/claude/phases.ts`)
336
337```typescript
338import { Phase, PermissionMode } from "../db/sessions";
339
340export interface PhaseConfig {
341 systemPrompt: string;
342 allowedTools: string[];
343 permissionMode: "plan" | PermissionMode;
344}
345
346export const phaseConfigs: Record<Phase, PhaseConfig> = {
347 research: {
348 permissionMode: "plan",
349 allowedTools: ["Read", "Glob", "Grep", "WebSearch", "WebFetch"],
350 systemPrompt: `You are in RESEARCH mode.
351
352Your job is to deeply understand the relevant parts of the codebase before any changes are made.
353
354Rules:
355- Read files thoroughly — "deeply", "in great detail", understand all intricacies
356- Write all findings to .claude-flow/research.md
357- DO NOT make any code changes
358- DO NOT modify any files except .claude-flow/research.md
359
360When finished, summarize what you learned and wait for the next instruction.`,
361 },
362
363 plan: {
364 permissionMode: "plan",
365 allowedTools: ["Read", "Glob", "Grep", "Write"],
366 systemPrompt: `You are in PLANNING mode.
367
368Your job is to write a detailed implementation plan based on the research.
369
370Rules:
371- Write the plan to .claude-flow/plan.md
372- Include specific code snippets showing proposed changes
373- Include file paths that will be modified
374- Include trade-offs and considerations
375- Add a granular TODO list at the end with phases and tasks
376- DO NOT implement anything
377- DO NOT modify any source files
378
379The plan should be detailed enough that implementation becomes mechanical.`,
380 },
381
382 annotate: {
383 permissionMode: "plan",
384 allowedTools: ["Read", "Write"],
385 systemPrompt: `You are in ANNOTATION mode.
386
387The human has added notes/comments to .claude-flow/plan.md.
388
389Rules:
390- Read .claude-flow/plan.md carefully
391- Find all inline notes (marked with comments, "// NOTE:", "REVIEW:", etc.)
392- Address each note by updating the relevant section
393- DO NOT implement anything
394- DO NOT modify any source files
395- Only update .claude-flow/plan.md
396
397When finished, summarize the changes you made to the plan.`,
398 },
399
400 implement: {
401 permissionMode: "acceptEdits", // Will be overridden by user setting
402 allowedTools: ["Read", "Write", "Edit", "Bash", "Glob", "Grep"],
403 systemPrompt: `You are in IMPLEMENTATION mode. The plan has been approved.
404
405Rules:
406- Follow .claude-flow/plan.md exactly
407- Mark tasks complete in the plan as you finish them
408- Run typecheck/lint continuously if available
409- Do not add unnecessary comments or jsdocs
410- Do not use any or unknown types
411- Do not stop until all tasks are complete
412
413If you encounter issues not covered by the plan, stop and ask.`,
414 },
415};
416
417export function getPhaseConfig(phase: Phase, userPermissionMode?: PermissionMode): PhaseConfig {
418 const config = { ...phaseConfigs[phase] };
419
420 // In implement phase, use user's permission mode preference
421 if (phase === "implement" && userPermissionMode) {
422 config.permissionMode = userPermissionMode;
423 }
424
425 return config;
426}
427```
428
429### 2.2 Hooks (`src/main/claude/hooks.ts`)
430
431```typescript
432import { HookCallback, PreToolUseHookInput } from "@anthropic-ai/claude-agent-sdk";
433import { Phase } from "../db/sessions";
434
435export function createPhaseEnforcementHook(getPhase: () => Phase): HookCallback {
436 return async (input, toolUseID, { signal }) => {
437 if (input.hook_event_name !== "PreToolUse") return {};
438
439 const preInput = input as PreToolUseHookInput;
440 const phase = getPhase();
441 const toolInput = preInput.tool_input as Record<string, unknown>;
442 const filePath = toolInput?.file_path as string | undefined;
443
444 // In non-implement phases, only allow writes to .claude-flow/ artifacts
445 if (phase !== "implement" && ["Write", "Edit"].includes(preInput.tool_name)) {
446 if (filePath && !filePath.includes(".claude-flow/")) {
447 return {
448 hookSpecificOutput: {
449 hookEventName: "PreToolUse",
450 permissionDecision: "deny",
451 permissionDecisionReason: `Cannot modify ${filePath} in ${phase} phase. Only .claude-flow/ artifacts allowed.`,
452 },
453 };
454 }
455 }
456
457 return {};
458 };
459}
460```
461
462### 2.3 Claude Wrapper (`src/main/claude/index.ts`)
463
464```typescript
465import { query, SDKMessage } from "@anthropic-ai/claude-agent-sdk";
466import { Session, Phase, updateSession } from "../db/sessions";
467import { getPhaseConfig } from "./phases";
468import { createPhaseEnforcementHook } from "./hooks";
469import { getProject } from "../db/projects";
470
471// Active queries by session ID
472const activeQueries = new Map<string, ReturnType<typeof query>>();
473
474export interface SendMessageOptions {
475 session: Session;
476 message: string;
477 onMessage: (msg: SDKMessage) => void;
478}
479
480export async function sendMessage({ session, message, onMessage }: SendMessageOptions): Promise<void> {
481 const project = getProject(session.project_id);
482 if (!project) throw new Error("Project not found");
483
484 const phaseConfig = getPhaseConfig(session.phase, session.permission_mode);
485
486 // Create phase getter that reads current session state
487 const getPhase = (): Phase => {
488 // In a real app, read from DB; here we use closure
489 return session.phase;
490 };
491
492 const q = query({
493 prompt: message,
494 options: {
495 cwd: project.path,
496 resume: session.claude_session_id ?? undefined,
497 systemPrompt: phaseConfig.systemPrompt,
498 allowedTools: phaseConfig.allowedTools,
499 permissionMode: phaseConfig.permissionMode,
500 hooks: {
501 PreToolUse: [
502 { hooks: [createPhaseEnforcementHook(getPhase)] },
503 ],
504 },
505 },
506 });
507
508 activeQueries.set(session.id, q);
509
510 try {
511 for await (const msg of q) {
512 // Capture session ID from init message
513 if (msg.type === "system" && msg.subtype === "init") {
514 if (!session.claude_session_id) {
515 updateSession(session.id, { claude_session_id: msg.session_id });
516 session.claude_session_id = msg.session_id;
517 }
518 }
519
520 onMessage(msg);
521 }
522 } finally {
523 activeQueries.delete(session.id);
524 }
525}
526
527export function interruptSession(sessionId: string): void {
528 const q = activeQueries.get(sessionId);
529 if (q) {
530 q.close();
531 activeQueries.delete(sessionId);
532 }
533}
534
535export async function setSessionPhase(session: Session, phase: Phase): Promise<void> {
536 updateSession(session.id, { phase });
537
538 // If there's an active query, update its permission mode
539 const q = activeQueries.get(session.id);
540 if (q && phase === "implement") {
541 await q.setPermissionMode(session.permission_mode);
542 }
543}
544```
545
546---
547
548## Phase 3: IPC Layer
549
550### 3.1 Preload (`src/main/preload.ts`)
551
552```typescript
553import { contextBridge, ipcRenderer, IpcRendererEvent } from "electron";
554
555export interface ClaudeFlowAPI {
556 // Projects
557 listProjects: () => Promise<any[]>;
558 createProject: (name: string, path: string) => Promise<any>;
559 deleteProject: (id: string) => Promise<void>;
560
561 // Sessions
562 listSessions: (projectId: string) => Promise<any[]>;
563 getSession: (id: string) => Promise<any>;
564 createSession: (projectId: string, name: string) => Promise<any>;
565 deleteSession: (id: string) => Promise<void>;
566 setPhase: (sessionId: string, phase: string) => Promise<void>;
567 setPermissionMode: (sessionId: string, mode: string) => Promise<void>;
568
569 // Messages
570 listMessages: (sessionId: string) => Promise<any[]>;
571 sendMessage: (sessionId: string, message: string) => Promise<void>;
572 interruptSession: (sessionId: string) => Promise<void>;
573
574 // Events
575 onClaudeMessage: (callback: (sessionId: string, message: any) => void) => () => void;
576
577 // Dialogs
578 selectDirectory: () => Promise<string | null>;
579}
580
581contextBridge.exposeInMainWorld("api", {
582 // Projects
583 listProjects: () => ipcRenderer.invoke("projects:list"),
584 createProject: (name: string, path: string) => ipcRenderer.invoke("projects:create", name, path),
585 deleteProject: (id: string) => ipcRenderer.invoke("projects:delete", id),
586
587 // Sessions
588 listSessions: (projectId: string) => ipcRenderer.invoke("sessions:list", projectId),
589 getSession: (id: string) => ipcRenderer.invoke("sessions:get", id),
590 createSession: (projectId: string, name: string) =>
591 ipcRenderer.invoke("sessions:create", projectId, name),
592 deleteSession: (id: string) => ipcRenderer.invoke("sessions:delete", id),
593 setPhase: (sessionId: string, phase: string) =>
594 ipcRenderer.invoke("sessions:setPhase", sessionId, phase),
595 setPermissionMode: (sessionId: string, mode: string) =>
596 ipcRenderer.invoke("sessions:setPermissionMode", sessionId, mode),
597
598 // Messages
599 listMessages: (sessionId: string) => ipcRenderer.invoke("messages:list", sessionId),
600 sendMessage: (sessionId: string, message: string) =>
601 ipcRenderer.invoke("claude:send", sessionId, message),
602 interruptSession: (sessionId: string) => ipcRenderer.invoke("claude:interrupt", sessionId),
603
604 // Events
605 onClaudeMessage: (callback: (sessionId: string, message: any) => void) => {
606 const handler = (_event: IpcRendererEvent, sessionId: string, message: any) => {
607 callback(sessionId, message);
608 };
609 ipcRenderer.on("claude:message", handler);
610 return () => ipcRenderer.removeListener("claude:message", handler);
611 },
612
613 // Dialogs
614 selectDirectory: () => ipcRenderer.invoke("dialog:selectDirectory"),
615} satisfies ClaudeFlowAPI);
616```
617
618### 3.2 IPC Handlers (`src/main/ipc/index.ts`)
619
620```typescript
621import { ipcMain, dialog, BrowserWindow } from "electron";
622import * as projects from "../db/projects";
623import * as sessions from "../db/sessions";
624import * as claude from "../claude";
625
626export function registerIpcHandlers(mainWindow: BrowserWindow) {
627 // Projects
628 ipcMain.handle("projects:list", () => projects.listProjects());
629 ipcMain.handle("projects:create", (_, name: string, path: string) =>
630 projects.createProject(name, path)
631 );
632 ipcMain.handle("projects:delete", (_, id: string) => projects.deleteProject(id));
633
634 // Sessions
635 ipcMain.handle("sessions:list", (_, projectId: string) => sessions.listSessions(projectId));
636 ipcMain.handle("sessions:get", (_, id: string) => sessions.getSession(id));
637 ipcMain.handle("sessions:create", (_, projectId: string, name: string) =>
638 sessions.createSession(projectId, name)
639 );
640 ipcMain.handle("sessions:delete", (_, id: string) => sessions.deleteSession(id));
641 ipcMain.handle("sessions:setPhase", async (_, sessionId: string, phase: string) => {
642 const session = sessions.getSession(sessionId);
643 if (session) {
644 await claude.setSessionPhase(session, phase as sessions.Phase);
645 }
646 });
647 ipcMain.handle("sessions:setPermissionMode", (_, sessionId: string, mode: string) => {
648 sessions.updateSession(sessionId, { permission_mode: mode as sessions.PermissionMode });
649 });
650
651 // Messages
652 ipcMain.handle("messages:list", (_, sessionId: string) => sessions.listMessages(sessionId));
653 ipcMain.handle("claude:send", async (_, sessionId: string, message: string) => {
654 const session = sessions.getSession(sessionId);
655 if (!session) throw new Error("Session not found");
656
657 // Store user message
658 sessions.addMessage(sessionId, "user", message);
659
660 // Send to Claude
661 await claude.sendMessage({
662 session,
663 message,
664 onMessage: (msg) => {
665 // Forward messages to renderer
666 mainWindow.webContents.send("claude:message", sessionId, msg);
667
668 // Store assistant messages
669 if (msg.type === "assistant") {
670 const content = msg.message.content
671 .filter((c: any) => c.type === "text")
672 .map((c: any) => c.text)
673 .join("\n");
674 if (content) {
675 sessions.addMessage(sessionId, "assistant", content);
676 }
677 }
678 },
679 });
680 });
681 ipcMain.handle("claude:interrupt", (_, sessionId: string) => {
682 claude.interruptSession(sessionId);
683 });
684
685 // Dialogs
686 ipcMain.handle("dialog:selectDirectory", async () => {
687 const result = await dialog.showOpenDialog(mainWindow, {
688 properties: ["openDirectory"],
689 });
690 return result.canceled ? null : result.filePaths[0];
691 });
692}
693```
694
695### 3.3 Update Main Entry (`src/main/index.ts`)
696
697```typescript
698import { app, BrowserWindow } from "electron";
699import path from "node:path";
700import { getDb, closeDb } from "./db";
701import { registerIpcHandlers } from "./ipc";
702
703const isDev = !app.isPackaged;
704
705let mainWindow: BrowserWindow | null = null;
706
707function createWindow() {
708 mainWindow = new BrowserWindow({
709 width: 1400,
710 height: 900,
711 minWidth: 800,
712 minHeight: 600,
713 show: false,
714 webPreferences: {
715 contextIsolation: true,
716 nodeIntegration: false,
717 preload: path.join(__dirname, "preload.js"),
718 },
719 });
720
721 // Register IPC handlers
722 registerIpcHandlers(mainWindow);
723
724 if (isDev) {
725 const url = process.env.VITE_DEV_SERVER_URL ?? "http://localhost:5173";
726 mainWindow.loadURL(url).finally(() => {
727 mainWindow!.show();
728 mainWindow!.webContents.openDevTools({ mode: "detach" });
729 });
730 } else {
731 const indexHtml = path.join(app.getAppPath(), "renderer", "dist", "index.html");
732 mainWindow.loadFile(indexHtml).finally(() => mainWindow!.show());
733 }
734}
735
736app.whenReady().then(() => {
737 // Initialize database
738 getDb();
739
740 createWindow();
741
742 app.on("activate", () => {
743 if (BrowserWindow.getAllWindows().length === 0) {
744 createWindow();
745 }
746 });
747});
748
749app.on("window-all-closed", () => {
750 closeDb();
751 if (process.platform !== "darwin") {
752 app.quit();
753 }
754});
755```
756
757---
758
759## Phase 4: React UI
760
761### 4.1 Types (`renderer/src/types.ts`)
762
763```typescript
764export interface Project {
765 id: string;
766 name: string;
767 path: string;
768 created_at: number;
769 updated_at: number;
770}
771
772export interface Session {
773 id: string;
774 project_id: string;
775 name: string;
776 phase: Phase;
777 claude_session_id: string | null;
778 permission_mode: PermissionMode;
779 created_at: number;
780 updated_at: number;
781}
782
783export type Phase = "research" | "plan" | "annotate" | "implement";
784export type PermissionMode = "acceptEdits" | "bypassPermissions";
785
786export interface Message {
787 id: string;
788 session_id: string;
789 role: "user" | "assistant" | "system";
790 content: string;
791 tool_use: string | null;
792 created_at: number;
793}
794```
795
796### 4.2 API Wrapper (`renderer/src/lib/api.ts`)
797
798```typescript
799import type { Project, Session, Message, Phase, PermissionMode } from "../types";
800
801declare global {
802 interface Window {
803 api: {
804 listProjects: () => Promise<Project[]>;
805 createProject: (name: string, path: string) => Promise<Project>;
806 deleteProject: (id: string) => Promise<void>;
807
808 listSessions: (projectId: string) => Promise<Session[]>;
809 getSession: (id: string) => Promise<Session | undefined>;
810 createSession: (projectId: string, name: string) => Promise<Session>;
811 deleteSession: (id: string) => Promise<void>;
812 setPhase: (sessionId: string, phase: Phase) => Promise<void>;
813 setPermissionMode: (sessionId: string, mode: PermissionMode) => Promise<void>;
814
815 listMessages: (sessionId: string) => Promise<Message[]>;
816 sendMessage: (sessionId: string, message: string) => Promise<void>;
817 interruptSession: (sessionId: string) => Promise<void>;
818
819 onClaudeMessage: (callback: (sessionId: string, message: any) => void) => () => void;
820
821 selectDirectory: () => Promise<string | null>;
822 };
823 }
824}
825
826export const api = window.api;
827```
828
829### 4.3 App Component (`renderer/src/App.tsx`)
830
831```typescript
832import React, { useState, useEffect } from "react";
833import { api } from "./lib/api";
834import type { Project, Session } from "./types";
835import { Sidebar } from "./components/Sidebar";
836import { Chat } from "./components/Chat";
837import { PhaseBar } from "./components/PhaseBar";
838import "./styles/globals.css";
839
840export function App() {
841 const [projects, setProjects] = useState<Project[]>([]);
842 const [selectedProject, setSelectedProject] = useState<Project | null>(null);
843 const [selectedSession, setSelectedSession] = useState<Session | null>(null);
844
845 useEffect(() => {
846 api.listProjects().then(setProjects);
847 }, []);
848
849 const handleCreateProject = async () => {
850 const path = await api.selectDirectory();
851 if (!path) return;
852
853 const name = path.split("/").pop() || "New Project";
854 const project = await api.createProject(name, path);
855 setProjects((prev) => [project, ...prev]);
856 setSelectedProject(project);
857 };
858
859 const handleSelectSession = async (session: Session) => {
860 setSelectedSession(session);
861 };
862
863 return (
864 <div className="app">
865 <Sidebar
866 projects={projects}
867 selectedProject={selectedProject}
868 selectedSession={selectedSession}
869 onSelectProject={setSelectedProject}
870 onSelectSession={handleSelectSession}
871 onCreateProject={handleCreateProject}
872 />
873 <main className="main">
874 {selectedSession ? (
875 <>
876 <PhaseBar
877 session={selectedSession}
878 onPhaseChange={(phase) => {
879 api.setPhase(selectedSession.id, phase);
880 setSelectedSession({ ...selectedSession, phase });
881 }}
882 onPermissionModeChange={(mode) => {
883 api.setPermissionMode(selectedSession.id, mode);
884 setSelectedSession({ ...selectedSession, permission_mode: mode });
885 }}
886 />
887 <Chat session={selectedSession} />
888 </>
889 ) : (
890 <div className="empty-state">
891 {selectedProject
892 ? "Select or create a session to start"
893 : "Select or create a project to start"}
894 </div>
895 )}
896 </main>
897 </div>
898 );
899}
900```
901
902### 4.4 Sidebar (`renderer/src/components/Sidebar.tsx`)
903
904```typescript
905import React, { useState, useEffect } from "react";
906import { api } from "../lib/api";
907import type { Project, Session } from "../types";
908
909interface SidebarProps {
910 projects: Project[];
911 selectedProject: Project | null;
912 selectedSession: Session | null;
913 onSelectProject: (project: Project) => void;
914 onSelectSession: (session: Session) => void;
915 onCreateProject: () => void;
916}
917
918export function Sidebar({
919 projects,
920 selectedProject,
921 selectedSession,
922 onSelectProject,
923 onSelectSession,
924 onCreateProject,
925}: SidebarProps) {
926 const [sessions, setSessions] = useState<Session[]>([]);
927
928 useEffect(() => {
929 if (selectedProject) {
930 api.listSessions(selectedProject.id).then(setSessions);
931 } else {
932 setSessions([]);
933 }
934 }, [selectedProject]);
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 onSelectSession(session);
942 };
943
944 return (
945 <aside className="sidebar">
946 <div className="sidebar-header">
947 <h2>Projects</h2>
948 <button onClick={onCreateProject}>+</button>
949 </div>
950
951 <ul className="project-list">
952 {projects.map((project) => (
953 <li key={project.id}>
954 <button
955 className={project.id === selectedProject?.id ? "selected" : ""}
956 onClick={() => onSelectProject(project)}
957 >
958 {project.name}
959 </button>
960 </li>
961 ))}
962 </ul>
963
964 {selectedProject && (
965 <>
966 <div className="sidebar-header">
967 <h3>Sessions</h3>
968 <button onClick={handleCreateSession}>+</button>
969 </div>
970
971 <ul className="session-list">
972 {sessions.map((session) => (
973 <li key={session.id}>
974 <button
975 className={session.id === selectedSession?.id ? "selected" : ""}
976 onClick={() => onSelectSession(session)}
977 >
978 <span className="session-name">{session.name}</span>
979 <span className={`phase-badge phase-${session.phase}`}>{session.phase}</span>
980 </button>
981 </li>
982 ))}
983 </ul>
984 </>
985 )}
986 </aside>
987 );
988}
989```
990
991### 4.5 PhaseBar (`renderer/src/components/PhaseBar.tsx`)
992
993```typescript
994import React from "react";
995import type { Session, Phase, PermissionMode } from "../types";
996
997interface PhaseBarProps {
998 session: Session;
999 onPhaseChange: (phase: Phase) => void;
1000 onPermissionModeChange: (mode: PermissionMode) => void;
1001}
1002
1003const phases: Phase[] = ["research", "plan", "annotate", "implement"];
1004
1005export function PhaseBar({ session, onPhaseChange, onPermissionModeChange }: PhaseBarProps) {
1006 return (
1007 <div className="phase-bar">
1008 <div className="phase-buttons">
1009 {phases.map((phase) => (
1010 <button
1011 key={phase}
1012 className={session.phase === phase ? "active" : ""}
1013 onClick={() => onPhaseChange(phase)}
1014 >
1015 {phase.charAt(0).toUpperCase() + phase.slice(1)}
1016 </button>
1017 ))}
1018 </div>
1019
1020 {session.phase === "implement" && (
1021 <div className="permission-toggle">
1022 <label>
1023 <input
1024 type="checkbox"
1025 checked={session.permission_mode === "bypassPermissions"}
1026 onChange={(e) =>
1027 onPermissionModeChange(e.target.checked ? "bypassPermissions" : "acceptEdits")
1028 }
1029 />
1030 Bypass Permissions
1031 </label>
1032 </div>
1033 )}
1034 </div>
1035 );
1036}
1037```
1038
1039### 4.6 Chat (`renderer/src/components/Chat.tsx`)
1040
1041```typescript
1042import React, { useState, useEffect, useRef } from "react";
1043import { api } from "../lib/api";
1044import type { Session, Message } from "../types";
1045
1046interface ChatProps {
1047 session: Session;
1048}
1049
1050export function Chat({ session }: ChatProps) {
1051 const [messages, setMessages] = useState<Message[]>([]);
1052 const [input, setInput] = useState("");
1053 const [isLoading, setIsLoading] = useState(false);
1054 const messagesEndRef = useRef<HTMLDivElement>(null);
1055
1056 // Load messages
1057 useEffect(() => {
1058 api.listMessages(session.id).then(setMessages);
1059 }, [session.id]);
1060
1061 // Subscribe to Claude messages
1062 useEffect(() => {
1063 const unsubscribe = api.onClaudeMessage((sessionId, msg) => {
1064 if (sessionId !== session.id) return;
1065
1066 if (msg.type === "assistant") {
1067 const content = msg.message.content
1068 .filter((c: any) => c.type === "text")
1069 .map((c: any) => c.text)
1070 .join("\n");
1071
1072 if (content) {
1073 setMessages((prev) => {
1074 // Update last assistant message or add new one
1075 const lastMsg = prev[prev.length - 1];
1076 if (lastMsg?.role === "assistant") {
1077 return [...prev.slice(0, -1), { ...lastMsg, content }];
1078 }
1079 return [
1080 ...prev,
1081 {
1082 id: crypto.randomUUID(),
1083 session_id: session.id,
1084 role: "assistant",
1085 content,
1086 tool_use: null,
1087 created_at: Date.now() / 1000,
1088 },
1089 ];
1090 });
1091 }
1092 }
1093
1094 if (msg.type === "result") {
1095 setIsLoading(false);
1096 }
1097 });
1098
1099 return unsubscribe;
1100 }, [session.id]);
1101
1102 // Auto-scroll
1103 useEffect(() => {
1104 messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
1105 }, [messages]);
1106
1107 const handleSend = async () => {
1108 if (!input.trim() || isLoading) return;
1109
1110 const userMessage: Message = {
1111 id: crypto.randomUUID(),
1112 session_id: session.id,
1113 role: "user",
1114 content: input,
1115 tool_use: null,
1116 created_at: Date.now() / 1000,
1117 };
1118
1119 setMessages((prev) => [...prev, userMessage]);
1120 setInput("");
1121 setIsLoading(true);
1122
1123 try {
1124 await api.sendMessage(session.id, input);
1125 } catch (error) {
1126 console.error("Failed to send message:", error);
1127 setIsLoading(false);
1128 }
1129 };
1130
1131 return (
1132 <div className="chat">
1133 <div className="messages">
1134 {messages.map((msg) => (
1135 <div key={msg.id} className={`message message-${msg.role}`}>
1136 <div className="message-content">{msg.content}</div>
1137 </div>
1138 ))}
1139 {isLoading && <div className="message message-loading">Claude is thinking...</div>}
1140 <div ref={messagesEndRef} />
1141 </div>
1142
1143 <div className="chat-input">
1144 <textarea
1145 value={input}
1146 onChange={(e) => setInput(e.target.value)}
1147 onKeyDown={(e) => {
1148 if (e.key === "Enter" && !e.shiftKey) {
1149 e.preventDefault();
1150 handleSend();
1151 }
1152 }}
1153 placeholder={`Message Claude (${session.phase} mode)...`}
1154 disabled={isLoading}
1155 />
1156 <button onClick={handleSend} disabled={isLoading || !input.trim()}>
1157 Send
1158 </button>
1159 {isLoading && (
1160 <button onClick={() => api.interruptSession(session.id)} className="interrupt">
1161 Stop
1162 </button>
1163 )}
1164 </div>
1165 </div>
1166 );
1167}
1168```
1169
1170### 4.7 Basic Styles (`renderer/src/styles/globals.css`)
1171
1172```css
1173* {
1174 box-sizing: border-box;
1175 margin: 0;
1176 padding: 0;
1177}
1178
1179body {
1180 font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
1181 background: #1a1a1a;
1182 color: #e0e0e0;
1183}
1184
1185.app {
1186 display: flex;
1187 height: 100vh;
1188}
1189
1190/* Sidebar */
1191.sidebar {
1192 width: 260px;
1193 background: #252525;
1194 border-right: 1px solid #333;
1195 display: flex;
1196 flex-direction: column;
1197 overflow-y: auto;
1198}
1199
1200.sidebar-header {
1201 display: flex;
1202 justify-content: space-between;
1203 align-items: center;
1204 padding: 16px;
1205 border-bottom: 1px solid #333;
1206}
1207
1208.sidebar-header h2,
1209.sidebar-header h3 {
1210 font-size: 14px;
1211 text-transform: uppercase;
1212 letter-spacing: 0.5px;
1213 color: #888;
1214}
1215
1216.sidebar-header button {
1217 background: #3b82f6;
1218 border: none;
1219 color: white;
1220 width: 24px;
1221 height: 24px;
1222 border-radius: 4px;
1223 cursor: pointer;
1224}
1225
1226.project-list,
1227.session-list {
1228 list-style: none;
1229}
1230
1231.project-list button,
1232.session-list button {
1233 width: 100%;
1234 padding: 12px 16px;
1235 background: none;
1236 border: none;
1237 color: #e0e0e0;
1238 text-align: left;
1239 cursor: pointer;
1240 display: flex;
1241 justify-content: space-between;
1242 align-items: center;
1243}
1244
1245.project-list button:hover,
1246.session-list button:hover {
1247 background: #333;
1248}
1249
1250.project-list button.selected,
1251.session-list button.selected {
1252 background: #3b82f6;
1253}
1254
1255.phase-badge {
1256 font-size: 10px;
1257 padding: 2px 6px;
1258 border-radius: 4px;
1259 text-transform: uppercase;
1260}
1261
1262.phase-research { background: #7c3aed; }
1263.phase-plan { background: #f59e0b; }
1264.phase-annotate { background: #10b981; }
1265.phase-implement { background: #3b82f6; }
1266
1267/* Main */
1268.main {
1269 flex: 1;
1270 display: flex;
1271 flex-direction: column;
1272 overflow: hidden;
1273}
1274
1275.empty-state {
1276 flex: 1;
1277 display: flex;
1278 align-items: center;
1279 justify-content: center;
1280 color: #666;
1281}
1282
1283/* Phase Bar */
1284.phase-bar {
1285 display: flex;
1286 justify-content: space-between;
1287 align-items: center;
1288 padding: 12px 16px;
1289 background: #252525;
1290 border-bottom: 1px solid #333;
1291}
1292
1293.phase-buttons {
1294 display: flex;
1295 gap: 8px;
1296}
1297
1298.phase-buttons button {
1299 padding: 8px 16px;
1300 background: #333;
1301 border: none;
1302 color: #888;
1303 border-radius: 4px;
1304 cursor: pointer;
1305}
1306
1307.phase-buttons button.active {
1308 background: #3b82f6;
1309 color: white;
1310}
1311
1312.permission-toggle {
1313 display: flex;
1314 align-items: center;
1315 gap: 8px;
1316 color: #888;
1317 font-size: 14px;
1318}
1319
1320/* Chat */
1321.chat {
1322 flex: 1;
1323 display: flex;
1324 flex-direction: column;
1325 overflow: hidden;
1326}
1327
1328.messages {
1329 flex: 1;
1330 overflow-y: auto;
1331 padding: 16px;
1332}
1333
1334.message {
1335 margin-bottom: 16px;
1336 padding: 12px 16px;
1337 border-radius: 8px;
1338 max-width: 80%;
1339}
1340
1341.message-user {
1342 background: #3b82f6;
1343 margin-left: auto;
1344}
1345
1346.message-assistant {
1347 background: #333;
1348}
1349
1350.message-loading {
1351 background: #333;
1352 color: #888;
1353 font-style: italic;
1354}
1355
1356.chat-input {
1357 display: flex;
1358 gap: 8px;
1359 padding: 16px;
1360 background: #252525;
1361 border-top: 1px solid #333;
1362}
1363
1364.chat-input textarea {
1365 flex: 1;
1366 padding: 12px;
1367 background: #333;
1368 border: 1px solid #444;
1369 border-radius: 8px;
1370 color: #e0e0e0;
1371 resize: none;
1372 min-height: 48px;
1373 max-height: 200px;
1374}
1375
1376.chat-input button {
1377 padding: 12px 24px;
1378 background: #3b82f6;
1379 border: none;
1380 color: white;
1381 border-radius: 8px;
1382 cursor: pointer;
1383}
1384
1385.chat-input button:disabled {
1386 opacity: 0.5;
1387 cursor: not-allowed;
1388}
1389
1390.chat-input button.interrupt {
1391 background: #ef4444;
1392}
1393```
1394
1395---
1396
1397## Phase 5: Wiring & Polish
1398
1399### 5.1 Update `renderer/src/main.tsx`
1400
1401```typescript
1402import React from "react";
1403import { createRoot } from "react-dom/client";
1404import { App } from "./App";
1405
1406createRoot(document.getElementById("root")!).render(
1407 <React.StrictMode>
1408 <App />
1409 </React.StrictMode>
1410);
1411```
1412
1413### 5.2 Update `package.json` Dependencies
1414
1415Add to dependencies:
1416```json
1417{
1418 "dependencies": {
1419 "@anthropic-ai/claude-agent-sdk": "^0.1.0",
1420 "better-sqlite3": "12.2.0",
1421 "react": "^19.1.1",
1422 "react-dom": "^19.1.1",
1423 "uuid": "^11.0.0"
1424 },
1425 "devDependencies": {
1426 "@types/uuid": "^10.0.0"
1427 }
1428}
1429```
1430
1431### 5.3 Ensure `.claude-flow/` Directory Exists
1432
1433In `src/main/claude/index.ts`, add helper to create artifact directory:
1434
1435```typescript
1436import fs from "node:fs";
1437import path from "node:path";
1438
1439function ensureArtifactDir(projectPath: string) {
1440 const artifactDir = path.join(projectPath, ".claude-flow");
1441 if (!fs.existsSync(artifactDir)) {
1442 fs.mkdirSync(artifactDir, { recursive: true });
1443 }
1444}
1445```
1446
1447---
1448
1449## TODO List
1450
1451### Phase 1: Database Layer
1452- [ ] Create `src/main/db/` directory
1453- [ ] Implement `src/main/db/schema.ts`
1454- [ ] Implement `src/main/db/index.ts`
1455- [ ] Implement `src/main/db/projects.ts`
1456- [ ] Implement `src/main/db/sessions.ts`
1457
1458### Phase 2: Claude Integration
1459- [ ] Create `src/main/claude/` directory
1460- [ ] Implement `src/main/claude/phases.ts`
1461- [ ] Implement `src/main/claude/hooks.ts`
1462- [ ] Implement `src/main/claude/index.ts`
1463- [ ] Add `@anthropic-ai/claude-agent-sdk` dependency
1464
1465### Phase 3: IPC Layer
1466- [ ] Create `src/main/ipc/` directory
1467- [ ] Implement `src/main/preload.ts`
1468- [ ] Implement `src/main/ipc/index.ts`
1469- [ ] Update `src/main/index.ts`
1470
1471### Phase 4: React UI
1472- [ ] Create `renderer/src/types.ts`
1473- [ ] Create `renderer/src/lib/api.ts`
1474- [ ] Create `renderer/src/styles/globals.css`
1475- [ ] Implement `renderer/src/App.tsx`
1476- [ ] Implement `renderer/src/components/Sidebar.tsx`
1477- [ ] Implement `renderer/src/components/PhaseBar.tsx`
1478- [ ] Implement `renderer/src/components/Chat.tsx`
1479- [ ] Update `renderer/src/main.tsx`
1480
1481### Phase 5: Wiring & Polish
1482- [ ] Update `package.json` with new dependencies
1483- [ ] Add `uuid` and `@types/uuid`
1484- [ ] Add `.claude-flow/` to `.gitignore` template
1485- [ ] Test full workflow: research → plan → annotate → implement
1486- [ ] Add keyboard shortcuts (Cmd+Enter to send)
1487- [ ] Add session rename/delete UI