From 9a636af9090b122db2e55737fca3e78550aab9df Mon Sep 17 00:00:00 2001
From: bndw
Date: Sat, 28 Feb 2026 19:14:01 -0800
Subject: fix: scope artifacts to sessions
---
plan.md | 1684 ---------------------------------------------------------------
1 file changed, 1684 deletions(-)
delete mode 100644 plan.md
(limited to 'plan.md')
diff --git a/plan.md b/plan.md
deleted file mode 100644
index 9f74c1c..0000000
--- a/plan.md
+++ /dev/null
@@ -1,1684 +0,0 @@
-# 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 (
-
-
- plan.md
- Implementing...
-
-
-
- );
- }
-
- const filename = phase === "research" ? "research.md" : "plan.md";
-
- return (
-
-
- {filename}
-
-
-
- {isEditing ? (
-