From afdf3d57cb7ae4cbf0a519d1b53872f151ecba87 Mon Sep 17 00:00:00 2001
From: bndw <ben@bdw.to>
Date: Sun, 1 Mar 2026 08:10:35 -0800
Subject: feat: Add `activityStatus` state and `handleCancel` to App.ts… (+4
 more)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

- ✅ 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)
---
 renderer/src/App.tsx | 105 +++++++++++++++++++++++++++++++++++----------------
 1 file changed, 73 insertions(+), 32 deletions(-)

(limited to 'renderer/src/App.tsx')

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() {
   const [documentContent, setDocumentContent] = useState<string>("");
   const [originalContent, setOriginalContent] = useState<string>("");
   const [isLoading, setIsLoading] = useState(false);
+  const [activityStatus, setActivityStatus] = useState<string | null>(null);
   const [tokenUsage, setTokenUsage] = useState<TokenUsage>({
     inputTokens: 0,
     outputTokens: 0,
@@ -84,6 +85,13 @@ export function App() {
     localStorage.setItem("cf-chat-collapsed", String(chatCollapsed));
   }, [chatCollapsed]);
 
+  const handleCancel = () => {
+    if (!selectedSession) return;
+    api.interruptSession(selectedSession.id);
+    setIsLoading(false);
+    setActivityStatus(null);
+  };
+
   const handleToggleTheme = () =>
     setTheme((t) => (t === "dark" ? "light" : "dark"));
 
@@ -121,6 +129,7 @@ export function App() {
       if (e.key === "Escape" && isLoading && selectedSession) {
         api.interruptSession(selectedSession.id);
         setIsLoading(false);
+        setActivityStatus(null);
       }
       // Cmd/Ctrl + Enter to submit
       if (
@@ -178,43 +187,73 @@ export function App() {
     const unsubscribe = api.onClaudeMessage((sessionId, msg) => {
       if (sessionId !== selectedSession?.id) return;
 
-      if (msg.type === "result" && msg.subtype === "success") {
+      // ── Live activity indicator ──────────────────────────────────────
+      // Primary: tool_progress fires periodically while a tool is running
+      if (msg.type === "tool_progress") {
+        const elapsed = Math.round(msg.elapsed_time_seconds);
+        setActivityStatus(
+          elapsed > 0
+            ? `Using ${msg.tool_name} (${elapsed}s)`
+            : `Using ${msg.tool_name}…`
+        );
+      }
+
+      // Secondary: task_progress carries last_tool_name for sub-agent tasks
+      if (
+        msg.type === "system" &&
+        msg.subtype === "task_progress" &&
+        (msg as { last_tool_name?: string }).last_tool_name
+      ) {
+        setActivityStatus(
+          `Using ${(msg as { last_tool_name: string }).last_tool_name}…`
+        );
+      }
+
+      // ── Result (success or error) ────────────────────────────────────
+      // Always clear loading state on any result subtype so error results
+      // don't leave the UI stuck in the loading/thinking state.
+      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 && selectedSession) {
-          const filename =
-            selectedSession.phase === "research" ? "research.md" : "plan.md";
-          // Capture for the async closure
-          const sessionAtTrigger = selectedSession;
-          api
-            .readSessionArtifact(selectedProject.id, sessionAtTrigger.id, filename)
-            .then((content) => {
-              const text = content || "";
-              setDocumentContent(text);
-              setOriginalContent(text);
-
-              // Auto-name: only during research, only while name is still the default
-              if (
-                sessionAtTrigger.phase === "research" &&
-                /^Session \d+$/.test(sessionAtTrigger.name) &&
-                text.length > 0
-              ) {
-                const derived = extractSessionName(text);
-                if (derived) {
-                  handleRenameSession(sessionAtTrigger.id, derived);
-                }
-              }
+        setActivityStatus(null);
+
+        if (msg.subtype === "success") {
+          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 && selectedSession) {
+            const filename =
+              selectedSession.phase === "research" ? "research.md" : "plan.md";
+            // Capture for the async closure
+            const sessionAtTrigger = selectedSession;
+            api
+              .readSessionArtifact(selectedProject.id, sessionAtTrigger.id, filename)
+              .then((content) => {
+                const text = content || "";
+                setDocumentContent(text);
+                setOriginalContent(text);
+
+                // Auto-name: only during research, only while name is still the default
+                if (
+                  sessionAtTrigger.phase === "research" &&
+                  /^Session \d+$/.test(sessionAtTrigger.name) &&
+                  text.length > 0
+                ) {
+                  const derived = extractSessionName(text);
+                  if (derived) {
+                    handleRenameSession(sessionAtTrigger.id, derived);
+                  }
+                }
+              });
+          }
         }
       }
 
+      // ── Assistant message ────────────────────────────────────────────
       if (msg.type === "assistant") {
         const content = (
           msg.message.content as Array<{ type: string; text?: string }>
@@ -432,6 +471,8 @@ export function App() {
           collapsed={chatCollapsed}
           chatWidth={chatWidth}
           onToggleCollapse={() => setChatCollapsed((c) => !c)}
+          activityStatus={activityStatus}
+          onCancel={handleCancel}
         />
       </div>
 
-- 
cgit v1.2.3

