From afdf3d57cb7ae4cbf0a519d1b53872f151ecba87 Mon Sep 17 00:00:00 2001 From: bndw 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 `` 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(""); const [originalContent, setOriginalContent] = useState(""); const [isLoading, setIsLoading] = useState(false); + const [activityStatus, setActivityStatus] = useState(null); const [tokenUsage, setTokenUsage] = useState({ 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} /> -- cgit v1.2.3