diff --git a/frontend/src/shell/SessionDocumentsPanel.test.tsx b/frontend/src/shell/SessionDocumentsPanel.test.tsx
new file mode 100644
index 00000000..74d2e1eb
--- /dev/null
+++ b/frontend/src/shell/SessionDocumentsPanel.test.tsx
@@ -0,0 +1,45 @@
+import { render, screen } from "@testing-library/react";
+import { http, HttpResponse } from "msw";
+import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
+import { server } from "../test/msw";
+import { SessionDocumentsPanel } from "./SessionDocumentsPanel";
+import type { SessionSummary } from "../api/types";
+
+function makeClient() {
+ return new QueryClient({ defaultOptions: { queries: { retry: false } } });
+}
+
+function wrap(ui: React.ReactElement, client = makeClient()) {
+ const Wrapper = ({ children }: { children: React.ReactNode }) => (
+ {children}
+ );
+ return render(ui, { wrapper: Wrapper });
+}
+
+const base: SessionSummary = {
+ id: "s1", status: "open", question: "How many ablations?", summary: null,
+ created_at: "2026-01-01T00:00:00Z", updated_at: null, author: null,
+ name: null, group: null, archived: false,
+};
+
+beforeEach(() => {
+ server.use(http.get("http://localhost:8787/sessions/s1/documents", () =>
+ HttpResponse.json([
+ { phase: "—", key: "question", title: "Original question", format: "text", content: "How many ablations?" },
+ { phase: "F7", key: "sql", title: "Final SQL", format: "sql", content: "SELECT 1" },
+ ]),
+ ));
+});
+
+test("renders the document titles from the API", async () => {
+ wrap();
+ expect(await screen.findByText("Original question")).toBeInTheDocument();
+ expect(screen.getByText("Final SQL")).toBeInTheDocument();
+});
+
+test("shows Resume for resumable, hides it for finalized", async () => {
+ const { rerender } = wrap();
+ expect(await screen.findByRole("button", { name: /resume/i })).toBeInTheDocument();
+ rerender();
+ expect(screen.queryByRole("button", { name: /resume/i })).not.toBeInTheDocument();
+});
diff --git a/frontend/src/shell/SessionDocumentsPanel.tsx b/frontend/src/shell/SessionDocumentsPanel.tsx
new file mode 100644
index 00000000..0aec5395
--- /dev/null
+++ b/frontend/src/shell/SessionDocumentsPanel.tsx
@@ -0,0 +1,94 @@
+import { useQuery } from "@tanstack/react-query";
+import { X } from "lucide-react";
+import { getSessionDocuments } from "../api/sessions";
+import type { SessionDocument, SessionSummary } from "../api/types";
+import { Button } from "../components/ui/button";
+import { SqlViewer } from "../viewers/SqlViewer";
+import { SchemaLinkingViewer } from "../viewers/SchemaLinkingViewer";
+import { MarkdownView } from "../viewers/MarkdownView";
+
+interface Props {
+ session: SessionSummary;
+ onClose: () => void;
+ onResume: (id: string) => void;
+}
+
+function statusLabel(s: SessionSummary): string {
+ if (s.archived) return "Archived";
+ if (s.status === "finalized") return "Completed";
+ return `In progress · ${s.status}`;
+}
+
+function DocBody({ doc }: { doc: SessionDocument }) {
+ if (doc.format === "sql") return ;
+ if (doc.format === "markdown") return ;
+ if (doc.format === "schema-linking") {
+ try {
+ const linking = JSON.parse(doc.content);
+ return ;
+ } catch {
+ return
{doc.content};
+ }
+ }
+ if (doc.format === "decisions") {
+ const lines = doc.content.split("\n").filter(Boolean);
+ return (
+
+ {`${lines.length} decisions`}
+
+ {lines.map((line, i) => {
+ let d: { type?: string; subject?: string; detail?: string };
+ try { d = JSON.parse(line); } catch { d = {}; }
+ return - [{d.type}] {d.subject}{d.detail ? ` — ${d.detail}` : ""}
;
+ })}
+
+
+ );
+ }
+ return {doc.content}
;
+}
+
+export function SessionDocumentsPanel({ session, onClose, onResume }: Props) {
+ const { data: docs = [], isLoading } = useQuery({
+ queryKey: ["session-documents", session.id],
+ queryFn: () => getSessionDocuments(session.id),
+ });
+ const resumable = session.status !== "finalized" && !session.archived;
+
+ return (
+
+ );
+}