From 6a30a11f4a136cef3a0f93aa1c878c30112221de Mon Sep 17 00:00:00 2001 From: Clawd Date: Fri, 27 Feb 2026 22:06:46 -0800 Subject: Rewrite plan with document-centric UX: Review/Submit workflow --- plan.md | 1666 ++++++++++++++++++++++++++++++++++----------------------------- 1 file changed, 896 insertions(+), 770 deletions(-) (limited to 'plan.md') diff --git a/plan.md b/plan.md index cf6ea0b..1a6578d 100644 --- a/plan.md +++ b/plan.md @@ -2,11 +2,85 @@ ## Overview -Build an Electron app that wraps the Claude Agent SDK with an opinionated workflow: Research → Plan → Annotate → Implement. +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 (Target) +## Directory Structure ``` claude-flow/ @@ -23,32 +97,22 @@ claude-flow/ │ │ ├── phases.ts # Phase configs (prompts, tools, permissions) │ │ └── hooks.ts # Custom hooks for phase enforcement │ └── ipc/ -│ ├── index.ts # Register all handlers -│ ├── projects.ts # Project IPC handlers -│ ├── sessions.ts # Session IPC handlers -│ └── claude.ts # Claude IPC handlers +│ └── handlers.ts # All IPC handlers ├── renderer/ │ ├── index.html │ └── src/ -│ ├── main.tsx # React entry -│ ├── App.tsx # Main app component +│ ├── main.tsx +│ ├── App.tsx │ ├── components/ -│ │ ├── Sidebar.tsx # Project/session tree -│ │ ├── SessionList.tsx # Sessions for a project -│ │ ├── Chat.tsx # Message thread -│ │ ├── ChatInput.tsx # Input box -│ │ ├── Message.tsx # Single message bubble -│ │ ├── ArtifactPane.tsx # Markdown editor for plan/research -│ │ ├── PhaseBar.tsx # Phase indicator + controls -│ │ ├── ContextIndicator.tsx # Token usage / context size display -│ │ └── Settings.tsx # API key, preferences -│ ├── hooks/ -│ │ ├── useProjects.ts -│ │ ├── useSessions.ts -│ │ └── useChat.ts +│ │ ├── 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 -│ ├── types.ts # Shared types +│ │ ├── api.ts # Typed IPC wrapper +│ │ └── markdown.ts # Markdown rendering utilities +│ ├── types.ts │ └── styles/ │ └── globals.css ├── package.json @@ -91,7 +155,6 @@ export function initSchema(db: Database.Database) { session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE, role TEXT NOT NULL, content TEXT NOT NULL, - tool_use TEXT, created_at INTEGER NOT NULL DEFAULT (unixepoch()) ); @@ -102,10 +165,9 @@ export function initSchema(db: Database.Database) { ``` **Notes:** +- `phase` is one of: `research`, `plan`, `implement` - `claude_session_id` stores the SDK's session ID for resuming -- `permission_mode` is user toggle: `acceptEdits` or `bypassPermissions` -- `phase` is one of: `research`, `plan`, `annotate`, `implement` -- Messages stored for display; actual context in SDK session +- `permission_mode`: `acceptEdits` or `bypassPermissions` (user toggle in implement phase) ### 1.2 Database Connection (`src/main/db/index.ts`) @@ -158,13 +220,15 @@ export interface Project { } export function listProjects(): Project[] { - const db = getDb(); - return db.prepare("SELECT * FROM projects ORDER BY updated_at DESC").all() as Project[]; + return getDb() + .prepare("SELECT * FROM projects ORDER BY updated_at DESC") + .all() as Project[]; } export function getProject(id: string): Project | undefined { - const db = getDb(); - return db.prepare("SELECT * FROM projects WHERE id = ?").get(id) as Project | undefined; + return getDb() + .prepare("SELECT * FROM projects WHERE id = ?") + .get(id) as Project | undefined; } export function createProject(name: string, projectPath: string): Project { @@ -179,32 +243,8 @@ export function createProject(name: string, projectPath: string): Project { return { id, name, path: projectPath, created_at: now, updated_at: now }; } -export function updateProject(id: string, updates: Partial>): void { - const db = getDb(); - const sets: string[] = []; - const values: any[] = []; - - if (updates.name !== undefined) { - sets.push("name = ?"); - values.push(updates.name); - } - if (updates.path !== undefined) { - sets.push("path = ?"); - values.push(updates.path); - } - - if (sets.length > 0) { - sets.push("updated_at = ?"); - values.push(Math.floor(Date.now() / 1000)); - values.push(id); - - db.prepare(`UPDATE projects SET ${sets.join(", ")} WHERE id = ?`).run(...values); - } -} - export function deleteProject(id: string): void { - const db = getDb(); - db.prepare("DELETE FROM projects WHERE id = ?").run(id); + getDb().prepare("DELETE FROM projects WHERE id = ?").run(id); } ``` @@ -214,7 +254,7 @@ export function deleteProject(id: string): void { import { getDb } from "./index"; import { v4 as uuid } from "uuid"; -export type Phase = "research" | "plan" | "annotate" | "implement"; +export type Phase = "research" | "plan" | "implement"; export type PermissionMode = "acceptEdits" | "bypassPermissions"; export interface Session { @@ -231,22 +271,21 @@ export interface Session { export interface Message { id: string; session_id: string; - role: "user" | "assistant" | "system"; + role: "user" | "assistant"; content: string; - tool_use: string | null; created_at: number; } export function listSessions(projectId: string): Session[] { - const db = getDb(); - return db + 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 { - const db = getDb(); - return db.prepare("SELECT * FROM sessions WHERE id = ?").get(id) as Session | undefined; + return getDb() + .prepare("SELECT * FROM sessions WHERE id = ?") + .get(id) as Session | undefined; } export function createSession(projectId: string, name: string): Session { @@ -290,42 +329,33 @@ export function updateSession( 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 { - const db = getDb(); - db.prepare("DELETE FROM sessions WHERE id = ?").run(id); + getDb().prepare("DELETE FROM sessions WHERE id = ?").run(id); } // Messages export function listMessages(sessionId: string): Message[] { - const db = getDb(); - return db + 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, - toolUse?: string -): 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, tool_use, created_at) VALUES (?, ?, ?, ?, ?, ?)" - ).run(id, sessionId, role, content, toolUse ?? null, now); + "INSERT INTO messages (id, session_id, role, content, created_at) VALUES (?, ?, ?, ?, ?)" + ).run(id, sessionId, role, content, now); - // Touch session updated_at db.prepare("UPDATE sessions SET updated_at = ? WHERE id = ?").run(now, sessionId); - return { id, session_id: sessionId, role, content, tool_use: toolUse ?? null, created_at: now }; + return { id, session_id: sessionId, role, content, created_at: now }; } ``` @@ -342,73 +372,69 @@ 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"], + 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 relevant parts of the codebase before any changes are made. +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: -- Read files thoroughly — "deeply", "in great detail", understand all intricacies -- Write all findings to .claude-flow/research.md - 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 finished, summarize what you learned and wait for the next instruction.`, +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. -Your job is to write a detailed implementation plan based on the research. - -Rules: -- Write the plan to .claude-flow/plan.md -- Include specific code snippets showing proposed changes -- Include file paths that will be modified -- Include trade-offs and considerations -- Add a granular TODO list at the end with phases and tasks -- DO NOT implement anything -- DO NOT modify any source files - -The plan should be detailed enough that implementation becomes mechanical.`, - }, - - annotate: { - permissionMode: "plan", - allowedTools: ["Read", "Write"], - systemPrompt: `You are in ANNOTATION mode. +Based on the research in .claude-flow/research.md, write a detailed implementation plan. -The human has added notes/comments to .claude-flow/plan.md. +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: -- Read .claude-flow/plan.md carefully -- Find all inline notes (marked with comments, "// NOTE:", "REVIEW:", etc.) -- Address each note by updating the relevant section - DO NOT implement anything - DO NOT modify any source files -- Only update .claude-flow/plan.md +- Only write to .claude-flow/plan.md -When finished, summarize the changes you made to the plan.`, +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. -Rules: -- Follow .claude-flow/plan.md exactly -- Mark tasks complete in the plan as you finish them +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 or jsdocs -- Do not use any or unknown types +- 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.`, @@ -417,61 +443,32 @@ If you encounter issues not covered by the plan, stop and ask.`, export function getPhaseConfig(phase: Phase, userPermissionMode?: PermissionMode): PhaseConfig { const config = { ...phaseConfigs[phase] }; - - // In implement phase, use user's permission mode preference if (phase === "implement" && userPermissionMode) { config.permissionMode = userPermissionMode; } - return config; } ``` -### 2.2 Hooks (`src/main/claude/hooks.ts`) - -```typescript -import { HookCallback, PreToolUseHookInput } from "@anthropic-ai/claude-agent-sdk"; -import { Phase } from "../db/sessions"; - -export function createPhaseEnforcementHook(getPhase: () => Phase): HookCallback { - return async (input, toolUseID, { signal }) => { - if (input.hook_event_name !== "PreToolUse") return {}; - - const preInput = input as PreToolUseHookInput; - const phase = getPhase(); - const toolInput = preInput.tool_input as Record; - const filePath = toolInput?.file_path as string | undefined; - - // In non-implement phases, only allow writes to .claude-flow/ artifacts - if (phase !== "implement" && ["Write", "Edit"].includes(preInput.tool_name)) { - if (filePath && !filePath.includes(".claude-flow/")) { - return { - hookSpecificOutput: { - hookEventName: "PreToolUse", - permissionDecision: "deny", - permissionDecisionReason: `Cannot modify ${filePath} in ${phase} phase. Only .claude-flow/ artifacts allowed.`, - }, - }; - } - } - - return {}; - }; -} -``` - -### 2.3 Claude Wrapper (`src/main/claude/index.ts`) +### 2.2 Claude Wrapper (`src/main/claude/index.ts`) ```typescript import { query, SDKMessage } from "@anthropic-ai/claude-agent-sdk"; -import { Session, Phase, updateSession } from "../db/sessions"; +import { Session, Phase, updateSession, getSession } from "../db/sessions"; import { getPhaseConfig } from "./phases"; -import { createPhaseEnforcementHook } from "./hooks"; import { getProject } from "../db/projects"; +import fs from "node:fs"; +import path from "node:path"; -// Active queries by session ID 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; @@ -482,13 +479,9 @@ export async function sendMessage({ session, message, onMessage }: SendMessageOp const project = getProject(session.project_id); if (!project) throw new Error("Project not found"); - const phaseConfig = getPhaseConfig(session.phase, session.permission_mode); + ensureArtifactDir(project.path); - // Create phase getter that reads current session state - const getPhase = (): Phase => { - // In a real app, read from DB; here we use closure - return session.phase; - }; + const phaseConfig = getPhaseConfig(session.phase, session.permission_mode); const q = query({ prompt: message, @@ -498,11 +491,6 @@ export async function sendMessage({ session, message, onMessage }: SendMessageOp systemPrompt: phaseConfig.systemPrompt, allowedTools: phaseConfig.allowedTools, permissionMode: phaseConfig.permissionMode, - hooks: { - PreToolUse: [ - { hooks: [createPhaseEnforcementHook(getPhase)] }, - ], - }, }, }); @@ -510,14 +498,11 @@ export async function sendMessage({ session, message, onMessage }: SendMessageOp try { for await (const msg of q) { - // Capture session ID from init message if (msg.type === "system" && msg.subtype === "init") { if (!session.claude_session_id) { updateSession(session.id, { claude_session_id: msg.session_id }); - session.claude_session_id = msg.session_id; } } - onMessage(msg); } } finally { @@ -533,14 +518,45 @@ export function interruptSession(sessionId: string): void { } } -export async function setSessionPhase(session: Session, phase: Phase): Promise { - updateSession(session.id, { phase }); +// 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; +} - // If there's an active query, update its permission mode - const q = activeQueries.get(session.id); - if (q && phase === "implement") { - await q.setPermissionMode(session.permission_mode); +// 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"); } ``` @@ -553,32 +569,6 @@ export async function setSessionPhase(session: Session, phase: Phase): Promise Promise; - createProject: (name: string, path: string) => Promise; - deleteProject: (id: string) => Promise; - - // Sessions - listSessions: (projectId: string) => Promise; - getSession: (id: string) => Promise; - createSession: (projectId: string, name: string) => Promise; - deleteSession: (id: string) => Promise; - setPhase: (sessionId: string, phase: string) => Promise; - setPermissionMode: (sessionId: string, mode: string) => Promise; - - // Messages - listMessages: (sessionId: string) => Promise; - sendMessage: (sessionId: string, message: string) => Promise; - interruptSession: (sessionId: string) => Promise; - - // Events - onClaudeMessage: (callback: (sessionId: string, message: any) => void) => () => void; - - // Dialogs - selectDirectory: () => Promise; -} - contextBridge.exposeInMainWorld("api", { // Projects listProjects: () => ipcRenderer.invoke("projects:list"), @@ -587,36 +577,37 @@ contextBridge.exposeInMainWorld("api", { // Sessions listSessions: (projectId: string) => ipcRenderer.invoke("sessions:list", projectId), - getSession: (id: string) => ipcRenderer.invoke("sessions:get", id), - createSession: (projectId: string, name: string) => - ipcRenderer.invoke("sessions:create", projectId, name), + createSession: (projectId: string, name: string) => ipcRenderer.invoke("sessions:create", projectId, name), deleteSession: (id: string) => ipcRenderer.invoke("sessions:delete", id), - setPhase: (sessionId: string, phase: string) => - ipcRenderer.invoke("sessions:setPhase", sessionId, phase), - setPermissionMode: (sessionId: string, mode: string) => - ipcRenderer.invoke("sessions:setPermissionMode", sessionId, mode), + 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), - // Messages - listMessages: (sessionId: string) => ipcRenderer.invoke("messages:list", sessionId), - sendMessage: (sessionId: string, message: string) => - ipcRenderer.invoke("claude:send", sessionId, message), - interruptSession: (sessionId: string) => ipcRenderer.invoke("claude:interrupt", sessionId), + // 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 = (_event: IpcRendererEvent, sessionId: string, message: any) => { - callback(sessionId, message); - }; + 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"), -} satisfies ClaudeFlowAPI); +}); ``` -### 3.2 IPC Handlers (`src/main/ipc/index.ts`) +### 3.2 IPC Handlers (`src/main/ipc/handlers.ts`) ```typescript import { ipcMain, dialog, BrowserWindow } from "electron"; @@ -627,46 +618,28 @@ 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: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:get", (_, id: string) => sessions.getSession(id)); - ipcMain.handle("sessions:create", (_, projectId: string, name: string) => - sessions.createSession(projectId, name) - ); + ipcMain.handle("sessions:create", (_, projectId: string, name: string) => sessions.createSession(projectId, name)); ipcMain.handle("sessions:delete", (_, id: string) => sessions.deleteSession(id)); - ipcMain.handle("sessions:setPhase", async (_, sessionId: string, phase: string) => { - const session = sessions.getSession(sessionId); - if (session) { - await claude.setSessionPhase(session, phase as sessions.Phase); - } - }); - ipcMain.handle("sessions:setPermissionMode", (_, sessionId: string, mode: string) => { - sessions.updateSession(sessionId, { permission_mode: mode as sessions.PermissionMode }); - }); + ipcMain.handle("sessions:get", (_, id: string) => sessions.getSession(id)); - // Messages - ipcMain.handle("messages:list", (_, sessionId: string) => sessions.listMessages(sessionId)); - ipcMain.handle("claude:send", async (_, sessionId: string, message: string) => { + // Chat + ipcMain.handle("chat:send", async (_, sessionId: string, message: string) => { const session = sessions.getSession(sessionId); if (!session) throw new Error("Session not found"); - // Store user message sessions.addMessage(sessionId, "user", message); - // Send to Claude await claude.sendMessage({ session, message, onMessage: (msg) => { - // Forward messages to renderer mainWindow.webContents.send("claude:message", sessionId, msg); - // Store assistant messages if (msg.type === "assistant") { const content = msg.message.content .filter((c: any) => c.type === "text") @@ -679,39 +652,67 @@ export function registerIpcHandlers(mainWindow: BrowserWindow) { }, }); }); - ipcMain.handle("claude:interrupt", (_, sessionId: string) => { + + 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"], - }); + const result = await dialog.showOpenDialog(mainWindow, { properties: ["openDirectory"] }); return result.canceled ? null : result.filePaths[0]; }); } ``` -### 3.3 Update Main Entry (`src/main/index.ts`) +### 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"; +import { registerIpcHandlers } from "./ipc/handlers"; const isDev = !app.isPackaged; - let mainWindow: BrowserWindow | null = null; function createWindow() { mainWindow = new BrowserWindow({ width: 1400, height: 900, - minWidth: 800, + minWidth: 1000, minHeight: 600, show: false, + titleBarStyle: "hiddenInset", webPreferences: { contextIsolation: true, nodeIntegration: false, @@ -719,7 +720,6 @@ function createWindow() { }, }); - // Register IPC handlers registerIpcHandlers(mainWindow); if (isDev) { @@ -735,23 +735,16 @@ function createWindow() { } app.whenReady().then(() => { - // Initialize database getDb(); - createWindow(); - app.on("activate", () => { - if (BrowserWindow.getAllWindows().length === 0) { - createWindow(); - } + if (BrowserWindow.getAllWindows().length === 0) createWindow(); }); }); app.on("window-all-closed", () => { closeDb(); - if (process.platform !== "darwin") { - app.quit(); - } + if (process.platform !== "darwin") app.quit(); }); ``` @@ -781,455 +774,546 @@ export interface Session { updated_at: number; } -export type Phase = "research" | "plan" | "annotate" | "implement"; +export type Phase = "research" | "plan" | "implement"; export type PermissionMode = "acceptEdits" | "bypassPermissions"; export interface Message { id: string; session_id: string; - role: "user" | "assistant" | "system"; + role: "user" | "assistant"; content: string; - tool_use: string | null; created_at: number; } -``` - -### 4.2 API Wrapper (`renderer/src/lib/api.ts`) -```typescript -import type { Project, Session, Message, Phase, PermissionMode } from "../types"; - -declare global { - interface Window { - api: { - listProjects: () => Promise; - createProject: (name: string, path: string) => Promise; - deleteProject: (id: string) => Promise; - - listSessions: (projectId: string) => Promise; - getSession: (id: string) => Promise; - createSession: (projectId: string, name: string) => Promise; - deleteSession: (id: string) => Promise; - setPhase: (sessionId: string, phase: Phase) => Promise; - setPermissionMode: (sessionId: string, mode: PermissionMode) => Promise; - - listMessages: (sessionId: string) => Promise; - sendMessage: (sessionId: string, message: string) => Promise; - interruptSession: (sessionId: string) => Promise; - - onClaudeMessage: (callback: (sessionId: string, message: any) => void) => () => void; - - selectDirectory: () => Promise; - }; - } +export interface TokenUsage { + inputTokens: number; + outputTokens: number; + cacheHits?: number; } - -export const api = window.api; ``` -### 4.3 App Component (`renderer/src/App.tsx`) +### 4.2 App Component (`renderer/src/App.tsx`) ```typescript -import React, { useState, useEffect } from "react"; -import { api } from "./lib/api"; -import type { Project, Session } from "./types"; -import { Sidebar } from "./components/Sidebar"; -import { Chat } from "./components/Chat"; -import { PhaseBar } from "./components/PhaseBar"; +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 handleSelectSession = async (session: Session) => { + 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} /> -
- {selectedSession ? ( - <> - { - api.setPhase(selectedSession.id, phase); - setSelectedSession({ ...selectedSession, phase }); - }} - onPermissionModeChange={(mode) => { - api.setPermissionMode(selectedSession.id, mode); - setSelectedSession({ ...selectedSession, permission_mode: mode }); - }} - /> - - - ) : ( -
- {selectedProject - ? "Select or create a session to start" - : "Select or create a project to start"} -
- )} -
); } ``` -### 4.4 Sidebar (`renderer/src/components/Sidebar.tsx`) +### 4.3 Header (`renderer/src/components/Header.tsx`) ```typescript -import React, { useState, useEffect } from "react"; -import { api } from "../lib/api"; -import type { Project, Session } from "../types"; +import React from "react"; +import type { Project, Session, Phase } from "../types"; -interface SidebarProps { +interface HeaderProps { projects: Project[]; + sessions: Session[]; selectedProject: Project | null; selectedSession: Session | null; - onSelectProject: (project: Project) => void; - onSelectSession: (session: Session) => void; + onSelectProject: (project: Project | null) => void; + onSelectSession: (session: Session | null) => void; onCreateProject: () => void; + onCreateSession: () => void; } -export function Sidebar({ - projects, - selectedProject, - selectedSession, - onSelectProject, - onSelectSession, - onCreateProject, -}: SidebarProps) { - const [sessions, setSessions] = useState([]); - - useEffect(() => { - if (selectedProject) { - api.listSessions(selectedProject.id).then(setSessions); - } else { - setSessions([]); - } - }, [selectedProject]); - - const handleCreateSession = async () => { - if (!selectedProject) return; - const name = `Session ${sessions.length + 1}`; - const session = await api.createSession(selectedProject.id, name); - setSessions((prev) => [session, ...prev]); - onSelectSession(session); - }; +const phaseLabels: Record = { + research: "Research", + plan: "Plan", + implement: "Implement", +}; +export function Header({ + projects, sessions, selectedProject, selectedSession, + onSelectProject, onSelectSession, onCreateProject, onCreateSession, +}: HeaderProps) { return ( - + + )} + + ); } ``` -### 4.5 PhaseBar (`renderer/src/components/PhaseBar.tsx`) +### 4.4 DocumentPane (`renderer/src/components/DocumentPane.tsx`) ```typescript -import React from "react"; -import type { Session, Phase, PermissionMode } from "../types"; +import React, { useMemo } from "react"; +import type { Phase } from "../types"; -interface PhaseBarProps { - session: Session; - onPhaseChange: (phase: Phase) => void; - onPermissionModeChange: (mode: PermissionMode) => void; -} +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 ( +

    +
    + plan.md + Implementing... +
    +
    +
    + ); + } -const phases: Phase[] = ["research", "plan", "annotate", "implement"]; + const filename = phase === "research" ? "research.md" : "plan.md"; -export function PhaseBar({ session, onPhaseChange, onPermissionModeChange }: PhaseBarProps) { return ( -
    -
    - {phases.map((phase) => ( - - ))} +
    +
    + {filename} +
    - {session.phase === "implement" && ( -
    - -
    - )} -
    - ); -} -``` - -### 4.6 ContextIndicator (`renderer/src/components/ContextIndicator.tsx`) - -```typescript -import React from "react"; - -interface ContextIndicatorProps { - inputTokens: number; - outputTokens: number; - cacheHits?: number; -} - -export function ContextIndicator({ inputTokens, outputTokens, cacheHits }: ContextIndicatorProps) { - const totalTokens = inputTokens + outputTokens; - const maxTokens = 200000; // Claude's context window - const usagePercent = Math.min((totalTokens / maxTokens) * 100, 100); - - const getColor = () => { - if (usagePercent > 80) return "#ef4444"; // red - if (usagePercent > 50) return "#f59e0b"; // amber - return "#10b981"; // green - }; - - return ( -
    -
    -
    onChange(e.target.value)} + disabled={disabled} + placeholder={`${filename} will appear here...`} /> -
    - - {(totalTokens / 1000).toFixed(1)}k / 200k tokens - {cacheHits ? ` (${(cacheHits / 1000).toFixed(1)}k cached)` : ""} - + ) : ( +
    Document will appear here after Claude generates it...

    ' }} + onClick={() => !disabled && setIsEditing(true)} + /> + )}
    ); } ``` -Add styles to `globals.css`: - -```css -/* Context Indicator */ -.context-indicator { - display: flex; - align-items: center; - gap: 8px; - padding: 0 16px; -} - -.context-bar { - width: 100px; - height: 6px; - background: #333; - border-radius: 3px; - overflow: hidden; -} - -.context-fill { - height: 100%; - transition: width 0.3s ease; -} - -.context-label { - font-size: 12px; - color: #888; -} -``` - -### 4.7 Chat (`renderer/src/components/Chat.tsx`) +### 4.5 ChatPane (`renderer/src/components/ChatPane.tsx`) ```typescript -import React, { useState, useEffect, useRef } from "react"; -import { api } from "../lib/api"; -import type { Session, Message } from "../types"; +import React, { useState, useRef, useEffect } from "react"; +import type { Message } from "../types"; -interface ChatProps { - session: Session; +interface ChatPaneProps { + messages: Message[]; + onSend: (message: string) => void; + isLoading: boolean; + disabled: boolean; + placeholder: string; } -export function Chat({ session }: ChatProps) { - const [messages, setMessages] = useState([]); +export function ChatPane({ messages, onSend, isLoading, disabled, placeholder }: ChatPaneProps) { const [input, setInput] = useState(""); - const [isLoading, setIsLoading] = useState(false); const messagesEndRef = useRef(null); - // Load messages - useEffect(() => { - api.listMessages(session.id).then(setMessages); - }, [session.id]); - - // Subscribe to Claude messages - useEffect(() => { - const unsubscribe = api.onClaudeMessage((sessionId, msg) => { - if (sessionId !== session.id) return; - - 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) => { - // Update last assistant message or add new one - const lastMsg = prev[prev.length - 1]; - if (lastMsg?.role === "assistant") { - return [...prev.slice(0, -1), { ...lastMsg, content }]; - } - return [ - ...prev, - { - id: crypto.randomUUID(), - session_id: session.id, - role: "assistant", - content, - tool_use: null, - created_at: Date.now() / 1000, - }, - ]; - }); - } - } - - if (msg.type === "result") { - setIsLoading(false); - } - }); - - return unsubscribe; - }, [session.id]); - - // Auto-scroll useEffect(() => { messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); }, [messages]); - const handleSend = async () => { - if (!input.trim() || isLoading) return; - - const userMessage: Message = { - id: crypto.randomUUID(), - session_id: session.id, - role: "user", - content: input, - tool_use: null, - created_at: Date.now() / 1000, - }; - - setMessages((prev) => [...prev, userMessage]); + const handleSend = () => { + if (!input.trim() || isLoading || disabled) return; + onSend(input.trim()); setInput(""); - setIsLoading(true); - - try { - await api.sendMessage(session.id, input); - } catch (error) { - console.error("Failed to send message:", error); - setIsLoading(false); - } }; return ( -
    -
    +
    +
    {messages.map((msg) => ( -
    +
    {msg.content}
    ))} - {isLoading &&
    Claude is thinking...
    } + {isLoading && ( +
    +
    Thinking...
    +
    + )}
    -