From 6d70c5f8a3ed90564b08616a3fb041409916059c Mon Sep 17 00:00:00 2001
From: Clawd
Date: Sat, 28 Feb 2026 07:30:40 -0800
Subject: Phase 4: React UI
- Add renderer/src/types.ts with Project, Session, Message, Phase types
- Add renderer/src/styles/globals.css with full styling
- Dark theme with accent colors
- Header, document pane, chat pane, action bar layouts
- Phase indicator, token usage bar, buttons
- Add renderer/src/components/Header.tsx
- Project/session dropdowns with create buttons
- Phase indicator showing current workflow state
- Add renderer/src/components/DocumentPane.tsx
- Markdown viewer/editor with toggle
- Syntax highlighting for review comments
- Task checkbox rendering
- Add renderer/src/components/ChatPane.tsx
- Message list with auto-scroll
- Input field with Enter to send
- Loading state indicator
- Add renderer/src/components/ActionBar.tsx
- Token usage bar with color coding
- Review/Submit buttons for workflow
- Permission mode toggle for implement phase
- Add renderer/src/App.tsx
- Full state management for projects, sessions, messages
- Claude message subscription
- Workflow handlers (review, submit, phase advance)
- Update renderer/src/main.tsx to render App
---
renderer/src/components/ActionBar.tsx | 97 ++++++++++++++++++++++++++++
renderer/src/components/ChatPane.tsx | 66 ++++++++++++++++++++
renderer/src/components/DocumentPane.tsx | 104 +++++++++++++++++++++++++++++++
renderer/src/components/Header.tsx | 99 +++++++++++++++++++++++++++++
4 files changed, 366 insertions(+)
create mode 100644 renderer/src/components/ActionBar.tsx
create mode 100644 renderer/src/components/ChatPane.tsx
create mode 100644 renderer/src/components/DocumentPane.tsx
create mode 100644 renderer/src/components/Header.tsx
(limited to 'renderer/src/components')
diff --git a/renderer/src/components/ActionBar.tsx b/renderer/src/components/ActionBar.tsx
new file mode 100644
index 0000000..22f34b4
--- /dev/null
+++ b/renderer/src/components/ActionBar.tsx
@@ -0,0 +1,97 @@
+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...
+ )}
+
+
+ );
+}
diff --git a/renderer/src/components/ChatPane.tsx b/renderer/src/components/ChatPane.tsx
new file mode 100644
index 0000000..917d462
--- /dev/null
+++ b/renderer/src/components/ChatPane.tsx
@@ -0,0 +1,66 @@
+import React, { useState, useRef, useEffect } from "react";
+import type { Message } from "../types";
+
+interface ChatPaneProps {
+ messages: Message[];
+ onSend: (message: string) => void;
+ isLoading: boolean;
+ disabled: boolean;
+ placeholder: string;
+}
+
+export function ChatPane({
+ messages,
+ onSend,
+ isLoading,
+ disabled,
+ placeholder,
+}: ChatPaneProps) {
+ const [input, setInput] = useState("");
+ const messagesEndRef = useRef(null);
+
+ useEffect(() => {
+ messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
+ }, [messages]);
+
+ const handleSend = () => {
+ if (!input.trim() || isLoading || disabled) return;
+ onSend(input.trim());
+ setInput("");
+ };
+
+ return (
+
+
+ {messages.map((msg) => (
+
+ ))}
+ {isLoading && (
+
+ )}
+
+
+
+
+ setInput(e.target.value)}
+ onKeyDown={(e) => e.key === "Enter" && !e.shiftKey && handleSend()}
+ placeholder={placeholder}
+ disabled={disabled || isLoading}
+ />
+
+
+
+ );
+}
diff --git a/renderer/src/components/DocumentPane.tsx b/renderer/src/components/DocumentPane.tsx
new file mode 100644
index 0000000..e44b0fb
--- /dev/null
+++ b/renderer/src/components/DocumentPane.tsx
@@ -0,0 +1,104 @@
+import React, { useState, useMemo } from "react";
+import type { Phase } from "../types";
+
+interface DocumentPaneProps {
+ content: string;
+ onChange: (content: string) => void;
+ phase: Phase;
+ disabled: boolean;
+}
+
+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")
+ .replace(/(
)/g, "$1")
+ .replace(/(<\/pre>)<\/p>/g, "$1")
+ .replace(/(
)<\/p>/g, "$1")
+ );
+}
+
+export function DocumentPane({
+ content,
+ onChange,
+ phase,
+ disabled,
+}: DocumentPaneProps) {
+ const [isEditing, setIsEditing] = useState(false);
+ const renderedHtml = useMemo(() => renderMarkdown(content), [content]);
+
+ if (phase === "implement") {
+ return (
+
+
+ plan.md
+ Implementing...
+
+
+
+ );
+ }
+
+ const filename = phase === "research" ? "research.md" : "plan.md";
+
+ return (
+
+
+ {filename}
+
+
+
+ {isEditing ? (
+