diff --git a/frontend/src/shell/AppShell.tsx b/frontend/src/shell/AppShell.tsx
index afb01de7..cb925c48 100644
--- a/frontend/src/shell/AppShell.tsx
+++ b/frontend/src/shell/AppShell.tsx
@@ -176,7 +176,7 @@ export function AppShell() {
{activeSessionId ? (
<>
-
+
>
) : (
@@ -188,20 +188,6 @@ export function AppShell() {
{/* Sticky composer: the box stays pinned while the conversation scrolls above. */}
- {activeSessionId && (
-
-
-
- )}
-
-
-
- );
-}
-
function EmptyState() {
return (
diff --git a/frontend/src/shell/CentralStatus.test.tsx b/frontend/src/shell/CentralStatus.test.tsx
index 61c5e151..23a11e10 100644
--- a/frontend/src/shell/CentralStatus.test.tsx
+++ b/frontend/src/shell/CentralStatus.test.tsx
@@ -1,10 +1,19 @@
import { render, screen } from "@testing-library/react";
-import { beforeEach } from "vitest";
+import userEvent from "@testing-library/user-event";
+import { beforeEach, vi } from "vitest";
import { useSessionStore } from "../store/sessionStore";
import { CentralStatus } from "./CentralStatus";
beforeEach(() => useSessionStore.getState().resetSession());
+test("the working activity icon opens the model-activity panel", async () => {
+ const onOpenActivity = vi.fn();
+ useSessionStore.getState().applyEvent({ type: "text_delta", text: "reading the schema" });
+ render();
+ await userEvent.click(screen.getByRole("button", { name: /model activity/i }));
+ expect(onOpenActivity).toHaveBeenCalledTimes(1);
+});
+
test("renders nothing when not working and there is no user entry or step messages", () => {
const { container } = render();
expect(container).toBeEmptyDOMElement();
diff --git a/frontend/src/shell/CentralStatus.tsx b/frontend/src/shell/CentralStatus.tsx
index 7d24b9b5..c45f8e78 100644
--- a/frontend/src/shell/CentralStatus.tsx
+++ b/frontend/src/shell/CentralStatus.tsx
@@ -1,5 +1,6 @@
import { useEffect, useReducer, useState } from "react";
import { useSessionStore } from "../store/sessionStore";
+import { WorkingSpinner } from "./WorkingSpinner";
/** Last few words of a string — a compact "what the model is doing now" tail. */
function lastWords(s: string, n = 8): string {
@@ -11,7 +12,13 @@ function lastWords(s: string, n = 8): string {
* (elapsed time + a short tail of the model stream) while the model is busy, and the
* gate's curated messages for the current step. The full verbose stream lives in the
* left Model-activity panel. */
-export function CentralStatus({ working }: { working: boolean }) {
+export function CentralStatus({
+ working,
+ onOpenActivity,
+}: {
+ working: boolean;
+ onOpenActivity?: () => void;
+}) {
const lastUserEntry = useSessionStore((s) => s.lastUserEntry);
const stepMessages = useSessionStore((s) => s.stepMessages);
const transcript = useSessionStore((s) => s.transcript);
@@ -49,7 +56,15 @@ export function CentralStatus({ working }: { working: boolean }) {
{working && (
-
+
{elapsed}s
{tail && · {tail}}
ThothII is working
diff --git a/frontend/src/shell/ModelActivityPanel.test.tsx b/frontend/src/shell/ModelActivityPanel.test.tsx
index 8e0e0b1b..b6b104a4 100644
--- a/frontend/src/shell/ModelActivityPanel.test.tsx
+++ b/frontend/src/shell/ModelActivityPanel.test.tsx
@@ -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();
+ 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();
+ await userEvent.click(screen.getByRole("button", { name: /show more/i }));
+ expect(screen.getByText("line-01")).toBeInTheDocument();
+});
diff --git a/frontend/src/shell/ModelActivityPanel.tsx b/frontend/src/shell/ModelActivityPanel.tsx
index 3052d324..d6ce8ac6 100644
--- a/frontend/src/shell/ModelActivityPanel.tsx
+++ b/frontend/src/shell/ModelActivityPanel.tsx
@@ -1,20 +1,57 @@
-import { X } from "lucide-react";
-import { Transcript } from "./Transcript";
+import { useState } from "react";
+import { ChevronDown, ChevronUp, X } from "lucide-react";
+import { useSessionStore } from "../store/sessionStore";
import { Button } from "../components/ui/button";
-/** Left drawer showing the active session's streamed model text ("model activity"),
- * opened on demand from the work-in-progress icon. */
+const TAIL_LINES = 5;
+
+/** Left drawer showing "what's happening": the tail of the model's streamed output.
+ * Collapsed it shows the last few lines (refreshing as the stream grows); the expand
+ * toggle reveals the full stream. Opened on demand from the rotating activity icon. */
export function ModelActivityPanel({ onClose }: { onClose: () => void }) {
+ const transcript = useSessionStore((s) => s.transcript);
+ const [expanded, setExpanded] = useState(false);
+
+ const lines = transcript
+ .map((e) => e.text)
+ .join("\n")
+ .split("\n")
+ .map((l) => l.trimEnd())
+ .filter((l) => l.trim() !== "");
+ const shown = expanded ? lines : lines.slice(-TAIL_LINES);
+ const canExpand = lines.length > TAIL_LINES;
+
return (
);
diff --git a/frontend/src/shell/Transcript.tsx b/frontend/src/shell/Transcript.tsx
deleted file mode 100644
index b1751e8e..00000000
--- a/frontend/src/shell/Transcript.tsx
+++ /dev/null
@@ -1,28 +0,0 @@
-// frontend/src/shell/Transcript.tsx
-import ReactMarkdown from "react-markdown";
-import remarkGfm from "remark-gfm";
-import { useSessionStore } from "../store/sessionStore";
-
-/**
- * Renders the streamed assistant conversation. The store accumulates assistant
- * text deltas in `transcript`; this is the surface that shows them.
- */
-export function Transcript() {
- const transcript = useSessionStore((s) => s.transcript);
- if (transcript.length === 0) return null;
-
- return (
-