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) && (