From 6d70c5f8a3ed90564b08616a3fb041409916059c Mon Sep 17 00:00:00 2001
From: Clawd <ai@clawd.bot>
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/DocumentPane.tsx | 104 +++++++++++++++++++++++++++++++
 1 file changed, 104 insertions(+)
 create mode 100644 renderer/src/components/DocumentPane.tsx

(limited to 'renderer/src/components/DocumentPane.tsx')

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, "<h3>$1</h3>")
+      .replace(/^## (.*$)/gm, "<h2>$1</h2>")
+      .replace(/^# (.*$)/gm, "<h1>$1</h1>")
+      // Bold/italic
+      .replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>")
+      .replace(/\*([^*]+)\*/g, "<em>$1</em>")
+      // Code blocks
+      .replace(
+        /```(\w*)\n([\s\S]*?)```/g,
+        '<pre><code class="language-$1">$2</code></pre>'
+      )
+      .replace(/`([^`]+)`/g, "<code>$1</code>")
+      // Lists
+      .replace(/^- \[x\] (.*$)/gm, '<li class="task done">☑ $1</li>')
+      .replace(/^- \[ \] (.*$)/gm, '<li class="task">☐ $1</li>')
+      .replace(/^- (.*$)/gm, "<li>$1</li>")
+      // Review comments (highlight them)
+      .replace(/(\/\/ REVIEW:.*$)/gm, '<mark class="review">$1</mark>')
+      .replace(/(\/\/ NOTE:.*$)/gm, '<mark class="note">$1</mark>')
+      // Paragraphs
+      .replace(/\n\n/g, "</p><p>")
+      .replace(/^(.+)$/gm, "<p>$1</p>")
+      // Clean up
+      .replace(/<p><\/p>/g, "")
+      .replace(/<p>(<h[1-3]>)/g, "$1")
+      .replace(/(<\/h[1-3]>)<\/p>/g, "$1")
+      .replace(/<p>(<pre>)/g, "$1")
+      .replace(/(<\/pre>)<\/p>/g, "$1")
+      .replace(/<p>(<li)/g, "$1")
+      .replace(/(<\/li>)<\/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 (
+      <div className="document-pane">
+        <div className="document-header">
+          <span>plan.md</span>
+          <span className="badge">Implementing...</span>
+        </div>
+        <div
+          className="document-content rendered"
+          dangerouslySetInnerHTML={{ __html: renderedHtml }}
+        />
+      </div>
+    );
+  }
+
+  const filename = phase === "research" ? "research.md" : "plan.md";
+
+  return (
+    <div className="document-pane">
+      <div className="document-header">
+        <span>{filename}</span>
+        <button onClick={() => setIsEditing(!isEditing)}>
+          {isEditing ? "Preview" : "Edit"}
+        </button>
+      </div>
+
+      {isEditing ? (
+        <textarea
+          className="document-content editing"
+          value={content}
+          onChange={(e) => onChange(e.target.value)}
+          disabled={disabled}
+          placeholder={`${filename} will appear here...`}
+        />
+      ) : (
+        <div
+          className="document-content rendered"
+          dangerouslySetInnerHTML={{
+            __html:
+              renderedHtml ||
+              '<p class="empty">Document will appear here after Claude generates it...</p>',
+          }}
+          onClick={() => !disabled && setIsEditing(true)}
+        />
+      )}
+    </div>
+  );
+}
-- 
cgit v1.2.3

