diff --git a/frontend/src/shell/CentralStatus.test.tsx b/frontend/src/shell/CentralStatus.test.tsx
new file mode 100644
index 00000000..b72f8dfe
--- /dev/null
+++ b/frontend/src/shell/CentralStatus.test.tsx
@@ -0,0 +1,20 @@
+import { render, screen } from "@testing-library/react";
+import { beforeEach } from "vitest";
+import { useSessionStore } from "../store/sessionStore";
+import { CentralStatus } from "./CentralStatus";
+
+beforeEach(() => useSessionStore.getState().resetSession());
+
+test("renders nothing when there is no user entry and no step messages", () => {
+ const { container } = render();
+ expect(container).toBeEmptyDOMElement();
+});
+
+test("echoes the last user entry and the step messages", () => {
+ const st = useSessionStore.getState();
+ st.setLastUserEntry({ kind: "input", text: "how many patients?" });
+ st.applyEvent({ type: "info", level: "info", text: "Searching the schema…" });
+ render();
+ expect(screen.getByText("how many patients?")).toBeInTheDocument();
+ expect(screen.getByText("Searching the schema…")).toBeInTheDocument();
+});
diff --git a/frontend/src/shell/CentralStatus.tsx b/frontend/src/shell/CentralStatus.tsx
new file mode 100644
index 00000000..ffc7dcc4
--- /dev/null
+++ b/frontend/src/shell/CentralStatus.tsx
@@ -0,0 +1,35 @@
+import { useSessionStore } from "../store/sessionStore";
+
+/** The minimal central view: the user's last input/choice + the gate's curated
+ * messages for the current step. The verbose model stream lives in the left panel. */
+export function CentralStatus() {
+ const lastUserEntry = useSessionStore((s) => s.lastUserEntry);
+ const stepMessages = useSessionStore((s) => s.stepMessages);
+ if (!lastUserEntry && stepMessages.length === 0) return null;
+
+ return (
+
+ {lastUserEntry && (
+
+
+ {lastUserEntry.kind === "input" ? "You asked" : "You chose"}
+
+
{lastUserEntry.text}
+
+ )}
+ {stepMessages.map((m, i) => (
+
+ {m.text}
+
+ ))}
+
+ );
+}
diff --git a/frontend/src/shell/ModelActivityPanel.test.tsx b/frontend/src/shell/ModelActivityPanel.test.tsx
new file mode 100644
index 00000000..8e0e0b1b
--- /dev/null
+++ b/frontend/src/shell/ModelActivityPanel.test.tsx
@@ -0,0 +1,20 @@
+import { render, screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { beforeEach, vi } from "vitest";
+import { useSessionStore } from "../store/sessionStore";
+import { ModelActivityPanel } from "./ModelActivityPanel";
+
+beforeEach(() => useSessionStore.getState().resetSession());
+
+test("renders the streamed model transcript", () => {
+ useSessionStore.getState().applyEvent({ type: "text_delta", text: "Promoting table dim_patient." });
+ render();
+ expect(screen.getByText(/Promoting table dim_patient/)).toBeInTheDocument();
+});
+
+test("close button calls onClose", async () => {
+ const onClose = vi.fn();
+ render();
+ await userEvent.click(screen.getByRole("button", { name: /close/i }));
+ expect(onClose).toHaveBeenCalled();
+});
diff --git a/frontend/src/shell/ModelActivityPanel.tsx b/frontend/src/shell/ModelActivityPanel.tsx
new file mode 100644
index 00000000..3052d324
--- /dev/null
+++ b/frontend/src/shell/ModelActivityPanel.tsx
@@ -0,0 +1,21 @@
+import { X } from "lucide-react";
+import { Transcript } from "./Transcript";
+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. */
+export function ModelActivityPanel({ onClose }: { onClose: () => void }) {
+ return (
+
+ );
+}