From 964bfa39ea3bae1db8df5da8dad1cf3555aa4e16 Mon Sep 17 00:00:00 2001 From: mptyl Date: Mon, 29 Jun 2026 13:05:40 +0200 Subject: [PATCH] feat(shell): add SessionDocumentsPanel read-only left drawer (Task 11) Co-Authored-By: Claude Sonnet 4.6 --- .../src/shell/SessionDocumentsPanel.test.tsx | 45 +++++++++ frontend/src/shell/SessionDocumentsPanel.tsx | 94 +++++++++++++++++++ 2 files changed, 139 insertions(+) create mode 100644 frontend/src/shell/SessionDocumentsPanel.test.tsx create mode 100644 frontend/src/shell/SessionDocumentsPanel.tsx 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 ( + + ); +}