From a39ba2cef6e28ddc62431cd133a3a0ea25aabd98 Mon Sep 17 00:00:00 2001 From: Dave Date: Tue, 24 Feb 2026 13:28:43 +0000 Subject: [PATCH] story-kit: merge 140_bug_activity_status_indicator_never_visible_due_to_display_condition --- frontend/src/components/Chat.test.tsx | 81 +++++++++++++++++++++++++++ frontend/src/components/Chat.tsx | 3 +- 2 files changed, 83 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/Chat.test.tsx b/frontend/src/components/Chat.test.tsx index d732d00..420fc96 100644 --- a/frontend/src/components/Chat.test.tsx +++ b/frontend/src/components/Chat.test.tsx @@ -18,6 +18,7 @@ type WsHandlers = { onUpdate: (history: Message[]) => void; onSessionId: (sessionId: string) => void; onError: (message: string) => void; + onActivity: (toolName: string) => void; onReconciliationProgress: ( storyId: string, status: string, @@ -393,3 +394,83 @@ describe("Chat reconciliation banner", () => { }); }); }); + +describe("Chat activity status indicator (Bug 140)", () => { + beforeEach(() => { + capturedWsHandlers = null; + setupMocks(); + }); + + it("shows activity label when tool activity fires during streaming content", async () => { + render(); + + await waitFor(() => expect(capturedWsHandlers).not.toBeNull()); + + // Simulate sending a message to set loading=true + const input = screen.getByPlaceholderText("Send a message..."); + await act(async () => { + fireEvent.change(input, { target: { value: "Read my file" } }); + }); + await act(async () => { + fireEvent.keyDown(input, { key: "Enter", shiftKey: false }); + }); + + // Simulate tokens arriving (streamingContent becomes non-empty) + act(() => { + capturedWsHandlers?.onToken("I'll read that file for you."); + }); + + // Now simulate a tool activity event while streamingContent is non-empty + act(() => { + capturedWsHandlers?.onActivity("read_file"); + }); + + // The activity indicator should be visible with the tool activity label + const indicator = await screen.findByTestId("activity-indicator"); + expect(indicator).toBeInTheDocument(); + expect(indicator).toHaveTextContent("Reading file..."); + }); + + it("shows Thinking... fallback when loading with no streaming and no activity", async () => { + render(); + + await waitFor(() => expect(capturedWsHandlers).not.toBeNull()); + + // Simulate sending a message to set loading=true + const input = screen.getByPlaceholderText("Send a message..."); + await act(async () => { + fireEvent.change(input, { target: { value: "Hello" } }); + }); + await act(async () => { + fireEvent.keyDown(input, { key: "Enter", shiftKey: false }); + }); + + // No tokens, no activity — should show "Thinking..." + const indicator = await screen.findByTestId("activity-indicator"); + expect(indicator).toBeInTheDocument(); + expect(indicator).toHaveTextContent("Thinking..."); + }); + + it("hides Thinking... when streaming content is present but no tool activity", async () => { + render(); + + await waitFor(() => expect(capturedWsHandlers).not.toBeNull()); + + // Simulate sending a message to set loading=true + const input = screen.getByPlaceholderText("Send a message..."); + await act(async () => { + fireEvent.change(input, { target: { value: "Hello" } }); + }); + await act(async () => { + fireEvent.keyDown(input, { key: "Enter", shiftKey: false }); + }); + + // Tokens arrive — streamingContent is non-empty, no activity + act(() => { + capturedWsHandlers?.onToken("Here is my response..."); + }); + + // The activity indicator should NOT be visible (just streaming bubble) + expect(screen.queryByTestId("activity-indicator")).not.toBeInTheDocument(); + }); +}); diff --git a/frontend/src/components/Chat.tsx b/frontend/src/components/Chat.tsx index 551e18d..8e7728b 100644 --- a/frontend/src/components/Chat.tsx +++ b/frontend/src/components/Chat.tsx @@ -683,8 +683,9 @@ export function Chat({ projectPath, onCloseProject }: ChatProps) { )} - {loading && !streamingContent && ( + {loading && (activityStatus != null || !streamingContent) && (