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