aboutsummaryrefslogtreecommitdiffstats
path: root/renderer/src
diff options
context:
space:
mode:
Diffstat (limited to 'renderer/src')
-rw-r--r--renderer/src/App.tsx241
-rw-r--r--renderer/src/components/ActionBar.tsx97
-rw-r--r--renderer/src/components/ChatPane.tsx66
-rw-r--r--renderer/src/components/DocumentPane.tsx104
-rw-r--r--renderer/src/components/Header.tsx99
-rw-r--r--renderer/src/main.tsx11
-rw-r--r--renderer/src/styles/globals.css391
-rw-r--r--renderer/src/types.ts35
8 files changed, 1041 insertions, 3 deletions
diff --git a/renderer/src/App.tsx b/renderer/src/App.tsx
new file mode 100644
index 0000000..7a8c378
--- /dev/null
+++ b/renderer/src/App.tsx
@@ -0,0 +1,241 @@
1import React, { useState, useEffect, useCallback } from "react";
2import { Header } from "./components/Header";
3import { DocumentPane } from "./components/DocumentPane";
4import { ChatPane } from "./components/ChatPane";
5import { ActionBar } from "./components/ActionBar";
6import type { Project, Session, Message, Phase, TokenUsage } from "./types";
7import "./styles/globals.css";
8
9const api = window.api;
10
11export function App() {
12 const [projects, setProjects] = useState<Project[]>([]);
13 const [sessions, setSessions] = useState<Session[]>([]);
14 const [selectedProject, setSelectedProject] = useState<Project | null>(null);
15 const [selectedSession, setSelectedSession] = useState<Session | null>(null);
16 const [messages, setMessages] = useState<Message[]>([]);
17 const [documentContent, setDocumentContent] = useState<string>("");
18 const [originalContent, setOriginalContent] = useState<string>("");
19 const [isLoading, setIsLoading] = useState(false);
20 const [tokenUsage, setTokenUsage] = useState<TokenUsage>({
21 inputTokens: 0,
22 outputTokens: 0,
23 });
24
25 const hasChanges = documentContent !== originalContent;
26
27 // Load projects on mount
28 useEffect(() => {
29 api.listProjects().then(setProjects);
30 }, []);
31
32 // Load sessions when project changes
33 useEffect(() => {
34 if (selectedProject) {
35 api.listSessions(selectedProject.id).then(setSessions);
36 } else {
37 setSessions([]);
38 }
39 }, [selectedProject]);
40
41 // Load messages and artifact when session changes
42 useEffect(() => {
43 if (selectedSession && selectedProject) {
44 // Load messages
45 api.listMessages(selectedSession.id).then(setMessages);
46
47 // Load artifact
48 const filename =
49 selectedSession.phase === "research" ? "research.md" : "plan.md";
50 api.readArtifact(selectedProject.path, filename).then((content) => {
51 const text = content || "";
52 setDocumentContent(text);
53 setOriginalContent(text);
54 });
55 } else {
56 setMessages([]);
57 setDocumentContent("");
58 setOriginalContent("");
59 }
60 }, [selectedSession?.id, selectedSession?.phase, selectedProject]);
61
62 // Subscribe to Claude messages
63 useEffect(() => {
64 const unsubscribe = api.onClaudeMessage((sessionId, msg) => {
65 if (sessionId !== selectedSession?.id) return;
66
67 if (msg.type === "result" && msg.subtype === "success") {
68 setIsLoading(false);
69 if (msg.usage) {
70 setTokenUsage({
71 inputTokens: msg.usage.input_tokens,
72 outputTokens: msg.usage.output_tokens,
73 cacheHits: msg.usage.cache_read_input_tokens,
74 });
75 }
76 // Reload artifact after Claude updates it
77 if (selectedProject) {
78 const filename =
79 selectedSession.phase === "research" ? "research.md" : "plan.md";
80 api.readArtifact(selectedProject.path, filename).then((content) => {
81 const text = content || "";
82 setDocumentContent(text);
83 setOriginalContent(text);
84 });
85 }
86 }
87
88 if (msg.type === "assistant") {
89 const content = (
90 msg.message.content as Array<{ type: string; text?: string }>
91 )
92 .filter((c) => c.type === "text" && c.text)
93 .map((c) => c.text!)
94 .join("\n");
95
96 if (content) {
97 setMessages((prev) => {
98 const last = prev[prev.length - 1];
99 if (last?.role === "assistant") {
100 // Update existing assistant message
101 return [...prev.slice(0, -1), { ...last, content }];
102 }
103 // Add new assistant message
104 return [
105 ...prev,
106 {
107 id: crypto.randomUUID(),
108 session_id: sessionId,
109 role: "assistant",
110 content,
111 created_at: Date.now() / 1000,
112 },
113 ];
114 });
115 }
116 }
117 });
118
119 return unsubscribe;
120 }, [selectedSession?.id, selectedSession?.phase, selectedProject]);
121
122 const handleSendMessage = async (message: string) => {
123 if (!selectedSession) return;
124 setIsLoading(true);
125 setMessages((prev) => [
126 ...prev,
127 {
128 id: crypto.randomUUID(),
129 session_id: selectedSession.id,
130 role: "user",
131 content: message,
132 created_at: Date.now() / 1000,
133 },
134 ]);
135 await api.sendMessage(selectedSession.id, message);
136 };
137
138 const handleReview = async () => {
139 if (!selectedSession || !selectedProject) return;
140 // Save user edits first
141 const filename =
142 selectedSession.phase === "research" ? "research.md" : "plan.md";
143 await api.writeArtifact(selectedProject.path, filename, documentContent);
144 setOriginalContent(documentContent);
145 setIsLoading(true);
146 await api.triggerReview(selectedSession.id);
147 };
148
149 const handleSubmit = async () => {
150 if (!selectedSession || !selectedProject) return;
151 // Save any pending edits
152 const filename =
153 selectedSession.phase === "research" ? "research.md" : "plan.md";
154 await api.writeArtifact(selectedProject.path, filename, documentContent);
155
156 const nextPhase = await api.advancePhase(selectedSession.id);
157 if (nextPhase) {
158 setSelectedSession({ ...selectedSession, phase: nextPhase });
159 // Trigger initial message for next phase
160 setIsLoading(true);
161 const initialMsg =
162 nextPhase === "plan"
163 ? "Create a detailed implementation plan based on the research."
164 : "Begin implementing the plan.";
165 await api.sendMessage(selectedSession.id, initialMsg);
166 }
167 };
168
169 const handleCreateProject = async () => {
170 const path = await api.selectDirectory();
171 if (!path) return;
172 const name = path.split("/").pop() || "New Project";
173 const project = await api.createProject(name, path);
174 setProjects((prev) => [project, ...prev]);
175 setSelectedProject(project);
176 };
177
178 const handleCreateSession = async () => {
179 if (!selectedProject) return;
180 const name = `Session ${sessions.length + 1}`;
181 const session = await api.createSession(selectedProject.id, name);
182 setSessions((prev) => [session, ...prev]);
183 setSelectedSession(session);
184 setMessages([]);
185 setDocumentContent("");
186 setOriginalContent("");
187 };
188
189 return (
190 <div className="app">
191 <Header
192 projects={projects}
193 sessions={sessions}
194 selectedProject={selectedProject}
195 selectedSession={selectedSession}
196 onSelectProject={setSelectedProject}
197 onSelectSession={setSelectedSession}
198 onCreateProject={handleCreateProject}
199 onCreateSession={handleCreateSession}
200 />
201
202 <div className="main-content">
203 <DocumentPane
204 content={documentContent}
205 onChange={setDocumentContent}
206 phase={selectedSession?.phase || "research"}
207 disabled={!selectedSession || selectedSession.phase === "implement"}
208 />
209
210 <ChatPane
211 messages={messages}
212 onSend={handleSendMessage}
213 isLoading={isLoading}
214 disabled={!selectedSession}
215 placeholder={
216 selectedSession
217 ? `Chat with Claude (${selectedSession.phase})...`
218 : "Select a session to start"
219 }
220 />
221 </div>
222
223 <ActionBar
224 phase={selectedSession?.phase || "research"}
225 hasChanges={hasChanges}
226 isLoading={isLoading}
227 tokenUsage={tokenUsage}
228 permissionMode={selectedSession?.permission_mode || "acceptEdits"}
229 onReview={handleReview}
230 onSubmit={handleSubmit}
231 onPermissionModeChange={(mode) => {
232 if (selectedSession) {
233 api.setPermissionMode(selectedSession.id, mode);
234 setSelectedSession({ ...selectedSession, permission_mode: mode });
235 }
236 }}
237 disabled={!selectedSession}
238 />
239 </div>
240 );
241}
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 @@
1import React from "react";
2import type { Phase, PermissionMode, TokenUsage } from "../types";
3
4interface ActionBarProps {
5 phase: Phase;
6 hasChanges: boolean;
7 isLoading: boolean;
8 tokenUsage: TokenUsage;
9 permissionMode: PermissionMode;
10 onReview: () => void;
11 onSubmit: () => void;
12 onPermissionModeChange: (mode: PermissionMode) => void;
13 disabled: boolean;
14}
15
16export function ActionBar({
17 phase,
18 hasChanges,
19 isLoading,
20 tokenUsage,
21 permissionMode,
22 onReview,
23 onSubmit,
24 onPermissionModeChange,
25 disabled,
26}: ActionBarProps) {
27 const totalTokens = tokenUsage.inputTokens + tokenUsage.outputTokens;
28 const maxTokens = 200000;
29 const usagePercent = Math.min((totalTokens / maxTokens) * 100, 100);
30
31 const getBarColor = () => {
32 if (usagePercent > 80) return "#ef4444";
33 if (usagePercent > 50) return "#f59e0b";
34 return "#10b981";
35 };
36
37 return (
38 <div className="action-bar">
39 <div className="action-bar-left">
40 <div className="token-indicator">
41 <div className="token-bar">
42 <div
43 className="token-fill"
44 style={{
45 width: `${usagePercent}%`,
46 backgroundColor: getBarColor(),
47 }}
48 />
49 </div>
50 <span className="token-label">
51 {(totalTokens / 1000).toFixed(1)}k / 200k
52 </span>
53 </div>
54
55 {phase === "implement" && (
56 <label className="permission-toggle">
57 <input
58 type="checkbox"
59 checked={permissionMode === "bypassPermissions"}
60 onChange={(e) =>
61 onPermissionModeChange(
62 e.target.checked ? "bypassPermissions" : "acceptEdits"
63 )
64 }
65 disabled={disabled}
66 />
67 Bypass Permissions
68 </label>
69 )}
70 </div>
71
72 <div className="action-bar-right">
73 {phase !== "implement" && (
74 <>
75 <button
76 onClick={onReview}
77 disabled={disabled || isLoading || !hasChanges}
78 className="btn-secondary"
79 >
80 Review
81 </button>
82 <button
83 onClick={onSubmit}
84 disabled={disabled || isLoading}
85 className="btn-primary"
86 >
87 Submit →
88 </button>
89 </>
90 )}
91 {phase === "implement" && isLoading && (
92 <span className="implementing-status">Implementing...</span>
93 )}
94 </div>
95 </div>
96 );
97}
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 @@
1import React, { useState, useRef, useEffect } from "react";
2import type { Message } from "../types";
3
4interface ChatPaneProps {
5 messages: Message[];
6 onSend: (message: string) => void;
7 isLoading: boolean;
8 disabled: boolean;
9 placeholder: string;
10}
11
12export function ChatPane({
13 messages,
14 onSend,
15 isLoading,
16 disabled,
17 placeholder,
18}: ChatPaneProps) {
19 const [input, setInput] = useState("");
20 const messagesEndRef = useRef<HTMLDivElement>(null);
21
22 useEffect(() => {
23 messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
24 }, [messages]);
25
26 const handleSend = () => {
27 if (!input.trim() || isLoading || disabled) return;
28 onSend(input.trim());
29 setInput("");
30 };
31
32 return (
33 <div className="chat-pane">
34 <div className="chat-messages">
35 {messages.map((msg) => (
36 <div key={msg.id} className={`message ${msg.role}`}>
37 <div className="message-content">{msg.content}</div>
38 </div>
39 ))}
40 {isLoading && (
41 <div className="message assistant loading">
42 <div className="message-content">Thinking...</div>
43 </div>
44 )}
45 <div ref={messagesEndRef} />
46 </div>
47
48 <div className="chat-input">
49 <input
50 type="text"
51 value={input}
52 onChange={(e) => setInput(e.target.value)}
53 onKeyDown={(e) => e.key === "Enter" && !e.shiftKey && handleSend()}
54 placeholder={placeholder}
55 disabled={disabled || isLoading}
56 />
57 <button
58 onClick={handleSend}
59 disabled={disabled || isLoading || !input.trim()}
60 >
61 Send
62 </button>
63 </div>
64 </div>
65 );
66}
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 @@
1import React, { useState, useMemo } from "react";
2import type { Phase } from "../types";
3
4interface DocumentPaneProps {
5 content: string;
6 onChange: (content: string) => void;
7 phase: Phase;
8 disabled: boolean;
9}
10
11function renderMarkdown(md: string): string {
12 return (
13 md
14 // Headers
15 .replace(/^### (.*$)/gm, "<h3>$1</h3>")
16 .replace(/^## (.*$)/gm, "<h2>$1</h2>")
17 .replace(/^# (.*$)/gm, "<h1>$1</h1>")
18 // Bold/italic
19 .replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>")
20 .replace(/\*([^*]+)\*/g, "<em>$1</em>")
21 // Code blocks
22 .replace(
23 /```(\w*)\n([\s\S]*?)```/g,
24 '<pre><code class="language-$1">$2</code></pre>'
25 )
26 .replace(/`([^`]+)`/g, "<code>$1</code>")
27 // Lists
28 .replace(/^- \[x\] (.*$)/gm, '<li class="task done">☑ $1</li>')
29 .replace(/^- \[ \] (.*$)/gm, '<li class="task">☐ $1</li>')
30 .replace(/^- (.*$)/gm, "<li>$1</li>")
31 // Review comments (highlight them)
32 .replace(/(\/\/ REVIEW:.*$)/gm, '<mark class="review">$1</mark>')
33 .replace(/(\/\/ NOTE:.*$)/gm, '<mark class="note">$1</mark>')
34 // Paragraphs
35 .replace(/\n\n/g, "</p><p>")
36 .replace(/^(.+)$/gm, "<p>$1</p>")
37 // Clean up
38 .replace(/<p><\/p>/g, "")
39 .replace(/<p>(<h[1-3]>)/g, "$1")
40 .replace(/(<\/h[1-3]>)<\/p>/g, "$1")
41 .replace(/<p>(<pre>)/g, "$1")
42 .replace(/(<\/pre>)<\/p>/g, "$1")
43 .replace(/<p>(<li)/g, "$1")
44 .replace(/(<\/li>)<\/p>/g, "$1")
45 );
46}
47
48export function DocumentPane({
49 content,
50 onChange,
51 phase,
52 disabled,
53}: DocumentPaneProps) {
54 const [isEditing, setIsEditing] = useState(false);
55 const renderedHtml = useMemo(() => renderMarkdown(content), [content]);
56
57 if (phase === "implement") {
58 return (
59 <div className="document-pane">
60 <div className="document-header">
61 <span>plan.md</span>
62 <span className="badge">Implementing...</span>
63 </div>
64 <div
65 className="document-content rendered"
66 dangerouslySetInnerHTML={{ __html: renderedHtml }}
67 />
68 </div>
69 );
70 }
71
72 const filename = phase === "research" ? "research.md" : "plan.md";
73
74 return (
75 <div className="document-pane">
76 <div className="document-header">
77 <span>{filename}</span>
78 <button onClick={() => setIsEditing(!isEditing)}>
79 {isEditing ? "Preview" : "Edit"}
80 </button>
81 </div>
82
83 {isEditing ? (
84 <textarea
85 className="document-content editing"
86 value={content}
87 onChange={(e) => onChange(e.target.value)}
88 disabled={disabled}
89 placeholder={`${filename} will appear here...`}
90 />
91 ) : (
92 <div
93 className="document-content rendered"
94 dangerouslySetInnerHTML={{
95 __html:
96 renderedHtml ||
97 '<p class="empty">Document will appear here after Claude generates it...</p>',
98 }}
99 onClick={() => !disabled && setIsEditing(true)}
100 />
101 )}
102 </div>
103 );
104}
diff --git a/renderer/src/components/Header.tsx b/renderer/src/components/Header.tsx
new file mode 100644
index 0000000..3dcbba9
--- /dev/null
+++ b/renderer/src/components/Header.tsx
@@ -0,0 +1,99 @@
1import React from "react";
2import type { Project, Session, Phase } from "../types";
3
4interface HeaderProps {
5 projects: Project[];
6 sessions: Session[];
7 selectedProject: Project | null;
8 selectedSession: Session | null;
9 onSelectProject: (project: Project | null) => void;
10 onSelectSession: (session: Session | null) => void;
11 onCreateProject: () => void;
12 onCreateSession: () => void;
13}
14
15const phaseLabels: Record<Phase, string> = {
16 research: "Research",
17 plan: "Plan",
18 implement: "Implement",
19};
20
21const phases: Phase[] = ["research", "plan", "implement"];
22
23export function Header({
24 projects,
25 sessions,
26 selectedProject,
27 selectedSession,
28 onSelectProject,
29 onSelectSession,
30 onCreateProject,
31 onCreateSession,
32}: HeaderProps) {
33 return (
34 <header className="header">
35 <div className="header-left">
36 <select
37 value={selectedProject?.id || ""}
38 onChange={(e) => {
39 const project = projects.find((p) => p.id === e.target.value);
40 onSelectProject(project || null);
41 onSelectSession(null);
42 }}
43 >
44 <option value="">Select Project...</option>
45 {projects.map((p) => (
46 <option key={p.id} value={p.id}>
47 {p.name}
48 </option>
49 ))}
50 </select>
51 <button onClick={onCreateProject}>+ Project</button>
52
53 {selectedProject && (
54 <>
55 <select
56 value={selectedSession?.id || ""}
57 onChange={(e) => {
58 const session = sessions.find((s) => s.id === e.target.value);
59 onSelectSession(session || null);
60 }}
61 >
62 <option value="">Select Session...</option>
63 {sessions.map((s) => (
64 <option key={s.id} value={s.id}>
65 {s.name}
66 </option>
67 ))}
68 </select>
69 <button onClick={onCreateSession}>+ Session</button>
70 </>
71 )}
72 </div>
73
74 <div className="header-right">
75 {selectedSession && (
76 <div className="phase-indicator">
77 {phases.map((phase) => {
78 const phaseIndex = phases.indexOf(phase);
79 const currentIndex = phases.indexOf(selectedSession.phase);
80 const isComplete = phaseIndex < currentIndex;
81 const isActive = phase === selectedSession.phase;
82
83 return (
84 <span
85 key={phase}
86 className={`phase-step ${isActive ? "active" : ""} ${
87 isComplete ? "complete" : ""
88 }`}
89 >
90 {phaseLabels[phase]}
91 </span>
92 );
93 })}
94 </div>
95 )}
96 </div>
97 </header>
98 );
99}
diff --git a/renderer/src/main.tsx b/renderer/src/main.tsx
index 9a6b03b..a2f33db 100644
--- a/renderer/src/main.tsx
+++ b/renderer/src/main.tsx
@@ -1,4 +1,9 @@
1import React from 'react' 1import React from "react";
2import { createRoot } from 'react-dom/client' 2import { createRoot } from "react-dom/client";
3import { App } from "./App";
3 4
4createRoot(document.getElementById('root')!).render(<main>hi</main>) 5createRoot(document.getElementById("root")!).render(
6 <React.StrictMode>
7 <App />
8 </React.StrictMode>
9);
diff --git a/renderer/src/styles/globals.css b/renderer/src/styles/globals.css
new file mode 100644
index 0000000..57dc917
--- /dev/null
+++ b/renderer/src/styles/globals.css
@@ -0,0 +1,391 @@
1* {
2 box-sizing: border-box;
3 margin: 0;
4 padding: 0;
5}
6
7:root {
8 --bg-primary: #1a1a1a;
9 --bg-secondary: #252525;
10 --bg-tertiary: #333;
11 --border: #444;
12 --text-primary: #e0e0e0;
13 --text-secondary: #888;
14 --accent: #3b82f6;
15 --accent-hover: #2563eb;
16 --success: #10b981;
17 --warning: #f59e0b;
18 --danger: #ef4444;
19}
20
21body {
22 font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
23 background: var(--bg-primary);
24 color: var(--text-primary);
25 overflow: hidden;
26}
27
28.app {
29 display: flex;
30 flex-direction: column;
31 height: 100vh;
32}
33
34/* Header */
35.header {
36 display: flex;
37 justify-content: space-between;
38 align-items: center;
39 padding: 12px 16px;
40 background: var(--bg-secondary);
41 border-bottom: 1px solid var(--border);
42 -webkit-app-region: drag;
43}
44
45.header-left,
46.header-right {
47 display: flex;
48 align-items: center;
49 gap: 8px;
50 -webkit-app-region: no-drag;
51}
52
53.header select,
54.header button {
55 padding: 6px 12px;
56 background: var(--bg-tertiary);
57 border: 1px solid var(--border);
58 border-radius: 4px;
59 color: var(--text-primary);
60 cursor: pointer;
61 font-size: 13px;
62}
63
64.header button:hover {
65 background: var(--border);
66}
67
68.phase-indicator {
69 display: flex;
70 gap: 4px;
71}
72
73.phase-step {
74 padding: 4px 12px;
75 font-size: 12px;
76 border-radius: 4px;
77 background: var(--bg-tertiary);
78 color: var(--text-secondary);
79}
80
81.phase-step.active {
82 background: var(--accent);
83 color: white;
84}
85
86.phase-step.complete {
87 background: var(--success);
88 color: white;
89}
90
91/* Main Content */
92.main-content {
93 flex: 1;
94 display: flex;
95 overflow: hidden;
96}
97
98/* Document Pane */
99.document-pane {
100 flex: 1;
101 display: flex;
102 flex-direction: column;
103 border-right: 1px solid var(--border);
104}
105
106.document-header {
107 display: flex;
108 justify-content: space-between;
109 align-items: center;
110 padding: 8px 16px;
111 background: var(--bg-secondary);
112 border-bottom: 1px solid var(--border);
113 font-size: 14px;
114 color: var(--text-secondary);
115}
116
117.document-header button {
118 padding: 4px 8px;
119 background: var(--bg-tertiary);
120 border: 1px solid var(--border);
121 border-radius: 4px;
122 color: var(--text-primary);
123 cursor: pointer;
124 font-size: 12px;
125}
126
127.document-content {
128 flex: 1;
129 overflow-y: auto;
130 padding: 24px;
131}
132
133.document-content.editing {
134 font-family: "SF Mono", Monaco, "Cascadia Code", monospace;
135 font-size: 14px;
136 line-height: 1.6;
137 background: var(--bg-primary);
138 border: none;
139 resize: none;
140 color: var(--text-primary);
141}
142
143.document-content.rendered {
144 line-height: 1.7;
145}
146
147.document-content.rendered h1 {
148 font-size: 28px;
149 margin: 24px 0 16px;
150}
151.document-content.rendered h2 {
152 font-size: 22px;
153 margin: 20px 0 12px;
154 color: var(--text-secondary);
155}
156.document-content.rendered h3 {
157 font-size: 18px;
158 margin: 16px 0 8px;
159}
160.document-content.rendered p {
161 margin: 8px 0;
162}
163.document-content.rendered code {
164 background: var(--bg-tertiary);
165 padding: 2px 6px;
166 border-radius: 4px;
167 font-size: 13px;
168}
169.document-content.rendered pre {
170 background: var(--bg-tertiary);
171 padding: 16px;
172 border-radius: 8px;
173 overflow-x: auto;
174 margin: 16px 0;
175}
176.document-content.rendered pre code {
177 background: none;
178 padding: 0;
179}
180.document-content.rendered li {
181 margin-left: 24px;
182 margin-bottom: 4px;
183}
184.document-content.rendered li.task {
185 list-style: none;
186 margin-left: 0;
187}
188.document-content.rendered li.task.done {
189 color: var(--success);
190}
191.document-content.rendered mark.review {
192 background: var(--warning);
193 color: black;
194 padding: 2px 4px;
195 border-radius: 2px;
196}
197.document-content.rendered mark.note {
198 background: var(--accent);
199 color: white;
200 padding: 2px 4px;
201 border-radius: 2px;
202}
203.document-content.rendered .empty {
204 color: var(--text-secondary);
205 font-style: italic;
206}
207
208.badge {
209 background: var(--accent);
210 color: white;
211 padding: 2px 8px;
212 border-radius: 4px;
213 font-size: 11px;
214}
215
216/* Chat Pane */
217.chat-pane {
218 width: 380px;
219 display: flex;
220 flex-direction: column;
221 background: var(--bg-secondary);
222}
223
224.chat-messages {
225 flex: 1;
226 overflow-y: auto;
227 padding: 16px;
228}
229
230.message {
231 margin-bottom: 12px;
232 padding: 10px 14px;
233 border-radius: 8px;
234 max-width: 90%;
235 font-size: 14px;
236 line-height: 1.5;
237 white-space: pre-wrap;
238}
239
240.message.user {
241 background: var(--accent);
242 margin-left: auto;
243}
244
245.message.assistant {
246 background: var(--bg-tertiary);
247}
248
249.message.loading {
250 color: var(--text-secondary);
251 font-style: italic;
252}
253
254.chat-input {
255 display: flex;
256 gap: 8px;
257 padding: 12px;
258 border-top: 1px solid var(--border);
259}
260
261.chat-input input {
262 flex: 1;
263 padding: 10px 14px;
264 background: var(--bg-tertiary);
265 border: 1px solid var(--border);
266 border-radius: 8px;
267 color: var(--text-primary);
268 font-size: 14px;
269}
270
271.chat-input input:focus {
272 outline: none;
273 border-color: var(--accent);
274}
275
276.chat-input button {
277 padding: 10px 16px;
278 background: var(--accent);
279 border: none;
280 border-radius: 8px;
281 color: white;
282 cursor: pointer;
283 font-size: 14px;
284}
285
286.chat-input button:hover:not(:disabled) {
287 background: var(--accent-hover);
288}
289
290.chat-input button:disabled {
291 opacity: 0.5;
292 cursor: not-allowed;
293}
294
295/* Action Bar */
296.action-bar {
297 display: flex;
298 justify-content: space-between;
299 align-items: center;
300 padding: 12px 16px;
301 background: var(--bg-secondary);
302 border-top: 1px solid var(--border);
303}
304
305.action-bar-left,
306.action-bar-right {
307 display: flex;
308 align-items: center;
309 gap: 16px;
310}
311
312.token-indicator {
313 display: flex;
314 align-items: center;
315 gap: 8px;
316}
317
318.token-bar {
319 width: 100px;
320 height: 6px;
321 background: var(--bg-tertiary);
322 border-radius: 3px;
323 overflow: hidden;
324}
325
326.token-fill {
327 height: 100%;
328 transition: width 0.3s ease;
329}
330
331.token-label {
332 font-size: 12px;
333 color: var(--text-secondary);
334}
335
336.permission-toggle {
337 display: flex;
338 align-items: center;
339 gap: 6px;
340 font-size: 13px;
341 color: var(--text-secondary);
342 cursor: pointer;
343}
344
345.permission-toggle input {
346 cursor: pointer;
347}
348
349.btn-secondary {
350 padding: 8px 16px;
351 background: var(--bg-tertiary);
352 border: 1px solid var(--border);
353 border-radius: 6px;
354 color: var(--text-primary);
355 cursor: pointer;
356 font-size: 14px;
357}
358
359.btn-secondary:hover:not(:disabled) {
360 background: var(--border);
361}
362
363.btn-secondary:disabled {
364 opacity: 0.5;
365 cursor: not-allowed;
366}
367
368.btn-primary {
369 padding: 8px 20px;
370 background: var(--accent);
371 border: none;
372 border-radius: 6px;
373 color: white;
374 cursor: pointer;
375 font-weight: 500;
376 font-size: 14px;
377}
378
379.btn-primary:hover:not(:disabled) {
380 background: var(--accent-hover);
381}
382
383.btn-primary:disabled {
384 opacity: 0.5;
385 cursor: not-allowed;
386}
387
388.implementing-status {
389 color: var(--success);
390 font-size: 14px;
391}
diff --git a/renderer/src/types.ts b/renderer/src/types.ts
new file mode 100644
index 0000000..11062ee
--- /dev/null
+++ b/renderer/src/types.ts
@@ -0,0 +1,35 @@
1export interface Project {
2 id: string;
3 name: string;
4 path: string;
5 created_at: number;
6 updated_at: number;
7}
8
9export interface Session {
10 id: string;
11 project_id: string;
12 name: string;
13 phase: Phase;
14 claude_session_id: string | null;
15 permission_mode: PermissionMode;
16 created_at: number;
17 updated_at: number;
18}
19
20export type Phase = "research" | "plan" | "implement";
21export type PermissionMode = "acceptEdits" | "bypassPermissions";
22
23export interface Message {
24 id: string;
25 session_id: string;
26 role: "user" | "assistant";
27 content: string;
28 created_at: number;
29}
30
31export interface TokenUsage {
32 inputTokens: number;
33 outputTokens: number;
34 cacheHits?: number;
35}