From dc09d925cd1d320910f13078254fc717e7865c61 Mon Sep 17 00:00:00 2001 From: Dave Waring Date: Sun, 29 Mar 2026 08:38:01 -0400 Subject: [PATCH 1/6] Hide Action Timeline and keep tool status visible during streaming MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two UX fixes for the chat experience: 1. Remove the Action Timeline card entirely — it shows operational detail (tool calls, approval results) that has no user value and makes the UI feel frozen during onboarding. 2. Keep tool status feedback ("Reading from your library...", "Writing to your library...") visible even after text has started streaming. Previously, the typing indicator disappeared once the first text-delta arrived, leaving the user with no feedback during subsequent tool calls (like writing the spec/plan). Before: isTyping = isLoading && !hasStartedAssistantReply After: isTyping = isLoading && (!hasStartedAssistantReply || !!toolStatus) Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/components/chat/ChatPanel.tsx | 29 ++++--------------- 1 file changed, 5 insertions(+), 24 deletions(-) diff --git a/builds/typescript/client_web/src/components/chat/ChatPanel.tsx b/builds/typescript/client_web/src/components/chat/ChatPanel.tsx index b1a64c9..54c6031 100644 --- a/builds/typescript/client_web/src/components/chat/ChatPanel.tsx +++ b/builds/typescript/client_web/src/components/chat/ChatPanel.tsx @@ -92,7 +92,6 @@ export default function ChatPanel({ conversationId, toolStatus, pendingApprovals, - activity, append, resolveApproval, stop @@ -173,11 +172,13 @@ export default function ChatPanel({ const lastMessage = messages.length > 0 ? messages[messages.length - 1] : null; const hasStartedAssistantReply = isLoading && lastMessage?.role === "assistant"; - const isTyping = isLoading && !hasStartedAssistantReply; - const typingStatus = isTyping + const isTyping = isLoading && (!hasStartedAssistantReply || !!toolStatus); + const typingStatus = isLoading ? toolStatus ? formatToolStatus(toolStatus) - : "Thinking..." + : hasStartedAssistantReply + ? undefined + : "Thinking..." : undefined; const chatError = historyError ?? error?.message ?? null; const visibleChatError = @@ -415,26 +416,6 @@ export default function ChatPanel({ )} - {activity.length > 0 && ( -
-
- Action Timeline -
-
- {activity.slice(-8).map((item) => ( -
- {item.message} - - {item.type} - -
- ))} -
-
- )} )) : contentOverride} From add74fbf0c4166d880bcb0fa7fa84c91c5b7e6d3 Mon Sep 17 00:00:00 2001 From: Dave Waring Date: Sun, 29 Mar 2026 09:03:42 -0400 Subject: [PATCH 2/6] Fix draft conversation history leak between projects When switching to a project with no saved conversation (draft mode), use empty messages instead of externalMessages, which may be stale from the previous project's history that hasn't cleared yet. This fix was on the onboarding-flow-updates branch but was lost when we created the clean fix/hide-action-timeline branch from main. Co-Authored-By: Claude Opus 4.6 (1M context) --- builds/typescript/client_web/src/api/useGatewayChat.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/builds/typescript/client_web/src/api/useGatewayChat.ts b/builds/typescript/client_web/src/api/useGatewayChat.ts index bf3ba9b..bcf7e18 100644 --- a/builds/typescript/client_web/src/api/useGatewayChat.ts +++ b/builds/typescript/client_web/src/api/useGatewayChat.ts @@ -181,7 +181,9 @@ export function useGatewayChat(options: UseGatewayChatOptions = {}): { conversationIdRef.current = restored.conversationId; backgroundStates.delete(cacheKey); } else { - setMessages(externalMessages); + // For draft conversations (no external ID), always start empty — externalMessages + // may be stale from the previous project's history that hasn't cleared yet. + setMessages(externalConversationId ? externalMessages : EMPTY_MESSAGES); setIsLoading(false); setError(null); setToolStatus(null); From 96fd6d3d2f91e756c0dfa3feeea67d94d99a9f3c Mon Sep 17 00:00:00 2001 From: Dave Waring Date: Sun, 29 Mar 2026 09:06:13 -0400 Subject: [PATCH 3/6] Fix conversation history leak for ALL projects, not just drafts The previous fix only handled draft projects (null conversationId). Projects WITH saved conversations had the same race condition: when the cacheKey effect runs, externalMessages still holds the previous project's history because setHistoryMessages([]) hasn't re-rendered yet. Fix: always start with empty messages on conversation switch. The secondary effect at line 199 applies the correct history once the async fetch completes. Co-Authored-By: Claude Opus 4.6 (1M context) --- builds/typescript/client_web/src/api/useGatewayChat.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/builds/typescript/client_web/src/api/useGatewayChat.ts b/builds/typescript/client_web/src/api/useGatewayChat.ts index bcf7e18..adb944a 100644 --- a/builds/typescript/client_web/src/api/useGatewayChat.ts +++ b/builds/typescript/client_web/src/api/useGatewayChat.ts @@ -181,9 +181,10 @@ export function useGatewayChat(options: UseGatewayChatOptions = {}): { conversationIdRef.current = restored.conversationId; backgroundStates.delete(cacheKey); } else { - // For draft conversations (no external ID), always start empty — externalMessages - // may be stale from the previous project's history that hasn't cleared yet. - setMessages(externalConversationId ? externalMessages : EMPTY_MESSAGES); + // Always start empty when switching conversations — externalMessages may be stale + // from the previous project's history that hasn't cleared yet. The secondary effect + // (below) will apply the correct history once it arrives from the async fetch. + setMessages(EMPTY_MESSAGES); setIsLoading(false); setError(null); setToolStatus(null); From 3dc6c3a42b1e02009b8807324d15c21ca1d3849f Mon Sep 17 00:00:00 2001 From: Dave Waring Date: Sun, 29 Mar 2026 09:15:42 -0400 Subject: [PATCH 4/6] Show typing feedback throughout entire AI response lifecycle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, the typing indicator disappeared once text started streaming. If the AI paused between text and tool calls (e.g., deciding to write spec/plan), the UI showed nothing — felt frozen. Now shows "Thinking..." or tool status ("Writing to your library...") for the entire duration the AI is loading. Composer stop button behavior preserved (only shows before text starts). Co-Authored-By: Claude Opus 4.6 (1M context) --- .../client_web/src/components/chat/ChatPanel.tsx | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/builds/typescript/client_web/src/components/chat/ChatPanel.tsx b/builds/typescript/client_web/src/components/chat/ChatPanel.tsx index 54c6031..66f5f12 100644 --- a/builds/typescript/client_web/src/components/chat/ChatPanel.tsx +++ b/builds/typescript/client_web/src/components/chat/ChatPanel.tsx @@ -172,13 +172,12 @@ export default function ChatPanel({ const lastMessage = messages.length > 0 ? messages[messages.length - 1] : null; const hasStartedAssistantReply = isLoading && lastMessage?.role === "assistant"; - const isTyping = isLoading && (!hasStartedAssistantReply || !!toolStatus); + const isWaitingForReply = isLoading && !hasStartedAssistantReply; + const showTypingFeedback = isLoading; const typingStatus = isLoading ? toolStatus ? formatToolStatus(toolStatus) - : hasStartedAssistantReply - ? undefined - : "Thinking..." + : "Thinking..." : undefined; const chatError = historyError ?? error?.message ?? null; const visibleChatError = @@ -273,7 +272,7 @@ export default function ChatPanel({ onRemoveAttachment: () => setAttachment(null), fileError, onClearFileError: () => setFileError(null), - isStreaming: isTyping, + isStreaming: isWaitingForReply, onStop: stop }; @@ -340,7 +339,7 @@ export default function ChatPanel({ ) : ( {visibleChatError && ( From 307fe5c8a50f309006333aae53a5cde50758fdbb Mon Sep 17 00:00:00 2001 From: Dave Waring Date: Sun, 29 Mar 2026 09:17:24 -0400 Subject: [PATCH 5/6] Hide typing indicator while waiting for user approval MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Don't show "Thinking..." when the AI is blocked on a pending approval card — the AI isn't thinking, it's waiting for the user. Co-Authored-By: Claude Opus 4.6 (1M context) --- builds/typescript/client_web/src/components/chat/ChatPanel.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/builds/typescript/client_web/src/components/chat/ChatPanel.tsx b/builds/typescript/client_web/src/components/chat/ChatPanel.tsx index 66f5f12..1b16dba 100644 --- a/builds/typescript/client_web/src/components/chat/ChatPanel.tsx +++ b/builds/typescript/client_web/src/components/chat/ChatPanel.tsx @@ -173,7 +173,7 @@ export default function ChatPanel({ const lastMessage = messages.length > 0 ? messages[messages.length - 1] : null; const hasStartedAssistantReply = isLoading && lastMessage?.role === "assistant"; const isWaitingForReply = isLoading && !hasStartedAssistantReply; - const showTypingFeedback = isLoading; + const showTypingFeedback = isLoading && pendingApprovals.length === 0; const typingStatus = isLoading ? toolStatus ? formatToolStatus(toolStatus) From 1c3e5f06d440f3f3dc780c0e9f907ee903f4c4c7 Mon Sep 17 00:00:00 2001 From: Dave Waring Date: Sun, 29 Mar 2026 09:21:35 -0400 Subject: [PATCH 6/6] Show tool status after approval, not 'Approval approved' After user approves a tool call, show the friendly tool status ("Writing to your library...") instead of "Approval approved". The user cares about what's happening, not the approval mechanics. - resolveApproval: captures tool name before removing approval, sets toolStatus to the tool name (not "Approval {decision}") - approval-request event: sets toolStatus to tool name directly (not "Approval required: tool_name") so formatToolStatus works - approval-result event: no longer sets toolStatus (resolveApproval already handled it) Co-Authored-By: Claude Opus 4.6 (1M context) --- .../client_web/src/api/useGatewayChat.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/builds/typescript/client_web/src/api/useGatewayChat.ts b/builds/typescript/client_web/src/api/useGatewayChat.ts index adb944a..35070f8 100644 --- a/builds/typescript/client_web/src/api/useGatewayChat.ts +++ b/builds/typescript/client_web/src/api/useGatewayChat.ts @@ -237,9 +237,12 @@ export function useGatewayChat(options: UseGatewayChatOptions = {}): { } async function resolveApproval(requestId: string, decision: ApprovalDecision): Promise { + // Capture the tool name before removing the approval so we can show + // a user-friendly status ("Writing to your library...") instead of "Approval approved" + const approvalToolName = pendingApprovals.find((a) => a.requestId === requestId)?.toolName; await submitApprovalDecision(requestId, decision); setPendingApprovals((current) => current.filter((approval) => approval.requestId !== requestId)); - setToolStatus(`Approval ${decision}`); + setToolStatus(decision === "approved" && approvalToolName ? approvalToolName : null); setActivity((current) => appendActivity(current, { id: nextActivityId(), @@ -512,9 +515,9 @@ export function useGatewayChat(options: UseGatewayChatOptions = {}): { message: `Approval required for ${humanizeToolName(event.tool_name)}`, }); if (isActive()) { - setToolStatus(`Approval required: ${event.tool_name}`); + setToolStatus(event.tool_name); } else { - updateBackground(() => ({ toolStatus: `Approval required: ${event.tool_name}` })); + updateBackground(() => ({ toolStatus: event.tool_name })); } break; case "approval-result": @@ -524,11 +527,8 @@ export function useGatewayChat(options: UseGatewayChatOptions = {}): { message: `Approval ${event.decision}`, status: event.decision, }); - if (isActive()) { - setToolStatus(`Approval ${event.decision}`); - } else { - updateBackground(() => ({ toolStatus: `Approval ${event.decision}` })); - } + // Don't set toolStatus here — resolveApproval already set it to the + // tool name (so it shows "Writing to your library..." not "Approval approved") break; } }