Files
ThothII/docs/superpowers/plans/2026-07-15-central-live-log-cte-density.md
T

792 lines
33 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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(<ModelActivityPanel onClose={vi.fn()} />);
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(<ModelActivityPanel onClose={vi.fn()} />);
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(<ModelActivityPanel onClose={onClose} />);
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(<ModelActivityPanel onClose={vi.fn()} />);
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(<ModelActivityPanel onClose={vi.fn()} />);
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(<ModelActivityPanel onClose={vi.fn()} />);
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(<CentralStatus working />);
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(<CentralStatus working />);
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(<CentralStatus working={working} />);
expect(container).toBeEmptyDOMElement();
});
test("bottom-follows appended transcript text only while near the bottom", () => {
useSessionStore.setState({ transcript: [{ role: "assistant", text: "First" }] });
render(<CentralStatus working />);
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(<CentralStatus working />);
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<HTMLElement, "scrollHeight" | "clientHeight" | "scrollTop">,
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<ActivityKind> = 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<HTMLOListElement>(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 (
<ol
ref={scrollRef}
role="log"
aria-label="Live model activity"
aria-live="polite"
aria-relevant="additions text"
tabIndex={0}
className="max-h-28 overflow-y-auto rounded-lg border border-border/60 bg-muted/40 px-3 py-2 outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring/40"
onScroll={(event) => {
followRef.current = isNearBottom(event.currentTarget);
}}
>
{rows.map((line, index) => {
const isLast = index === rows.length - 1;
return (
<li
key={index}
className={[
"flex items-baseline gap-2 font-mono text-[0.72rem] leading-relaxed",
isLast ? "text-foreground/80" : "text-muted-foreground",
].join(" ")}
>
<span
aria-hidden
className={isLast ? "shrink-0 select-none text-primary" : "shrink-0 select-none text-muted-foreground/35"}
>
›
</span>
<span className="min-w-0 flex-1 whitespace-pre-wrap break-words">{line}</span>
</li>
);
})}
</ol>
);
}
```
In `AppShell.tsx`, call only:
```tsx
<CentralStatus working={working} />
```
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(<CtePlanViewer plan={plan} />);
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(<CtePlanViewer plan={plan} />);
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
<CardHeader className="gap-3 rounded-t-lg border-b border-border/60 bg-muted/15 px-3 py-2 sm:px-4 [&.border-b]:pb-2">
// CTE content
<CardContent className="flex min-w-0 flex-col gap-5 px-3 py-2 sm:px-4">
// 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
<section className="min-w-0 border-t border-border/60 pt-2">
```
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
frontend_log_patterns=$(docker logs --since 10m --tail 200 thothii-frontend-1 2>&1 | \
rg -i -c 'fatal|emerg|panic|authorization:|api[_-]?key|password|secret' || true)
printf 'frontend_log_patterns=%s\n' "${frontend_log_patterns:-0}"
```
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.