# Plan: Claude Flow Implementation
## Overview
A document-centric coding assistant that enforces a structured workflow: **Research → Plan → Implement**.
The primary UI is a **markdown document viewer/editor** with a **chat sidebar**. The workflow is driven by the document, not the chat.
---
## User Flow
### 1. Project Setup
- Add a project by selecting a folder
- Start a new session within the project
### 2. Research Phase
- Chat dialogue: Claude asks what to research and what you want to build
- You provide direction via chat
- Claude generates `research.md` → displayed as rendered markdown
- You edit the document (add comments, adjustments)
- App detects changes → enables **[Review]** and **[Submit]** buttons
- **Review**: Claude reads your changes and adjusts the document
- **Submit**: Move to Plan phase
### 3. Plan Phase
- Claude generates `plan.md` based on research
- Displayed as rendered markdown
- You edit, iterate with **[Review]**
- **Submit**: Kicks off Implementation
### 4. Implement Phase
- Claude executes the plan
- Marks tasks complete as it goes
- Chat shows progress and tool usage
---
## UI Layout
```
┌──────────────────────────────────────────────────────────────┐
│ ┌─────────────────┐ ┌─────────────────┐ [Research ● ───]│
│ │ Project ▾ │ │ Session ▾ │ [Plan ○────────]│
│ └─────────────────┘ └─────────────────┘ [Implement ○───]│
├────────────────────────────────────────┬─────────────────────┤
│ │ │
│ ┌────────────────────────────────┐ │ Chat Dialogue │
│ │ │ │ │
│ │ # Research Findings │ │ ┌───────────────┐ │
│ │ │ │ │ What areas │ │
│ │ ## Authentication System │ │ │ should I │ │
│ │ │ │ │ research? │ │
│ │ The auth module uses JWT... │ │ └───────────────┘ │
│ │ │ │ │
│ │ // REVIEW: check OAuth too │ │ ┌───────────────┐ │
│ │ │ │ │ Research the │ │
│ │ │ │ │ auth system, │ │
│ │ │ │ │ I want OAuth │ │
│ └────────────────────────────────┘ │ └───────────────┘ │
│ │ │
│ 42k / 200k tokens ████░░░░░░░ │ │
├────────────────────────────────────────┼─────────────────────┤
│ [Review] [Submit →] │ [____________] ⏎ │
└────────────────────────────────────────┴─────────────────────┘
```
### Key UI Elements
| Element | Behavior |
|---------|----------|
| **Project dropdown** | Select/create projects |
| **Session dropdown** | Select/create sessions within project |
| **Phase indicator** | Shows current phase (Research → Plan → Implement) |
| **Document pane** | Rendered markdown, editable |
| **Chat pane** | Dialogue with Claude |
| **Review button** | Disabled until document edited. Triggers Claude to read changes. |
| **Submit button** | Advances to next phase |
| **Token indicator** | Shows context usage |
---
## Directory Structure
```
claude-flow/
├── src/main/
│ ├── index.ts # App lifecycle, window management
│ ├── preload.ts # IPC bridge
│ ├── db/
│ │ ├── index.ts # Database connection singleton
│ │ ├── schema.ts # Table definitions + migrations
│ │ ├── projects.ts # Project CRUD
│ │ └── sessions.ts # Session CRUD
│ ├── claude/
│ │ ├── index.ts # Claude SDK wrapper
│ │ ├── phases.ts # Phase configs (prompts, tools, permissions)
│ │ └── hooks.ts # Custom hooks for phase enforcement
│ └── ipc/
│ └── handlers.ts # All IPC handlers
├── renderer/
│ ├── index.html
│ └── src/
│ ├── main.tsx
│ ├── App.tsx
│ ├── components/
│ │ ├── Header.tsx # Project/session dropdowns + phase indicator
│ │ ├── DocumentPane.tsx # Markdown viewer/editor
│ │ ├── ChatPane.tsx # Chat dialogue
│ │ ├── ActionBar.tsx # Review/Submit buttons + token indicator
│ │ └── Message.tsx # Single chat message
│ ├── lib/
│ │ ├── api.ts # Typed IPC wrapper
│ │ └── markdown.ts # Markdown rendering utilities
│ ├── types.ts
│ └── styles/
│ └── globals.css
├── package.json
├── tsconfig.json
└── vite.config.ts
```
---
## Phase 1: Database Layer
### 1.1 Schema (`src/main/db/schema.ts`)
```typescript
import Database from "better-sqlite3";
export function initSchema(db: Database.Database) {
db.exec(`
CREATE TABLE IF NOT EXISTS projects (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
path TEXT NOT NULL,
created_at INTEGER NOT NULL DEFAULT (unixepoch()),
updated_at INTEGER NOT NULL DEFAULT (unixepoch())
);
CREATE TABLE IF NOT EXISTS sessions (
id TEXT PRIMARY KEY,
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
name TEXT NOT NULL,
phase TEXT NOT NULL DEFAULT 'research',
claude_session_id TEXT,
permission_mode TEXT NOT NULL DEFAULT 'acceptEdits',
created_at INTEGER NOT NULL DEFAULT (unixepoch()),
updated_at INTEGER NOT NULL DEFAULT (unixepoch())
);
CREATE TABLE IF NOT EXISTS messages (
id TEXT PRIMARY KEY,
session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
role TEXT NOT NULL,
content TEXT NOT NULL,
created_at INTEGER NOT NULL DEFAULT (unixepoch())
);
CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_id);
CREATE INDEX IF NOT EXISTS idx_messages_session ON messages(session_id);
`);
}
```
**Notes:**
- `phase` is one of: `research`, `plan`, `implement`
- `claude_session_id` stores the SDK's session ID for resuming
- `permission_mode`: `acceptEdits` or `bypassPermissions` (user toggle in implement phase)
### 1.2 Database Connection (`src/main/db/index.ts`)
```typescript
import Database from "better-sqlite3";
import { app } from "electron";
import path from "node:path";
import fs from "node:fs";
import { initSchema } from "./schema";
let db: Database.Database | null = null;
export function getDb(): Database.Database {
if (db) return db;
const dbDir = app.getPath("userData");
if (!fs.existsSync(dbDir)) {
fs.mkdirSync(dbDir, { recursive: true });
}
const dbPath = path.join(dbDir, "claude-flow.db");
db = new Database(dbPath);
db.pragma("journal_mode = WAL");
db.pragma("foreign_keys = ON");
initSchema(db);
return db;
}
export function closeDb() {
if (db) {
db.close();
db = null;
}
}
```
### 1.3 Project CRUD (`src/main/db/projects.ts`)
```typescript
import { getDb } from "./index";
import { v4 as uuid } from "uuid";
export interface Project {
id: string;
name: string;
path: string;
created_at: number;
updated_at: number;
}
export function listProjects(): Project[] {
return getDb()
.prepare("SELECT * FROM projects ORDER BY updated_at DESC")
.all() as Project[];
}
export function getProject(id: string): Project | undefined {
return getDb()
.prepare("SELECT * FROM projects WHERE id = ?")
.get(id) as Project | undefined;
}
export function createProject(name: string, projectPath: string): Project {
const db = getDb();
const id = uuid();
const now = Math.floor(Date.now() / 1000);
db.prepare(
"INSERT INTO projects (id, name, path, created_at, updated_at) VALUES (?, ?, ?, ?, ?)"
).run(id, name, projectPath, now, now);
return { id, name, path: projectPath, created_at: now, updated_at: now };
}
export function deleteProject(id: string): void {
getDb().prepare("DELETE FROM projects WHERE id = ?").run(id);
}
```
### 1.4 Session CRUD (`src/main/db/sessions.ts`)
```typescript
import { getDb } from "./index";
import { v4 as uuid } from "uuid";
export type Phase = "research" | "plan" | "implement";
export type PermissionMode = "acceptEdits" | "bypassPermissions";
export interface Session {
id: string;
project_id: string;
name: string;
phase: Phase;
claude_session_id: string | null;
permission_mode: PermissionMode;
created_at: number;
updated_at: number;
}
export interface Message {
id: string;
session_id: string;
role: "user" | "assistant";
content: string;
created_at: number;
}
export function listSessions(projectId: string): Session[] {
return getDb()
.prepare("SELECT * FROM sessions WHERE project_id = ? ORDER BY updated_at DESC")
.all(projectId) as Session[];
}
export function getSession(id: string): Session | undefined {
return getDb()
.prepare("SELECT * FROM sessions WHERE id = ?")
.get(id) as Session | undefined;
}
export function createSession(projectId: string, name: string): Session {
const db = getDb();
const id = uuid();
const now = Math.floor(Date.now() / 1000);
db.prepare(
`INSERT INTO sessions (id, project_id, name, phase, permission_mode, created_at, updated_at)
VALUES (?, ?, ?, 'research', 'acceptEdits', ?, ?)`
).run(id, projectId, name, now, now);
return {
id,
project_id: projectId,
name,
phase: "research",
claude_session_id: null,
permission_mode: "acceptEdits",
created_at: now,
updated_at: now,
};
}
export function updateSession(
id: string,
updates: Partial>
): void {
const db = getDb();
const sets: string[] = [];
const values: any[] = [];
for (const [key, value] of Object.entries(updates)) {
if (value !== undefined) {
sets.push(`${key} = ?`);
values.push(value);
}
}
if (sets.length > 0) {
sets.push("updated_at = ?");
values.push(Math.floor(Date.now() / 1000));
values.push(id);
db.prepare(`UPDATE sessions SET ${sets.join(", ")} WHERE id = ?`).run(...values);
}
}
export function deleteSession(id: string): void {
getDb().prepare("DELETE FROM sessions WHERE id = ?").run(id);
}
// Messages
export function listMessages(sessionId: string): Message[] {
return getDb()
.prepare("SELECT * FROM messages WHERE session_id = ? ORDER BY created_at ASC")
.all(sessionId) as Message[];
}
export function addMessage(sessionId: string, role: Message["role"], content: string): Message {
const db = getDb();
const id = uuid();
const now = Math.floor(Date.now() / 1000);
db.prepare(
"INSERT INTO messages (id, session_id, role, content, created_at) VALUES (?, ?, ?, ?, ?)"
).run(id, sessionId, role, content, now);
db.prepare("UPDATE sessions SET updated_at = ? WHERE id = ?").run(now, sessionId);
return { id, session_id: sessionId, role, content, created_at: now };
}
```
---
## Phase 2: Claude Integration
### 2.1 Phase Configs (`src/main/claude/phases.ts`)
```typescript
import { Phase, PermissionMode } from "../db/sessions";
export interface PhaseConfig {
systemPrompt: string;
allowedTools: string[];
permissionMode: "plan" | PermissionMode;
initialMessage: string; // What Claude says when entering this phase
}
export const phaseConfigs: Record = {
research: {
permissionMode: "plan",
allowedTools: ["Read", "Glob", "Grep", "WebSearch", "WebFetch", "Write"],
initialMessage: "What areas of the codebase should I research? What are you trying to build?",
systemPrompt: `You are in RESEARCH mode.
Your job is to deeply understand the codebase before any changes are made.
When the user tells you what to research:
1. Read files thoroughly — understand all intricacies
2. Write your findings to .claude-flow/research.md
3. Format it as clear, readable markdown
Rules:
- DO NOT make any code changes
- DO NOT modify any files except .claude-flow/research.md
- Be thorough — surface-level reading is not acceptable
When the user clicks "Review", read .claude-flow/research.md for their annotations and update accordingly.
When the user clicks "Submit", they're ready to move to planning.`,
},
plan: {
permissionMode: "plan",
allowedTools: ["Read", "Glob", "Grep", "Write"],
initialMessage: "I'll create a detailed implementation plan based on my research. Give me a moment...",
systemPrompt: `You are in PLANNING mode.
Based on the research in .claude-flow/research.md, write a detailed implementation plan.
Write the plan to .claude-flow/plan.md with:
- Detailed explanation of the approach
- Specific code snippets showing proposed changes
- File paths that will be modified
- Trade-offs and considerations
- A granular TODO list with checkboxes
Rules:
- DO NOT implement anything
- DO NOT modify any source files
- Only write to .claude-flow/plan.md
The plan should be detailed enough that implementation becomes mechanical.
When the user clicks "Review", read .claude-flow/plan.md for their annotations and update accordingly.
When the user clicks "Submit", begin implementation.`,
},
implement: {
permissionMode: "acceptEdits", // Will be overridden by user setting
allowedTools: ["Read", "Write", "Edit", "Bash", "Glob", "Grep"],
initialMessage: "Starting implementation. I'll follow the plan exactly and mark tasks complete as I go.",
systemPrompt: `You are in IMPLEMENTATION mode. The plan has been approved.
Read .claude-flow/plan.md and execute it:
- Follow the plan exactly
- Mark tasks complete (- [x]) as you finish them
- Run typecheck/lint continuously if available
- Do not add unnecessary comments
- Do not stop until all tasks are complete
If you encounter issues not covered by the plan, stop and ask.`,
},
};
export function getPhaseConfig(phase: Phase, userPermissionMode?: PermissionMode): PhaseConfig {
const config = { ...phaseConfigs[phase] };
if (phase === "implement" && userPermissionMode) {
config.permissionMode = userPermissionMode;
}
return config;
}
```
### 2.2 Claude Wrapper (`src/main/claude/index.ts`)
```typescript
import { query, SDKMessage } from "@anthropic-ai/claude-agent-sdk";
import { Session, Phase, updateSession, getSession } from "../db/sessions";
import { getPhaseConfig } from "./phases";
import { getProject } from "../db/projects";
import fs from "node:fs";
import path from "node:path";
const activeQueries = new Map>();
function ensureArtifactDir(projectPath: string): void {
const dir = path.join(projectPath, ".claude-flow");
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
}
export interface SendMessageOptions {
session: Session;
message: string;
onMessage: (msg: SDKMessage) => void;
}
export async function sendMessage({ session, message, onMessage }: SendMessageOptions): Promise {
const project = getProject(session.project_id);
if (!project) throw new Error("Project not found");
ensureArtifactDir(project.path);
const phaseConfig = getPhaseConfig(session.phase, session.permission_mode);
const q = query({
prompt: message,
options: {
cwd: project.path,
resume: session.claude_session_id ?? undefined,
systemPrompt: phaseConfig.systemPrompt,
allowedTools: phaseConfig.allowedTools,
permissionMode: phaseConfig.permissionMode,
},
});
activeQueries.set(session.id, q);
try {
for await (const msg of q) {
if (msg.type === "system" && msg.subtype === "init") {
if (!session.claude_session_id) {
updateSession(session.id, { claude_session_id: msg.session_id });
}
}
onMessage(msg);
}
} finally {
activeQueries.delete(session.id);
}
}
export function interruptSession(sessionId: string): void {
const q = activeQueries.get(sessionId);
if (q) {
q.close();
activeQueries.delete(sessionId);
}
}
// Trigger review: Claude reads the document and addresses annotations
export async function triggerReview(session: Session, onMessage: (msg: SDKMessage) => void): Promise {
const docName = session.phase === "research" ? "research.md" : "plan.md";
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.`;
await sendMessage({ session, message, onMessage });
}
// Advance to next phase
export function advancePhase(session: Session): Phase | null {
const nextPhase: Record = {
research: "plan",
plan: "implement",
implement: null,
};
const next = nextPhase[session.phase];
if (next) {
updateSession(session.id, { phase: next });
}
return next;
}
// Read artifact file
export function readArtifact(projectPath: string, filename: string): string | null {
const filePath = path.join(projectPath, ".claude-flow", filename);
if (fs.existsSync(filePath)) {
return fs.readFileSync(filePath, "utf-8");
}
return null;
}
// Write artifact file (for user edits)
export function writeArtifact(projectPath: string, filename: string, content: string): void {
const dir = path.join(projectPath, ".claude-flow");
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
fs.writeFileSync(path.join(dir, filename), content, "utf-8");
}
```
---
## Phase 3: IPC Layer
### 3.1 Preload (`src/main/preload.ts`)
```typescript
import { contextBridge, ipcRenderer, IpcRendererEvent } from "electron";
contextBridge.exposeInMainWorld("api", {
// Projects
listProjects: () => ipcRenderer.invoke("projects:list"),
createProject: (name: string, path: string) => ipcRenderer.invoke("projects:create", name, path),
deleteProject: (id: string) => ipcRenderer.invoke("projects:delete", id),
// Sessions
listSessions: (projectId: string) => ipcRenderer.invoke("sessions:list", projectId),
createSession: (projectId: string, name: string) => ipcRenderer.invoke("sessions:create", projectId, name),
deleteSession: (id: string) => ipcRenderer.invoke("sessions:delete", id),
getSession: (id: string) => ipcRenderer.invoke("sessions:get", id),
// Chat
sendMessage: (sessionId: string, message: string) => ipcRenderer.invoke("chat:send", sessionId, message),
interruptSession: (sessionId: string) => ipcRenderer.invoke("chat:interrupt", sessionId),
// Workflow
triggerReview: (sessionId: string) => ipcRenderer.invoke("workflow:review", sessionId),
advancePhase: (sessionId: string) => ipcRenderer.invoke("workflow:advance", sessionId),
setPermissionMode: (sessionId: string, mode: string) => ipcRenderer.invoke("workflow:setPermissionMode", sessionId, mode),
// Artifacts
readArtifact: (projectPath: string, filename: string) => ipcRenderer.invoke("artifact:read", projectPath, filename),
writeArtifact: (projectPath: string, filename: string, content: string) =>
ipcRenderer.invoke("artifact:write", projectPath, filename, content),
// Events
onClaudeMessage: (callback: (sessionId: string, message: any) => void) => {
const handler = (_: IpcRendererEvent, sessionId: string, message: any) => callback(sessionId, message);
ipcRenderer.on("claude:message", handler);
return () => ipcRenderer.removeListener("claude:message", handler);
},
// Dialogs
selectDirectory: () => ipcRenderer.invoke("dialog:selectDirectory"),
});
```
### 3.2 IPC Handlers (`src/main/ipc/handlers.ts`)
```typescript
import { ipcMain, dialog, BrowserWindow } from "electron";
import * as projects from "../db/projects";
import * as sessions from "../db/sessions";
import * as claude from "../claude";
export function registerIpcHandlers(mainWindow: BrowserWindow) {
// Projects
ipcMain.handle("projects:list", () => projects.listProjects());
ipcMain.handle("projects:create", (_, name: string, path: string) => projects.createProject(name, path));
ipcMain.handle("projects:delete", (_, id: string) => projects.deleteProject(id));
// Sessions
ipcMain.handle("sessions:list", (_, projectId: string) => sessions.listSessions(projectId));
ipcMain.handle("sessions:create", (_, projectId: string, name: string) => sessions.createSession(projectId, name));
ipcMain.handle("sessions:delete", (_, id: string) => sessions.deleteSession(id));
ipcMain.handle("sessions:get", (_, id: string) => sessions.getSession(id));
// Chat
ipcMain.handle("chat:send", async (_, sessionId: string, message: string) => {
const session = sessions.getSession(sessionId);
if (!session) throw new Error("Session not found");
sessions.addMessage(sessionId, "user", message);
await claude.sendMessage({
session,
message,
onMessage: (msg) => {
mainWindow.webContents.send("claude:message", sessionId, msg);
if (msg.type === "assistant") {
const content = msg.message.content
.filter((c: any) => c.type === "text")
.map((c: any) => c.text)
.join("\n");
if (content) {
sessions.addMessage(sessionId, "assistant", content);
}
}
},
});
});
ipcMain.handle("chat:interrupt", (_, sessionId: string) => {
claude.interruptSession(sessionId);
});
// Workflow
ipcMain.handle("workflow:review", async (_, sessionId: string) => {
const session = sessions.getSession(sessionId);
if (!session) throw new Error("Session not found");
await claude.triggerReview(session, (msg) => {
mainWindow.webContents.send("claude:message", sessionId, msg);
});
});
ipcMain.handle("workflow:advance", (_, sessionId: string) => {
const session = sessions.getSession(sessionId);
if (!session) throw new Error("Session not found");
return claude.advancePhase(session);
});
ipcMain.handle("workflow:setPermissionMode", (_, sessionId: string, mode: string) => {
sessions.updateSession(sessionId, { permission_mode: mode as sessions.PermissionMode });
});
// Artifacts
ipcMain.handle("artifact:read", (_, projectPath: string, filename: string) => {
return claude.readArtifact(projectPath, filename);
});
ipcMain.handle("artifact:write", (_, projectPath: string, filename: string, content: string) => {
claude.writeArtifact(projectPath, filename, content);
});
// Dialogs
ipcMain.handle("dialog:selectDirectory", async () => {
const result = await dialog.showOpenDialog(mainWindow, { properties: ["openDirectory"] });
return result.canceled ? null : result.filePaths[0];
});
}
```
### 3.3 Main Entry (`src/main/index.ts`)
```typescript
import { app, BrowserWindow } from "electron";
import path from "node:path";
import { getDb, closeDb } from "./db";
import { registerIpcHandlers } from "./ipc/handlers";
const isDev = !app.isPackaged;
let mainWindow: BrowserWindow | null = null;
function createWindow() {
mainWindow = new BrowserWindow({
width: 1400,
height: 900,
minWidth: 1000,
minHeight: 600,
show: false,
titleBarStyle: "hiddenInset",
webPreferences: {
contextIsolation: true,
nodeIntegration: false,
preload: path.join(__dirname, "preload.js"),
},
});
registerIpcHandlers(mainWindow);
if (isDev) {
const url = process.env.VITE_DEV_SERVER_URL ?? "http://localhost:5173";
mainWindow.loadURL(url).finally(() => {
mainWindow!.show();
mainWindow!.webContents.openDevTools({ mode: "detach" });
});
} else {
const indexHtml = path.join(app.getAppPath(), "renderer", "dist", "index.html");
mainWindow.loadFile(indexHtml).finally(() => mainWindow!.show());
}
}
app.whenReady().then(() => {
getDb();
createWindow();
app.on("activate", () => {
if (BrowserWindow.getAllWindows().length === 0) createWindow();
});
});
app.on("window-all-closed", () => {
closeDb();
if (process.platform !== "darwin") app.quit();
});
```
---
## Phase 4: React UI
### 4.1 Types (`renderer/src/types.ts`)
```typescript
export interface Project {
id: string;
name: string;
path: string;
created_at: number;
updated_at: number;
}
export interface Session {
id: string;
project_id: string;
name: string;
phase: Phase;
claude_session_id: string | null;
permission_mode: PermissionMode;
created_at: number;
updated_at: number;
}
export type Phase = "research" | "plan" | "implement";
export type PermissionMode = "acceptEdits" | "bypassPermissions";
export interface Message {
id: string;
session_id: string;
role: "user" | "assistant";
content: string;
created_at: number;
}
export interface TokenUsage {
inputTokens: number;
outputTokens: number;
cacheHits?: number;
}
```
### 4.2 App Component (`renderer/src/App.tsx`)
```typescript
import React, { useState, useEffect, useCallback } from "react";
import { Header } from "./components/Header";
import { DocumentPane } from "./components/DocumentPane";
import { ChatPane } from "./components/ChatPane";
import { ActionBar } from "./components/ActionBar";
import type { Project, Session, Message, TokenUsage } from "./types";
import "./styles/globals.css";
const api = window.api;
export function App() {
const [projects, setProjects] = useState([]);
const [sessions, setSessions] = useState([]);
const [selectedProject, setSelectedProject] = useState(null);
const [selectedSession, setSelectedSession] = useState(null);
const [messages, setMessages] = useState([]);
const [documentContent, setDocumentContent] = useState("");
const [originalContent, setOriginalContent] = useState("");
const [isLoading, setIsLoading] = useState(false);
const [tokenUsage, setTokenUsage] = useState({ inputTokens: 0, outputTokens: 0 });
const hasChanges = documentContent !== originalContent;
// Load projects on mount
useEffect(() => {
api.listProjects().then(setProjects);
}, []);
// Load sessions when project changes
useEffect(() => {
if (selectedProject) {
api.listSessions(selectedProject.id).then(setSessions);
} else {
setSessions([]);
}
}, [selectedProject]);
// Load artifact when session/phase changes
useEffect(() => {
if (selectedSession && selectedProject) {
const filename = selectedSession.phase === "research" ? "research.md" : "plan.md";
api.readArtifact(selectedProject.path, filename).then((content) => {
const text = content || "";
setDocumentContent(text);
setOriginalContent(text);
});
}
}, [selectedSession?.id, selectedSession?.phase, selectedProject]);
// Subscribe to Claude messages
useEffect(() => {
const unsubscribe = api.onClaudeMessage((sessionId, msg) => {
if (sessionId !== selectedSession?.id) return;
if (msg.type === "result") {
setIsLoading(false);
if (msg.usage) {
setTokenUsage({
inputTokens: msg.usage.input_tokens,
outputTokens: msg.usage.output_tokens,
cacheHits: msg.usage.cache_read_input_tokens,
});
}
// Reload artifact after Claude updates it
if (selectedProject) {
const filename = selectedSession.phase === "research" ? "research.md" : "plan.md";
api.readArtifact(selectedProject.path, filename).then((content) => {
const text = content || "";
setDocumentContent(text);
setOriginalContent(text);
});
}
}
if (msg.type === "assistant") {
const content = msg.message.content
.filter((c: any) => c.type === "text")
.map((c: any) => c.text)
.join("\n");
if (content) {
setMessages((prev) => {
const last = prev[prev.length - 1];
if (last?.role === "assistant") {
return [...prev.slice(0, -1), { ...last, content }];
}
return [...prev, { id: crypto.randomUUID(), session_id: sessionId, role: "assistant", content, created_at: Date.now() / 1000 }];
});
}
}
});
return unsubscribe;
}, [selectedSession?.id, selectedSession?.phase, selectedProject]);
const handleSendMessage = async (message: string) => {
if (!selectedSession) return;
setIsLoading(true);
setMessages((prev) => [...prev, { id: crypto.randomUUID(), session_id: selectedSession.id, role: "user", content: message, created_at: Date.now() / 1000 }]);
await api.sendMessage(selectedSession.id, message);
};
const handleReview = async () => {
if (!selectedSession || !selectedProject) return;
// Save user edits first
const filename = selectedSession.phase === "research" ? "research.md" : "plan.md";
await api.writeArtifact(selectedProject.path, filename, documentContent);
setOriginalContent(documentContent);
setIsLoading(true);
await api.triggerReview(selectedSession.id);
};
const handleSubmit = async () => {
if (!selectedSession || !selectedProject) return;
// Save any pending edits
const filename = selectedSession.phase === "research" ? "research.md" : "plan.md";
await api.writeArtifact(selectedProject.path, filename, documentContent);
const nextPhase = await api.advancePhase(selectedSession.id);
if (nextPhase) {
setSelectedSession({ ...selectedSession, phase: nextPhase });
// Trigger initial message for next phase
setIsLoading(true);
const initialMsg = nextPhase === "plan"
? "Create a detailed implementation plan based on the research."
: "Begin implementing the plan.";
await api.sendMessage(selectedSession.id, initialMsg);
}
};
const handleCreateProject = async () => {
const path = await api.selectDirectory();
if (!path) return;
const name = path.split("/").pop() || "New Project";
const project = await api.createProject(name, path);
setProjects((prev) => [project, ...prev]);
setSelectedProject(project);
};
const handleCreateSession = async () => {
if (!selectedProject) return;
const name = `Session ${sessions.length + 1}`;
const session = await api.createSession(selectedProject.id, name);
setSessions((prev) => [session, ...prev]);
setSelectedSession(session);
setMessages([]);
setDocumentContent("");
setOriginalContent("");
};
return (
{
if (selectedSession) {
api.setPermissionMode(selectedSession.id, mode);
setSelectedSession({ ...selectedSession, permission_mode: mode });
}
}}
disabled={!selectedSession}
/>
);
}
```
### 4.3 Header (`renderer/src/components/Header.tsx`)
```typescript
import React from "react";
import type { Project, Session, Phase } from "../types";
interface HeaderProps {
projects: Project[];
sessions: Session[];
selectedProject: Project | null;
selectedSession: Session | null;
onSelectProject: (project: Project | null) => void;
onSelectSession: (session: Session | null) => void;
onCreateProject: () => void;
onCreateSession: () => void;
}
const phaseLabels: Record = {
research: "Research",
plan: "Plan",
implement: "Implement",
};
export function Header({
projects, sessions, selectedProject, selectedSession,
onSelectProject, onSelectSession, onCreateProject, onCreateSession,
}: HeaderProps) {
return (
{selectedProject && (
<>
>
)}
{selectedSession && (
{(["research", "plan", "implement"] as Phase[]).map((phase) => (
{phaseLabels[phase]}
))}
)}
);
}
```
### 4.4 DocumentPane (`renderer/src/components/DocumentPane.tsx`)
```typescript
import React, { useMemo } from "react";
import type { Phase } from "../types";
interface DocumentPaneProps {
content: string;
onChange: (content: string) => void;
phase: Phase;
disabled: boolean;
}
// Simple markdown renderer (can be replaced with a library like react-markdown)
function renderMarkdown(md: string): string {
return md
// Headers
.replace(/^### (.*$)/gm, '$1
')
.replace(/^## (.*$)/gm, '$1
')
.replace(/^# (.*$)/gm, '$1
')
// Bold/italic
.replace(/\*\*([^*]+)\*\*/g, '$1')
.replace(/\*([^*]+)\*/g, '$1')
// Code blocks
.replace(/```(\w*)\n([\s\S]*?)```/g, '$2
')
.replace(/`([^`]+)`/g, '$1')
// Lists
.replace(/^\- \[x\] (.*$)/gm, '☑ $1')
.replace(/^\- \[ \] (.*$)/gm, '☐ $1')
.replace(/^\- (.*$)/gm, '$1')
// Review comments (highlight them)
.replace(/(\/\/ REVIEW:.*$)/gm, '$1')
.replace(/(\/\/ NOTE:.*$)/gm, '$1')
// Paragraphs
.replace(/\n\n/g, '
')
.replace(/^(.+)$/gm, '
$1
')
// Clean up
.replace(/<\/p>/g, '')
.replace(/
()/g, '$1')
.replace(/(<\/h[1-3]>)<\/p>/g, '$1');
}
export function DocumentPane({ content, onChange, phase, disabled }: DocumentPaneProps) {
const [isEditing, setIsEditing] = React.useState(false);
const renderedHtml = useMemo(() => renderMarkdown(content), [content]);
if (phase === "implement") {
// In implement phase, show read-only rendered view
return (
);
}
const filename = phase === "research" ? "research.md" : "plan.md";
return (
{filename}
{isEditing ? (