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(-)
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 (
-
-
-
- {projects.map((project) => (
- -
-
')
+ .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) => (
-
+
))}
- {isLoading &&
Claude is thinking...
}
+ {isLoading && (
+
+ )}
-
+
+ );
+}
+```
+
+### 4.6 ActionBar (`renderer/src/components/ActionBar.tsx`)
+
+```typescript
+import React from "react";
+import type { Phase, PermissionMode, TokenUsage } from "../types";
+
+interface ActionBarProps {
+ phase: Phase;
+ hasChanges: boolean;
+ isLoading: boolean;
+ tokenUsage: TokenUsage;
+ permissionMode: PermissionMode;
+ onReview: () => void;
+ onSubmit: () => void;
+ onPermissionModeChange: (mode: PermissionMode) => void;
+ disabled: boolean;
+}
+
+export function ActionBar({
+ phase, hasChanges, isLoading, tokenUsage, permissionMode,
+ onReview, onSubmit, onPermissionModeChange, disabled,
+}: ActionBarProps) {
+ const totalTokens = tokenUsage.inputTokens + tokenUsage.outputTokens;
+ const maxTokens = 200000;
+ const usagePercent = Math.min((totalTokens / maxTokens) * 100, 100);
+
+ const getBarColor = () => {
+ if (usagePercent > 80) return "#ef4444";
+ if (usagePercent > 50) return "#f59e0b";
+ return "#10b981";
+ };
+
+ return (
+
+
+
+
+
+ {(totalTokens / 1000).toFixed(1)}k / 200k
+
+
+
+ {phase === "implement" && (
+
+ )}
+
+
+
+ {phase !== "implement" && (
+ <>
+
+
+ >
+ )}
+ {phase === "implement" && isLoading && (
+ Implementing...
)}
@@ -1237,7 +1321,7 @@ export function Chat({ session }: ChatProps) {
}
```
-### 4.7 Basic Styles (`renderer/src/styles/globals.css`)
+### 4.7 Styles (`renderer/src/styles/globals.css`)
```css
* {
@@ -1246,209 +1330,224 @@ export function Chat({ session }: ChatProps) {
padding: 0;
}
+:root {
+ --bg-primary: #1a1a1a;
+ --bg-secondary: #252525;
+ --bg-tertiary: #333;
+ --border: #444;
+ --text-primary: #e0e0e0;
+ --text-secondary: #888;
+ --accent: #3b82f6;
+ --accent-hover: #2563eb;
+ --success: #10b981;
+ --warning: #f59e0b;
+ --danger: #ef4444;
+}
+
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
- background: #1a1a1a;
- color: #e0e0e0;
+ background: var(--bg-primary);
+ color: var(--text-primary);
+ overflow: hidden;
}
.app {
- display: flex;
- height: 100vh;
-}
-
-/* Sidebar */
-.sidebar {
- width: 260px;
- background: #252525;
- border-right: 1px solid #333;
display: flex;
flex-direction: column;
- overflow-y: auto;
+ height: 100vh;
}
-.sidebar-header {
+/* Header */
+.header {
display: flex;
justify-content: space-between;
align-items: center;
- padding: 16px;
- border-bottom: 1px solid #333;
+ padding: 12px 16px;
+ background: var(--bg-secondary);
+ border-bottom: 1px solid var(--border);
+ -webkit-app-region: drag;
}
-.sidebar-header h2,
-.sidebar-header h3 {
- font-size: 14px;
- text-transform: uppercase;
- letter-spacing: 0.5px;
- color: #888;
+.header-left, .header-right {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ -webkit-app-region: no-drag;
}
-.sidebar-header button {
- background: #3b82f6;
- border: none;
- color: white;
- width: 24px;
- height: 24px;
+.header select, .header button {
+ padding: 6px 12px;
+ background: var(--bg-tertiary);
+ border: 1px solid var(--border);
border-radius: 4px;
+ color: var(--text-primary);
cursor: pointer;
}
-.project-list,
-.session-list {
- list-style: none;
+.header button:hover {
+ background: var(--border);
}
-.project-list button,
-.session-list button {
- width: 100%;
- padding: 12px 16px;
- background: none;
- border: none;
- color: #e0e0e0;
- text-align: left;
- cursor: pointer;
+.phase-indicator {
display: flex;
- justify-content: space-between;
- align-items: center;
+ gap: 4px;
}
-.project-list button:hover,
-.session-list button:hover {
- background: #333;
+.phase-step {
+ padding: 4px 12px;
+ font-size: 12px;
+ border-radius: 4px;
+ background: var(--bg-tertiary);
+ color: var(--text-secondary);
}
-.project-list button.selected,
-.session-list button.selected {
- background: #3b82f6;
+.phase-step.active {
+ background: var(--accent);
+ color: white;
}
-.phase-badge {
- font-size: 10px;
- padding: 2px 6px;
- border-radius: 4px;
- text-transform: uppercase;
+.phase-step.complete {
+ background: var(--success);
+ color: white;
}
-.phase-research { background: #7c3aed; }
-.phase-plan { background: #f59e0b; }
-.phase-annotate { background: #10b981; }
-.phase-implement { background: #3b82f6; }
-
-/* Main */
-.main {
+/* Main Content */
+.main-content {
flex: 1;
display: flex;
- flex-direction: column;
overflow: hidden;
}
-.empty-state {
+/* Document Pane */
+.document-pane {
flex: 1;
display: flex;
- align-items: center;
- justify-content: center;
- color: #666;
+ flex-direction: column;
+ border-right: 1px solid var(--border);
}
-/* Phase Bar */
-.phase-bar {
+.document-header {
display: flex;
justify-content: space-between;
align-items: center;
- padding: 12px 16px;
- background: #252525;
- border-bottom: 1px solid #333;
-}
-
-.phase-buttons {
- display: flex;
- gap: 8px;
+ padding: 8px 16px;
+ background: var(--bg-secondary);
+ border-bottom: 1px solid var(--border);
+ font-size: 14px;
+ color: var(--text-secondary);
}
-.phase-buttons button {
- padding: 8px 16px;
- background: #333;
- border: none;
- color: #888;
+.document-header button {
+ padding: 4px 8px;
+ background: var(--bg-tertiary);
+ border: 1px solid var(--border);
border-radius: 4px;
+ color: var(--text-primary);
cursor: pointer;
+ font-size: 12px;
}
-.phase-buttons button.active {
- background: #3b82f6;
- color: white;
+.document-content {
+ flex: 1;
+ overflow-y: auto;
+ padding: 24px;
}
-.permission-toggle {
- display: flex;
- align-items: center;
- gap: 8px;
- color: #888;
+.document-content.editing {
+ font-family: "SF Mono", Monaco, "Cascadia Code", monospace;
font-size: 14px;
+ line-height: 1.6;
+ background: var(--bg-primary);
+ border: none;
+ resize: none;
+ color: var(--text-primary);
}
-/* Chat */
-.chat {
- flex: 1;
+.document-content.rendered {
+ line-height: 1.7;
+}
+
+.document-content.rendered h1 { font-size: 28px; margin: 24px 0 16px; }
+.document-content.rendered h2 { font-size: 22px; margin: 20px 0 12px; color: var(--text-secondary); }
+.document-content.rendered h3 { font-size: 18px; margin: 16px 0 8px; }
+.document-content.rendered p { margin: 8px 0; }
+.document-content.rendered code { background: var(--bg-tertiary); padding: 2px 6px; border-radius: 4px; font-size: 13px; }
+.document-content.rendered pre { background: var(--bg-tertiary); padding: 16px; border-radius: 8px; overflow-x: auto; margin: 16px 0; }
+.document-content.rendered pre code { background: none; padding: 0; }
+.document-content.rendered li { margin-left: 24px; margin-bottom: 4px; }
+.document-content.rendered li.task { list-style: none; margin-left: 0; }
+.document-content.rendered li.task.done { color: var(--success); }
+.document-content.rendered mark.review { background: var(--warning); color: black; padding: 2px 4px; border-radius: 2px; }
+.document-content.rendered mark.note { background: var(--accent); color: white; padding: 2px 4px; border-radius: 2px; }
+.document-content.rendered .empty { color: var(--text-secondary); font-style: italic; }
+
+.badge {
+ background: var(--accent);
+ color: white;
+ padding: 2px 8px;
+ border-radius: 4px;
+ font-size: 11px;
+}
+
+/* Chat Pane */
+.chat-pane {
+ width: 380px;
display: flex;
flex-direction: column;
- overflow: hidden;
+ background: var(--bg-secondary);
}
-.messages {
+.chat-messages {
flex: 1;
overflow-y: auto;
padding: 16px;
}
.message {
- margin-bottom: 16px;
- padding: 12px 16px;
+ margin-bottom: 12px;
+ padding: 10px 14px;
border-radius: 8px;
- max-width: 80%;
+ max-width: 90%;
+ font-size: 14px;
+ line-height: 1.5;
}
-.message-user {
- background: #3b82f6;
+.message.user {
+ background: var(--accent);
margin-left: auto;
}
-.message-assistant {
- background: #333;
+.message.assistant {
+ background: var(--bg-tertiary);
}
-.message-loading {
- background: #333;
- color: #888;
+.message.loading {
+ color: var(--text-secondary);
font-style: italic;
}
.chat-input {
display: flex;
gap: 8px;
- padding: 16px;
- background: #252525;
- border-top: 1px solid #333;
+ padding: 12px;
+ border-top: 1px solid var(--border);
}
-.chat-input textarea {
+.chat-input input {
flex: 1;
- padding: 12px;
- background: #333;
- border: 1px solid #444;
+ padding: 10px 14px;
+ background: var(--bg-tertiary);
+ border: 1px solid var(--border);
border-radius: 8px;
- color: #e0e0e0;
- resize: none;
- min-height: 48px;
- max-height: 200px;
+ color: var(--text-primary);
}
.chat-input button {
- padding: 12px 24px;
- background: #3b82f6;
+ padding: 10px 16px;
+ background: var(--accent);
border: none;
- color: white;
border-radius: 8px;
+ color: white;
cursor: pointer;
}
@@ -1457,60 +1556,91 @@ body {
cursor: not-allowed;
}
-.chat-input button.interrupt {
- background: #ef4444;
+/* Action Bar */
+.action-bar {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 12px 16px;
+ background: var(--bg-secondary);
+ border-top: 1px solid var(--border);
}
-```
----
+.action-bar-left, .action-bar-right {
+ display: flex;
+ align-items: center;
+ gap: 16px;
+}
-## Phase 5: Wiring & Polish
+.token-indicator {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
-### 5.1 Update `renderer/src/main.tsx`
+.token-bar {
+ width: 100px;
+ height: 6px;
+ background: var(--bg-tertiary);
+ border-radius: 3px;
+ overflow: hidden;
+}
-```typescript
-import React from "react";
-import { createRoot } from "react-dom/client";
-import { App } from "./App";
-
-createRoot(document.getElementById("root")!).render(
-
-
-
-);
-```
+.token-fill {
+ height: 100%;
+ transition: width 0.3s ease;
+}
-### 5.2 Update `package.json` Dependencies
-
-Add to dependencies:
-```json
-{
- "dependencies": {
- "@anthropic-ai/claude-agent-sdk": "^0.1.0",
- "better-sqlite3": "12.2.0",
- "react": "^19.1.1",
- "react-dom": "^19.1.1",
- "uuid": "^11.0.0"
- },
- "devDependencies": {
- "@types/uuid": "^10.0.0"
- }
+.token-label {
+ font-size: 12px;
+ color: var(--text-secondary);
}
-```
-### 5.3 Ensure `.claude-flow/` Directory Exists
+.permission-toggle {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ font-size: 13px;
+ color: var(--text-secondary);
+ cursor: pointer;
+}
-In `src/main/claude/index.ts`, add helper to create artifact directory:
+.btn-secondary {
+ padding: 8px 16px;
+ background: var(--bg-tertiary);
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ color: var(--text-primary);
+ cursor: pointer;
+}
-```typescript
-import fs from "node:fs";
-import path from "node:path";
+.btn-secondary:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+}
-function ensureArtifactDir(projectPath: string) {
- const artifactDir = path.join(projectPath, ".claude-flow");
- if (!fs.existsSync(artifactDir)) {
- fs.mkdirSync(artifactDir, { recursive: true });
- }
+.btn-primary {
+ padding: 8px 20px;
+ background: var(--accent);
+ border: none;
+ border-radius: 6px;
+ color: white;
+ cursor: pointer;
+ font-weight: 500;
+}
+
+.btn-primary:hover:not(:disabled) {
+ background: var(--accent-hover);
+}
+
+.btn-primary:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+}
+
+.implementing-status {
+ color: var(--success);
+ font-size: 14px;
}
```
@@ -1528,31 +1658,27 @@ function ensureArtifactDir(projectPath: string) {
### Phase 2: Claude Integration
- [ ] Create `src/main/claude/` directory
- [ ] Implement `src/main/claude/phases.ts`
-- [ ] Implement `src/main/claude/hooks.ts`
- [ ] Implement `src/main/claude/index.ts`
- [ ] Add `@anthropic-ai/claude-agent-sdk` dependency
### Phase 3: IPC Layer
-- [ ] Create `src/main/ipc/` directory
- [ ] Implement `src/main/preload.ts`
-- [ ] Implement `src/main/ipc/index.ts`
+- [ ] Implement `src/main/ipc/handlers.ts`
- [ ] Update `src/main/index.ts`
### Phase 4: React UI
- [ ] Create `renderer/src/types.ts`
-- [ ] Create `renderer/src/lib/api.ts`
-- [ ] Create `renderer/src/styles/globals.css`
+- [ ] Create `renderer/src/lib/api.ts` (declare window.api types)
- [ ] Implement `renderer/src/App.tsx`
-- [ ] Implement `renderer/src/components/Sidebar.tsx`
-- [ ] Implement `renderer/src/components/PhaseBar.tsx`
-- [ ] Implement `renderer/src/components/ContextIndicator.tsx`
-- [ ] Implement `renderer/src/components/Chat.tsx`
+- [ ] Implement `renderer/src/components/Header.tsx`
+- [ ] Implement `renderer/src/components/DocumentPane.tsx`
+- [ ] Implement `renderer/src/components/ChatPane.tsx`
+- [ ] Implement `renderer/src/components/ActionBar.tsx`
+- [ ] Create `renderer/src/styles/globals.css`
- [ ] Update `renderer/src/main.tsx`
-### Phase 5: Wiring & Polish
-- [ ] Update `package.json` with new dependencies
-- [ ] Add `uuid` and `@types/uuid`
-- [ ] Add `.claude-flow/` to `.gitignore` template
-- [ ] Test full workflow: research → plan → annotate → implement
-- [ ] Add keyboard shortcuts (Cmd+Enter to send)
-- [ ] Add session rename/delete UI
+### Phase 5: Integration & Polish
+- [ ] Add `uuid` dependency
+- [ ] Test full workflow: Research → Plan → Implement
+- [ ] Add error handling and loading states
+- [ ] Add keyboard shortcuts
--
cgit v1.2.3