feat(frontend): rotating activity icon + 5-line expandable activity panel

- CentralStatus: replace the red pulsing dot at the start of the "working" line
  with the rotating WorkingSpinner, now a button that opens the model-activity
  panel (onOpenActivity). WorkingSpinner is extracted to its own module (was
  local to AppShell).
- ModelActivityPanel: show the last 5 lines of the model-stream tail (refreshing
  as the stream grows) with an expand toggle to the full stream, replacing the
  unbounded full transcript.
- AppShell: drop the separate spinner button (the inline icon is now the single
  trigger) and the local WorkingSpinner def.
- Remove the now-orphaned Transcript.tsx (its only consumer was the panel).

TDD: CentralStatus + ModelActivityPanel tests RED->GREEN; frontend suite 87/87;
tsc clean. Live visual verification pending (to do with the B/C frontend pass).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-30 16:26:43 +02:00
co-authored by Claude Opus 4.8
parent c12bdcd257
commit 0eeb3f7522
7 changed files with 115 additions and 70 deletions
@@ -18,3 +18,21 @@ test("close button calls onClose", async () => {
await userEvent.click(screen.getByRole("button", { name: /close/i }));
expect(onClose).toHaveBeenCalled();
});
const EIGHT = ["line-01", "line-02", "line-03", "line-04", "line-05", "line-06", "line-07", "line-08"].join("\n");
test("collapsed shows only the last 5 lines of the model-stream tail", () => {
useSessionStore.getState().applyEvent({ type: "text_delta", text: EIGHT });
render(<ModelActivityPanel onClose={vi.fn()} />);
expect(screen.getByText("line-08")).toBeInTheDocument();
expect(screen.getByText("line-04")).toBeInTheDocument();
expect(screen.queryByText("line-03")).not.toBeInTheDocument();
expect(screen.queryByText("line-01")).not.toBeInTheDocument();
});
test("expanding reveals the full stream", async () => {
useSessionStore.getState().applyEvent({ type: "text_delta", text: EIGHT });
render(<ModelActivityPanel onClose={vi.fn()} />);
await userEvent.click(screen.getByRole("button", { name: /show more/i }));
expect(screen.getByText("line-01")).toBeInTheDocument();
});