diff --git a/docs/superpowers/plans/2026-07-15-central-live-log-cte-density.md b/docs/superpowers/plans/2026-07-15-central-live-log-cte-density.md new file mode 100644 index 00000000..fe71b8fb --- /dev/null +++ b/docs/superpowers/plans/2026-07-15-central-live-log-cte-density.md @@ -0,0 +1,790 @@ +# Central Live Log and Compact CTE Plan Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Deduplicate the live workflow UI so the center contains only a compact scrolling assistant-stream log plus reviewer forms/artifacts, the left panel contains only thinking/status, and F6 CTE cards use dense vertical padding with moderate responsive lateral padding. + +**Architecture:** Keep the complete Zustand folds and all backend/SSE contracts unchanged. Apply two rendering projections: `CentralStatus` derives a bounded chronological log from `transcript`, while `ModelActivityPanel` default-denies every kind except `thinking` and `status`. Reuse one pure near-bottom helper for both scroll surfaces, and limit CTE spacing changes to `CtePlanViewer` rather than the shared card primitive. + +**Tech Stack:** React 18, TypeScript, Zustand, Tailwind CSS 3.4, React Testing Library, Vitest, Docker Compose. + +## Global Constraints + +- `activityLog`, `transcript`, `lastUserEntry`, `stepMessages`, store event folds, backend, SSE/replay, workflow, persistence, and provider contracts remain unchanged. +- The central body renders only the compact assistant-transcript live log while `working` plus the existing `WidgetHost`, artifact viewers, finalized-session card, and composer owned by `AppShell`. +- The central log contains every non-blank line from all assistant transcript entries in chronological order; it never contains raw tool or lifecycle events. +- The central log is absent when `working` is false or the transcript has no non-blank content; do not synthesize waiting/status copy. +- The central log uses a bounded scroll viewport, wraps rather than truncates, has accessible log semantics, follows only near the bottom, and preserves manual scroll position. +- `CentralStatus` must not render last-user echo, spinner, timer, working label, or `stepMessages`. +- The left panel allowlist is exactly `thinking` and `status`; `prompt`, `gate`, `assistant`, `tool`, `lifecycle`, and unknown future kinds are hidden. +- Panel filtering stays at the rendering boundary. Empty state and auto-scroll derive only from visible `thinking`/`status` entries. +- CTE header, content, table rows, and filter rows use 8 px vertical padding, 12 px lateral padding below `sm`, and 16 px lateral padding from `sm` upward. +- The bordered CTE header explicitly overrides the shared `CardHeader` bottom padding with `[&.border-b]:pb-2`; rationale/divider top padding is 8 px. +- Preserve CTE grids, gaps, borders, headings, badges, chips, wrapping, `min-w-0`, ordered-list semantics, roles, ARIA labels, typography, and colors. +- UI strings remain English. Do not add dependencies, preferences, toggles, text heuristics, or unrelated refactors. +- Preserve the user-owned untracked `.vite/` tree: do not add, delete, clean, or commit it. +- In an isolated worktree, if `deploy/thothii.env` is absent, create only an ignored symlink to + `/home/chirone/ThothII/deploy/thothii.env`; never read, copy, print, stage, or commit its contents. +- Deployment may rebuild/recreate only `frontend`; restart only `omics_portal-web-1` if the Vite entry changes. Never restart `core`, mutate settings/sessions, or terminate an unrelated Pi process. + +--- + +### Task 1: Central live log and non-duplicating activity panel + +**Files:** +- Create: `frontend/src/shell/activityScroll.ts` +- Modify: `frontend/src/shell/ModelActivityPanel.tsx:1-141` +- Modify: `frontend/src/shell/CentralStatus.tsx:1-112` +- Modify: `frontend/src/shell/AppShell.tsx:283-295,405-414` +- Test: `frontend/src/shell/ModelActivityPanel.test.tsx` +- Test: `frontend/src/shell/CentralStatus.test.tsx` +- Test: `frontend/src/shell/AppShell.new-session.test.tsx:48-78` +- Test: `frontend/src/shell/AppShell.session-mgmt.test.tsx:691-731` + +**Interfaces:** +- Consumes: unchanged `useSessionStore` fields `activityLog`, `transcript`, `pendingWidget`, and `agentActive`. +- Produces: `isNearBottom(el, threshold?)` from `activityScroll.ts`; `isVisibleModelActivity(entry)` allowing only `thinking`/`status`; `CentralStatus({ working }: { working: boolean })` rendering the central log. + +- [ ] **Step 1: Write failing panel projection tests** + +Update the mixed-sequence and allowlist cases in `ModelActivityPanel.test.tsx` so their essential assertions are: + +```tsx +test("renders only thinking and status from a mixed F1 sequence", () => { + const store = useSessionStore.getState(); + store.setPhase("F1"); + store.setLastUserEntry({ kind: "input", text: "How many patients?" }); + store.applyEvent({ type: "system_event", event: "agent_start" }); + store.applyEvent({ + type: "activity_event", + activity: { kind: "tool", toolCallId: "tool-1", toolName: "bash", status: "running" }, + }); + store.applyEvent({ type: "text_delta", text: "Let me run another bash search." }); + store.applyEvent({ type: "activity_delta", text: "Inspecting **schema**." }); + store.applyEvent({ type: "info", level: "warning", text: "Retrying schema lookup" }); + store.applyEvent({ + type: "ui_request", + ui_request: { id: "gate-1", widget: "select", phase: "F1_review", title: "Confirm cohort" }, + }); + + render(); + + expect(screen.getAllByRole("article")).toHaveLength(2); + expect(screen.getByText("schema").tagName).toBe("STRONG"); + expect(screen.getByText("Retrying schema lookup")).toBeInTheDocument(); + expect(screen.queryByText("How many patients?")).not.toBeInTheDocument(); + expect(screen.queryByText("Confirm cohort")).not.toBeInTheDocument(); + expect(screen.queryByText("bash")).not.toBeInTheDocument(); + expect(screen.queryByText("Let me run another bash search.")).not.toBeInTheDocument(); + expect(useSessionStore.getState().transcript).toEqual([ + { role: "assistant", text: "Let me run another bash search." }, + ]); +}); + +test("uses an explicit default-deny activity-kind allowlist", () => { + const predicate = (activityPanelModule as unknown as { + isVisibleModelActivity?: (entry: ActivityEntry) => boolean; + }).isVisibleModelActivity; + expect(predicate).toBeTypeOf("function"); + if (!predicate) return; + + for (const kind of ["thinking", "status"] satisfies ActivityKind[]) { + expect(predicate({ kind, phase: "F1", text: kind })).toBe(true); + } + for (const kind of ["prompt", "gate", "assistant", "tool", "lifecycle"] satisfies ActivityKind[]) { + expect(predicate({ kind, phase: "F1", text: kind })).toBe(false); + } + expect(predicate({ kind: "future-kind" as ActivityKind, phase: "F1", text: "future" })).toBe(false); +}); +``` + +Replace the hidden-only case and update the close/scroll seeds with these exact forms: + +```tsx +test("shows the empty state when the raw log contains only hidden entries", () => { + useSessionStore.setState({ + activityLog: [ + { kind: "prompt", phase: "F1", text: "How many patients?" }, + { kind: "gate", phase: "F1", text: "Confirm cohort" }, + { kind: "assistant", phase: "F1", text: "Let me try another command." }, + { kind: "tool", phase: "F1", text: "bash", toolCallId: "tool-1", status: "completed" }, + { kind: "lifecycle", phase: "F1", text: "Turn end" }, + ], + }); + + render(); + + expect(screen.getByText("No activity yet.")).toBeInTheDocument(); + expect(screen.queryAllByRole("article")).toHaveLength(0); +}); + +test("close calls onClose without mutating the activity log", async () => { + const onClose = vi.fn(); + const activityLog: ActivityEntry[] = [ + { kind: "status", phase: "F1", text: "Keep this row" }, + ]; + useSessionStore.setState({ activityLog }); + render(); + const expectedLog = useSessionStore.getState().activityLog.map((entry) => ({ ...entry })); + + await userEvent.click(screen.getByRole("button", { name: /close model activity/i })); + + expect(onClose).toHaveBeenCalledOnce(); + expect(useSessionStore.getState().activityLog).toEqual(expectedLog); +}); + +test("follows appended visible activity while near the bottom", () => { + useSessionStore.setState({ + activityLog: [{ kind: "thinking", phase: "F1", text: "First" }], + }); + render(); + const viewport = screen.getByTestId("activity-scroll"); + setScrollGeometry(viewport, { scrollHeight: 400, clientHeight: 100, scrollTop: 300 }); + + act(() => useSessionStore.getState().applyEvent({ type: "info", text: "Second" })); + + expect(viewport.scrollTop).toBe(400); + expect(viewport).toHaveAttribute("tabindex", "0"); +}); + +test("does not bottom-follow when only a hidden prompt arrives", () => { + useSessionStore.setState({ + activityLog: [{ kind: "status", phase: "F1", text: "Visible status" }], + }); + render(); + const viewport = screen.getByTestId("activity-scroll"); + setScrollGeometry(viewport, { scrollHeight: 400, clientHeight: 100, scrollTop: 300 }); + + act(() => useSessionStore.getState().setLastUserEntry({ kind: "input", text: "Hidden prompt" })); + + expect(viewport.scrollTop).toBe(300); +}); +``` + +Use this exact manual-scroll case: + +```tsx +test("preserves manual scroll position when visible activity arrives", () => { + useSessionStore.setState({ + activityLog: [{ kind: "thinking", phase: "F1", text: "First" }], + }); + render(); + const viewport = screen.getByTestId("activity-scroll"); + setScrollGeometry(viewport, { scrollHeight: 400, clientHeight: 100, scrollTop: 100 }); + fireEvent.scroll(viewport); + + act(() => useSessionStore.getState().applyEvent({ type: "info", text: "Second" })); + + expect(viewport.scrollTop).toBe(100); +}); +``` + +- [ ] **Step 2: Write failing central live-log tests** + +Replace `CentralStatus.test.tsx` with tests built around the real Zustand store and rendered component: + +```tsx +import { act, fireEvent, render, screen } from "@testing-library/react"; +import { beforeEach } from "vitest"; +import { useSessionStore } from "../store/sessionStore"; +import { CentralStatus } from "./CentralStatus"; + +beforeEach(() => useSessionStore.getState().resetSession()); + +function setScrollGeometry( + element: HTMLElement, + values: { scrollHeight: number; clientHeight: number; scrollTop: number }, +) { + Object.defineProperties(element, { + scrollHeight: { configurable: true, value: values.scrollHeight }, + clientHeight: { configurable: true, value: values.clientHeight }, + scrollTop: { configurable: true, writable: true, value: values.scrollTop }, + }); +} + +test("renders only the chronological assistant transcript log while working", () => { + useSessionStore.setState({ + lastUserEntry: { kind: "input", text: "how many patients?" }, + stepMessages: [{ level: "warning", text: "Searching the schema…" }], + transcript: [ + { role: "assistant", text: "First line\n\nSecond line" }, + { role: "assistant", text: "Third line" }, + ], + }); + + render(); + + const log = screen.getByRole("log", { name: "Live model activity" }); + expect(log).toHaveTextContent("First line"); + expect(log).toHaveTextContent("Second line"); + expect(log).toHaveTextContent("Third line"); + expect(log).toHaveClass("max-h-28", "overflow-y-auto"); + expect(screen.queryByText("how many patients?")).not.toBeInTheDocument(); + expect(screen.queryByText("Searching the schema…")).not.toBeInTheDocument(); + expect(screen.queryByRole("status")).not.toBeInTheDocument(); + expect(screen.queryByText(/\d+s/)).not.toBeInTheDocument(); +}); + +test("wraps live log lines instead of truncating them", () => { + useSessionStore.setState({ + transcript: [{ role: "assistant", text: "a_very_long_model_activity_line" }], + }); + render(); + expect(screen.getByText("a_very_long_model_activity_line")).toHaveClass( + "whitespace-pre-wrap", + "break-words", + ); + expect(screen.getByText("a_very_long_model_activity_line")).not.toHaveClass("truncate"); +}); + +test.each([ + { working: false, transcript: [{ role: "assistant" as const, text: "hidden" }] }, + { working: true, transcript: [] }, + { working: true, transcript: [{ role: "assistant" as const, text: " \n " }] }, +])("renders nothing without active non-blank transcript content", ({ working, transcript }) => { + useSessionStore.setState({ transcript }); + const { container } = render(); + expect(container).toBeEmptyDOMElement(); +}); + +test("bottom-follows appended transcript text only while near the bottom", () => { + useSessionStore.setState({ transcript: [{ role: "assistant", text: "First" }] }); + render(); + const log = screen.getByRole("log", { name: "Live model activity" }); + setScrollGeometry(log, { scrollHeight: 400, clientHeight: 100, scrollTop: 300 }); + + act(() => useSessionStore.getState().applyEvent({ type: "text_delta", text: " second" })); + + expect(log.scrollTop).toBe(400); +}); + +test("preserves manual live-log scroll position away from the bottom", () => { + useSessionStore.setState({ transcript: [{ role: "assistant", text: "First" }] }); + render(); + const log = screen.getByRole("log", { name: "Live model activity" }); + setScrollGeometry(log, { scrollHeight: 400, clientHeight: 100, scrollTop: 100 }); + fireEvent.scroll(log); + + act(() => useSessionStore.getState().applyEvent({ type: "text_delta", text: " second" })); + + expect(log.scrollTop).toBe(100); +}); +``` + +- [ ] **Step 3: Write failing AppShell integration assertions** + +In `AppShell.new-session.test.tsx`, import `act`, rename the creation test, and replace its central-body assertions with: + +```tsx +expect(screen.queryByText("How many patients?", { selector: "p" })).not.toBeInTheDocument(); +expect(screen.queryByText("Creating session…")).not.toBeInTheDocument(); +expect(screen.queryByText(/\d+s/)).not.toBeInTheDocument(); +expect(useSessionStore.getState().activityLog[0]).toEqual({ + kind: "prompt", + phase: "F1", + text: "How many patients?", +}); + +releaseCreate(); +await waitFor(() => expect(FakeEventSource.instances).toHaveLength(1)); +act(() => FakeEventSource.instances[0].emit({ type: "text_delta", text: "Inspecting cohort" })); +expect(await screen.findByRole("log", { name: "Live model activity" })) + .toHaveTextContent("Inspecting cohort"); +expect(screen.queryByText("Analyzing question…")).not.toBeInTheDocument(); +expect(useSessionStore.getState().currentPhase).toBe("F1"); +``` + +In the close/reopen activity test in `AppShell.session-mgmt.test.tsx`, use this event sequence and +panel contract before closing: + +```tsx +act(() => { + const store = useSessionStore.getState(); + store.setPhase("F4"); + store.setLastUserEntry({ kind: "input", text: "Inspect patient cohort" }); + store.applyEvent({ + type: "activity_event", + activity: { kind: "tool", toolCallId: "tool-1", toolName: "bash", status: "completed" }, + }); + store.applyEvent({ type: "text_delta", text: "Cohort ready" }); + store.applyEvent({ type: "activity_delta", text: "Inspecting **cohort**" }); + store.applyEvent({ type: "info", level: "info", text: "Cohort status" }); +}); +const beforeClose = useSessionStore.getState().activityLog.map((entry) => ({ ...entry })); + +await userEvent.click(screen.getByRole("button", { name: /model activity/i })); +const panel = (await screen.findByRole("heading", { name: /model activity/i })).closest("aside"); +expect(panel).not.toBeNull(); +expect(within(panel!).getByText("cohort").tagName).toBe("STRONG"); +expect(within(panel!).getByText("Cohort status")).toBeInTheDocument(); +expect(within(panel!).queryByText("Inspect patient cohort")).not.toBeInTheDocument(); +expect(within(panel!).queryByText("bash")).not.toBeInTheDocument(); +expect(within(panel!).queryByText("Cohort ready")).not.toBeInTheDocument(); +``` + +Close and reopen with these exact assertions: + +```tsx +await userEvent.click(screen.getByRole("button", { name: /close model activity/i })); +expect(screen.queryByRole("heading", { name: /model activity/i })).not.toBeInTheDocument(); +expect(useSessionStore.getState().activityLog).toEqual(beforeClose); + +await userEvent.click(screen.getByRole("button", { name: /show model activity/i })); +const reopened = (await screen.findByRole("heading", { name: /model activity/i })).closest("aside"); +expect(reopened).not.toBeNull(); +expect(within(reopened!).getByText("cohort").tagName).toBe("STRONG"); +expect(within(reopened!).getByText("Cohort status")).toBeInTheDocument(); +expect(within(reopened!).queryByText("Inspect patient cohort")).not.toBeInTheDocument(); +expect(within(reopened!).queryByText("bash")).not.toBeInTheDocument(); +expect(within(reopened!).queryByText("Cohort ready")).not.toBeInTheDocument(); +expect(useSessionStore.getState().activityLog).toEqual(beforeClose); +``` + +- [ ] **Step 4: Run the focused tests and verify RED** + +Run: + +```bash +cd frontend +npx vitest run \ + src/shell/ModelActivityPanel.test.tsx \ + src/shell/CentralStatus.test.tsx \ + src/shell/AppShell.new-session.test.tsx \ + src/shell/AppShell.session-mgmt.test.tsx +``` + +Expected: failures show the old four-kind panel allowlist, prompt/status central rendering, missing live-log semantics/scrolling, and old creation labels. There must be no syntax/setup failure. + +- [ ] **Step 5: Extract the shared near-bottom predicate** + +Create `frontend/src/shell/activityScroll.ts`: + +```ts +export function isNearBottom( + el: Pick, + threshold = 48, +): boolean { + return el.scrollHeight - el.clientHeight - el.scrollTop <= threshold; +} +``` + +Import it from both `ModelActivityPanel.tsx` and `CentralStatus.tsx`. Update +`ModelActivityPanel.test.tsx` to import `isNearBottom` from `./activityScroll`. Do not change the +48 px boundary assertions. + +- [ ] **Step 6: Implement the strict panel allowlist** + +Replace the visible-kind set in `ModelActivityPanel.tsx` with: + +```ts +const VISIBLE_MODEL_ACTIVITY_KINDS: ReadonlySet = new Set([ + "thinking", + "status", +]); +``` + +Leave `activityLog` ingestion, `visibleActivity`, empty state, visible-tail dependencies, Markdown, +warning/error severity text, close behavior, and panel accessibility unchanged. + +- [ ] **Step 7: Implement the central live log** + +Replace `CentralStatus.tsx` with: + +```tsx +import { useLayoutEffect, useRef } from "react"; +import { useSessionStore } from "../store/sessionStore"; +import { isNearBottom } from "./activityScroll"; + +function transcriptLines(transcript: Array<{ text: string }>): string[] { + return transcript.flatMap(({ text }) => + text + .split("\n") + .map((line) => line.trimEnd()) + .filter((line) => line.trim() !== ""), + ); +} + +/** Compact central projection of the assistant stream while the model is working. */ +export function CentralStatus({ working }: { working: boolean }) { + const transcript = useSessionStore((state) => state.transcript); + const rows = transcriptLines(transcript); + const rowCount = rows.length; + const tail = rows.at(-1); + const scrollRef = useRef(null); + const followRef = useRef(true); + + useLayoutEffect(() => { + const viewport = scrollRef.current; + if (viewport && followRef.current) viewport.scrollTop = viewport.scrollHeight; + }, [rowCount, tail]); + + if (!working || rows.length === 0) return null; + + return ( +
    { + followRef.current = isNearBottom(event.currentTarget); + }} + > + {rows.map((line, index) => { + const isLast = index === rows.length - 1; + return ( +
  1. + + › + + {line} +
  2. + ); + })} +
+ ); +} +``` + +In `AppShell.tsx`, call only: + +```tsx + +``` + +Remove the stale `workingLabel` prop and update comments that still describe a spinner/timer. + +- [ ] **Step 8: Run focused and adjacent tests GREEN** + +Run: + +```bash +cd frontend +npx vitest run \ + src/shell/ModelActivityPanel.test.tsx \ + src/shell/CentralStatus.test.tsx \ + src/shell/AppShell.new-session.test.tsx \ + src/shell/AppShell.session-mgmt.test.tsx \ + src/store/sessionStore.test.ts \ + src/stream/useSessionStream.test.tsx +npx tsc -b +``` + +Expected: every focused/adjacent test passes, the raw store-fold tests remain unchanged, and +TypeScript exits 0. Existing MSW/React warnings may remain; no new warning is acceptable. + +- [ ] **Step 9: Review and commit Task 1** + +Run: + +```bash +git diff --check +git status --short +git diff -- frontend/src/shell/activityScroll.ts \ + frontend/src/shell/ModelActivityPanel.tsx \ + frontend/src/shell/ModelActivityPanel.test.tsx \ + frontend/src/shell/CentralStatus.tsx \ + frontend/src/shell/CentralStatus.test.tsx \ + frontend/src/shell/AppShell.tsx \ + frontend/src/shell/AppShell.new-session.test.tsx \ + frontend/src/shell/AppShell.session-mgmt.test.tsx +git add frontend/src/shell/activityScroll.ts \ + frontend/src/shell/ModelActivityPanel.tsx \ + frontend/src/shell/ModelActivityPanel.test.tsx \ + frontend/src/shell/CentralStatus.tsx \ + frontend/src/shell/CentralStatus.test.tsx \ + frontend/src/shell/AppShell.tsx \ + frontend/src/shell/AppShell.new-session.test.tsx \ + frontend/src/shell/AppShell.session-mgmt.test.tsx +git commit -m "fix(frontend): deduplicate live workflow activity" +``` + +Expected: only the listed frontend files enter the Task 1 commit; `.vite/` remains untracked. + +--- + +### Task 2: Compact vertical spacing for F6 CTE cards + +**Files:** +- Modify: `frontend/src/viewers/CtePlanViewer.tsx:14-166` +- Test: `frontend/src/viewers/CtePlanViewer.test.tsx:71-166` + +**Interfaces:** +- Consumes: unchanged `CtePlanV2`, `CtePlanCte`, `CtePlanFilter`, shared `Card`, `CardHeader`, and `CardContent`. +- Produces: the same `CtePlanViewer({ plan })` DOM/semantic contract with exact compact spacing utilities. + +- [ ] **Step 1: Write failing exact-spacing tests** + +Replace the padding test in `CtePlanViewer.test.tsx` with: + +```tsx +test("uses compact vertical and responsive lateral CTE card padding", () => { + const { container } = render(); + + for (const header of container.querySelectorAll('[data-slot="card-header"]')) { + expect(header).toHaveClass("px-3", "py-2", "sm:px-4", "[&.border-b]:pb-2"); + expect(header).not.toHaveClass("p-4", "sm:p-5", "sm:[&.border-b]:pb-5"); + } + for (const content of container.querySelectorAll('[data-slot="card-content"]')) { + expect(content).toHaveClass("px-3", "py-2", "sm:px-4"); + expect(content).not.toHaveClass("p-4", "sm:p-5"); + } + for (const card of container.querySelectorAll('[data-slot="card"]')) { + expect(card).toHaveClass("shadow-none"); + } +}); +``` + +Update the filter-row test to require `px-3 py-2 sm:px-4` and reject `p-3 sm:p-4`. Add: + +```tsx +test("uses the same compact padding for table rows and rationale dividers", () => { + render(); + const card = getCteCard("pazienti_idonei"); + const tableName = within(card).getByText("pazienti"); + expect(tableName.parentElement).toHaveClass("px-3", "py-2", "sm:px-4"); + expect(tableName.parentElement).not.toHaveClass("p-3", "sm:p-4"); + + const filterRationale = within(card).getByText("Solo pazienti in follow-up").parentElement; + expect(filterRationale).toHaveClass("pt-2"); + expect(filterRationale).not.toHaveClass("pt-3"); + + const cteRationale = within(card) + .getByText("Base della catena: riduce il volume prima dei join") + .closest("section"); + expect(cteRationale).toHaveClass("border-t", "pt-2"); + expect(cteRationale).not.toHaveClass("pt-4"); +}); +``` + +- [ ] **Step 2: Run the CTE viewer test and verify RED** + +Run: + +```bash +cd frontend +npx vitest run src/viewers/CtePlanViewer.test.tsx +``` + +Expected: exact class assertions fail against the current 16/20 px padding and `pt-3`/`pt-4` dividers. + +- [ ] **Step 3: Implement compact spacing without changing structure** + +Apply these exact replacements in `CtePlanViewer.tsx`: + +```tsx +// Filter row +className="min-w-0 space-y-3 px-3 py-2 sm:px-4" + +// Filter description/rationale divider +className="grid min-w-0 gap-x-4 gap-y-2 border-t border-border/50 pt-2 md:grid-cols-[5.5rem_minmax(0,1fr)]" + +// CTE header + + +// CTE content + + +// Table row +className="grid min-w-0 gap-1 px-3 py-2 sm:px-4 md:grid-cols-[minmax(10rem,0.8fr)_minmax(0,1.2fr)] md:gap-4" + +// CTE rationale +
+``` + +Do not change any `gap-*` outside these snippets, grid templates, content, labels, typography, +wrapping, roles, or semantic elements. + +- [ ] **Step 4: Run CTE tests, TypeScript, and Impeccable layout scan GREEN** + +Run: + +```bash +cd frontend +npx vitest run src/viewers/CtePlanViewer.test.tsx src/viewers/ArtifactView.test.tsx +npx tsc -b +node /home/admlocforn1/.codex/skills/impeccable/scripts/detect.mjs \ + --json --scope layout src/viewers/CtePlanViewer.tsx +``` + +Expected: tests and TypeScript pass; detector output is exactly `[]`. Long identifier/wrapping tests +remain green. + +- [ ] **Step 5: Review and commit Task 2** + +Run: + +```bash +git diff --check +git diff -- frontend/src/viewers/CtePlanViewer.tsx frontend/src/viewers/CtePlanViewer.test.tsx +git add frontend/src/viewers/CtePlanViewer.tsx frontend/src/viewers/CtePlanViewer.test.tsx +git commit -m "style(frontend): compact CTE plan spacing" +``` + +Expected: the commit contains only the viewer and its test; shared `card.tsx` is untouched. + +--- + +### Task 3: Full verification, frontend-only deployment, and durable state + +**Files:** +- Modify: `brain/codebase/workflow-ui-contracts.md:26-30` +- Modify: `PROJECT_STATE.md` + +**Interfaces:** +- Consumes: Task 1 central/panel projections, Task 2 compact CTE classes, the existing root Compose frontend service, and the running portal manifest cache. +- Produces: a running frontend image/entry with unchanged core runtime plus durable documentation of the new UI contracts and observed verification evidence. + +- [ ] **Step 1: Run the complete source verification gate** + +Run: + +```bash +cd frontend +npx vitest run +npx tsc -b +npm run build +node /home/admlocforn1/.codex/skills/impeccable/scripts/detect.mjs --json --scope layout \ + src/shell/CentralStatus.tsx src/shell/ModelActivityPanel.tsx src/viewers/CtePlanViewer.tsx +cd .. +git diff --check +git status --short --untracked-files=all +``` + +Expected: all frontend tests pass, TypeScript/build exit 0, detector output is `[]`, only the two +planned documentation files remain uncommitted after Tasks 1-2, and `.vite/` is still untracked. +Record the exact passing test/file counts for `PROJECT_STATE.md`. + +- [ ] **Step 2: Record live runtime state without mutating the active session** + +Run: + +```bash +docker inspect -f '{{.Name}}|{{.State.Status}}|{{if .State.Health}}{{.State.Health.Status}}{{else}}no-healthcheck{{end}}|{{.Image}}|{{.State.StartedAt}}' \ + thothii-core-1 thothii-frontend-1 omics_portal-web-1 +docker exec thothii-frontend-1 sh -lc \ + "rg -n -B 3 '\"isEntry\": true' /usr/share/nginx/html/.vite/manifest.json 2>/dev/null || grep -n -B 3 '\"isEntry\": true' /usr/share/nginx/html/.vite/manifest.json" +docker top thothii-core-1 -eo pid,ppid,etime,args +``` + +Expected: record current core/frontend/portal image and start timestamps, current Vite entry, and +whether an unrelated Pi process is active. Do not stop it, close its session, alter settings, or run +a model smoke. + +- [ ] **Step 3: Build and recreate only frontend** + +From an isolated worktree, make the existing ignored deployment environment available without +reading or copying it, then run from the repository root: + +```bash +if [ ! -e deploy/thothii.env ]; then + ln -s /home/chirone/ThothII/deploy/thothii.env deploy/thothii.env +fi +test -e deploy/thothii.env +docker compose build frontend +docker compose up -d --no-deps --force-recreate --wait --wait-timeout 60 frontend +docker inspect -f '{{.Name}}|{{.State.Status}}|{{.Image}}|{{.State.StartedAt}}' thothii-frontend-1 +docker exec thothii-frontend-1 sh -lc \ + "rg -n -B 3 '\"isEntry\": true' /usr/share/nginx/html/.vite/manifest.json 2>/dev/null || grep -n -B 3 '\"isEntry\": true' /usr/share/nginx/html/.vite/manifest.json" +``` + +Expected: build and frontend-only recreate exit 0, frontend is running on the new image, and the +new entry is recorded. Compare it with Step 2. If the entry differs, run exactly: + +```bash +docker restart omics_portal-web-1 +docker inspect -f '{{.Name}}|{{.State.Status}}|{{.State.StartedAt}}' omics_portal-web-1 +``` + +If the entry is identical, do not restart the portal. In both branches, do not recreate/restart +core or unrelated portal services. + +- [ ] **Step 4: Verify post-deploy isolation and logs** + +Run: + +```bash +docker inspect -f '{{.Name}}|{{.State.Status}}|{{if .State.Health}}{{.State.Health.Status}}{{else}}no-healthcheck{{end}}|{{.Image}}|{{.State.StartedAt}}' \ + thothii-core-1 thothii-frontend-1 omics_portal-web-1 +docker top thothii-core-1 -eo pid,ppid,etime,args +docker logs --since 10m --tail 200 thothii-frontend-1 2>&1 | \ + rg -i -c 'fatal|emerg|panic|authorization:|api[_-]?key|password|secret' || true +``` + +Expected: core is healthy with exactly its Step 2 image/start timestamp, frontend and portal are +running, an active unrelated Pi remains untouched if it existed, and the count-only frontend log +scan reports 0. Do not print settings, session questions, raw logs, or secret values. + +- [ ] **Step 5: Update durable UI contracts and project state** + +Replace the activity paragraph in `brain/codebase/workflow-ui-contracts.md` with this exact text: + +```markdown +- `activityLog` remains the complete in-memory chronological fold of prompt, thinking, assistant, + sanitized tool lifecycle, reviewer gates, status, and turn lifecycle. The left Model activity + panel is a strict projection of only thinking and status; prompt, gate, assistant, tool, + lifecycle, and unknown future kinds are hidden. While the model is working, the central body + projects every non-blank assistant transcript line into one bounded, accessible scrolling log; + user-entry echoes, timer/spinner labels, and step messages are not rendered there. Reviewer + widgets, artifacts, store folds, and workflow state continue to consume their existing events. +- F6 CTE cards keep their existing semantic structure and responsive grids while using 8 px + vertical padding, 12 px lateral padding below `sm`, and 16 px lateral padding from `sm` upward + for headers, content, table rows, and filter rows. Divider top padding is 8 px. +``` + +Add a dated resolved-state section to `PROJECT_STATE.md` that states, in prose, all observed values +from Steps 1-4: source HEAD, exact frontend test count, TypeScript/build/detector results, new +frontend image ID, old/new Vite entry, whether portal restart occurred, unchanged core image/start +timestamp, final container state, and preservation of any unrelated Pi process. Do not claim a +live model smoke because this task intentionally performs no settings/session mutation. + +- [ ] **Step 6: Review documentation scope and commit evidence** + +Run: + +```bash +git diff --check +git status --short --untracked-files=all +git diff -- brain/codebase/workflow-ui-contracts.md PROJECT_STATE.md +git add brain/codebase/workflow-ui-contracts.md PROJECT_STATE.md +git diff --cached --check +git commit -m "docs: record central live log deployment" +``` + +Expected: only the two documentation files enter the commit; `.vite/` remains untracked and no +runtime/config/session file is staged. + +--- + +## Final review gate + +Before integration, request an independent review of the complete implementation range. The review +must verify: + +- central body contains only the assistant transcript log plus unchanged `AppShell` widgets and + artifacts; +- last-user echo, timer/spinner/label, and `stepMessages` are absent from `CentralStatus`; +- the live log contains all chronological non-blank transcript lines, wraps, uses log semantics, + follows near-bottom updates, and preserves manual scroll; +- left panel allowlists only thinking/status at its rendering boundary and preserves the complete + raw store fold; +- prompt/gate/assistant/tool/lifecycle/unknown updates cannot render or move panel scroll; +- compact CTE classes exactly match the 8 px vertical and 12/16 px lateral contract, including the + `CardHeader` bottom-padding override; +- frontend tests/typecheck/build/detector are green; `.vite/`, backend, SSE, store, workflow, core, + settings, sessions, and unrelated Pi runtime remain outside the change; +- deployment evidence matches the live frontend/core/portal containers. + +Fix every Critical or Important finding before declaring the work complete.