aboutsummaryrefslogtreecommitdiffstats
path: root/renderer/src/App.tsx
diff options
context:
space:
mode:
authorbndw <ben@bdw.to>2026-03-01 08:10:35 -0800
committerbndw <ben@bdw.to>2026-03-01 08:10:35 -0800
commitafdf3d57cb7ae4cbf0a519d1b53872f151ecba87 (patch)
tree601cf9cfa293c2287d6f4fc0aefefd37c99a29b7 /renderer/src/App.tsx
parent454453788e759fa16442e755434fbb842fa1acab (diff)
feat: Add `activityStatus` state and `handleCancel` to App.ts… (+4 more)
- ✅ Add `activityStatus` state and `handleCancel` to App.tsx; update Escape handler - ✅ Extend `onClaudeMessage` handler: tool_progress, task_progress, restructure result block - ✅ Pass `activityStatus` and `onCancel` props to `<ChatPane>` in App.tsx - ✅ Update ChatPane.tsx props interface and loading bubble render - ✅ Add loading bubble CSS (flex layout, cancel × styles, pulse animation)
Diffstat (limited to 'renderer/src/App.tsx')
-rw-r--r--renderer/src/App.tsx105
1 files changed, 73 insertions, 32 deletions
diff --git a/renderer/src/App.tsx b/renderer/src/App.tsx
index c3eafd4..36d3a82 100644
--- a/renderer/src/App.tsx
+++ b/renderer/src/App.tsx
@@ -52,6 +52,7 @@ export function App() {
52 const [documentContent, setDocumentContent] = useState<string>(""); 52 const [documentContent, setDocumentContent] = useState<string>("");
53 const [originalContent, setOriginalContent] = useState<string>(""); 53 const [originalContent, setOriginalContent] = useState<string>("");
54 const [isLoading, setIsLoading] = useState(false); 54 const [isLoading, setIsLoading] = useState(false);
55 const [activityStatus, setActivityStatus] = useState<string | null>(null);
55 const [tokenUsage, setTokenUsage] = useState<TokenUsage>({ 56 const [tokenUsage, setTokenUsage] = useState<TokenUsage>({
56 inputTokens: 0, 57 inputTokens: 0,
57 outputTokens: 0, 58 outputTokens: 0,
@@ -84,6 +85,13 @@ export function App() {
84 localStorage.setItem("cf-chat-collapsed", String(chatCollapsed)); 85 localStorage.setItem("cf-chat-collapsed", String(chatCollapsed));
85 }, [chatCollapsed]); 86 }, [chatCollapsed]);
86 87
88 const handleCancel = () => {
89 if (!selectedSession) return;
90 api.interruptSession(selectedSession.id);
91 setIsLoading(false);
92 setActivityStatus(null);
93 };
94
87 const handleToggleTheme = () => 95 const handleToggleTheme = () =>
88 setTheme((t) => (t === "dark" ? "light" : "dark")); 96 setTheme((t) => (t === "dark" ? "light" : "dark"));
89 97
@@ -121,6 +129,7 @@ export function App() {
121 if (e.key === "Escape" && isLoading && selectedSession) { 129 if (e.key === "Escape" && isLoading && selectedSession) {
122 api.interruptSession(selectedSession.id); 130 api.interruptSession(selectedSession.id);
123 setIsLoading(false); 131 setIsLoading(false);
132 setActivityStatus(null);
124 } 133 }
125 // Cmd/Ctrl + Enter to submit 134 // Cmd/Ctrl + Enter to submit
126 if ( 135 if (
@@ -178,43 +187,73 @@ export function App() {
178 const unsubscribe = api.onClaudeMessage((sessionId, msg) => { 187 const unsubscribe = api.onClaudeMessage((sessionId, msg) => {
179 if (sessionId !== selectedSession?.id) return; 188 if (sessionId !== selectedSession?.id) return;
180 189
181 if (msg.type === "result" && msg.subtype === "success") { 190 // ── Live activity indicator ──────────────────────────────────────
191 // Primary: tool_progress fires periodically while a tool is running
192 if (msg.type === "tool_progress") {
193 const elapsed = Math.round(msg.elapsed_time_seconds);
194 setActivityStatus(
195 elapsed > 0
196 ? `Using ${msg.tool_name} (${elapsed}s)`
197 : `Using ${msg.tool_name}…`
198 );
199 }
200
201 // Secondary: task_progress carries last_tool_name for sub-agent tasks
202 if (
203 msg.type === "system" &&
204 msg.subtype === "task_progress" &&
205 (msg as { last_tool_name?: string }).last_tool_name
206 ) {
207 setActivityStatus(
208 `Using ${(msg as { last_tool_name: string }).last_tool_name}…`
209 );
210 }
211
212 // ── Result (success or error) ────────────────────────────────────
213 // Always clear loading state on any result subtype so error results
214 // don't leave the UI stuck in the loading/thinking state.
215 if (msg.type === "result") {
182 setIsLoading(false); 216 setIsLoading(false);
183 if (msg.usage) { 217 setActivityStatus(null);
184 setTokenUsage({ 218
185 inputTokens: msg.usage.input_tokens, 219 if (msg.subtype === "success") {
186 outputTokens: msg.usage.output_tokens, 220 if (msg.usage) {
187 cacheHits: msg.usage.cache_read_input_tokens, 221 setTokenUsage({
188 }); 222 inputTokens: msg.usage.input_tokens,
189 } 223 outputTokens: msg.usage.output_tokens,
190 // Reload artifact after Claude updates it 224 cacheHits: msg.usage.cache_read_input_tokens,
191 if (selectedProject && selectedSession) {
192 const filename =
193 selectedSession.phase === "research" ? "research.md" : "plan.md";
194 // Capture for the async closure
195 const sessionAtTrigger = selectedSession;
196 api
197 .readSessionArtifact(selectedProject.id, sessionAtTrigger.id, filename)
198 .then((content) => {
199 const text = content || "";
200 setDocumentContent(text);
201 setOriginalContent(text);
202
203 // Auto-name: only during research, only while name is still the default
204 if (
205 sessionAtTrigger.phase === "research" &&
206 /^Session \d+$/.test(sessionAtTrigger.name) &&
207 text.length > 0
208 ) {
209 const derived = extractSessionName(text);
210 if (derived) {
211 handleRenameSession(sessionAtTrigger.id, derived);
212 }
213 }
214 }); 225 });
226 }
227 // Reload artifact after Claude updates it
228 if (selectedProject && selectedSession) {
229 const filename =
230 selectedSession.phase === "research" ? "research.md" : "plan.md";
231 // Capture for the async closure
232 const sessionAtTrigger = selectedSession;
233 api
234 .readSessionArtifact(selectedProject.id, sessionAtTrigger.id, filename)
235 .then((content) => {
236 const text = content || "";
237 setDocumentContent(text);
238 setOriginalContent(text);
239
240 // Auto-name: only during research, only while name is still the default
241 if (
242 sessionAtTrigger.phase === "research" &&
243 /^Session \d+$/.test(sessionAtTrigger.name) &&
244 text.length > 0
245 ) {
246 const derived = extractSessionName(text);
247 if (derived) {
248 handleRenameSession(sessionAtTrigger.id, derived);
249 }
250 }
251 });
252 }
215 } 253 }
216 } 254 }
217 255
256 // ── Assistant message ────────────────────────────────────────────
218 if (msg.type === "assistant") { 257 if (msg.type === "assistant") {
219 const content = ( 258 const content = (
220 msg.message.content as Array<{ type: string; text?: string }> 259 msg.message.content as Array<{ type: string; text?: string }>
@@ -432,6 +471,8 @@ export function App() {
432 collapsed={chatCollapsed} 471 collapsed={chatCollapsed}
433 chatWidth={chatWidth} 472 chatWidth={chatWidth}
434 onToggleCollapse={() => setChatCollapsed((c) => !c)} 473 onToggleCollapse={() => setChatCollapsed((c) => !c)}
474 activityStatus={activityStatus}
475 onCancel={handleCancel}
435 /> 476 />
436 </div> 477 </div>
437 478