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