33 KiB
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
workingplus the existingWidgetHost, artifact viewers, finalized-session card, and composer owned byAppShell. - 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
workingis 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.
CentralStatusmust not render last-user echo, spinner, timer, working label, orstepMessages.- The left panel allowlist is exactly
thinkingandstatus;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/statusentries. - CTE header, content, table rows, and filter rows use 8 px vertical padding, 12 px lateral padding below
sm, and 16 px lateral padding fromsmupward. - The bordered CTE header explicitly overrides the shared
CardHeaderbottom 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.envis 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 onlyomics_portal-web-1if the Vite entry changes. Never restartcore, 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
useSessionStorefieldsactivityLog,transcript,pendingWidget, andagentActive. -
Produces:
isNearBottom(el, threshold?)fromactivityScroll.ts;isVisibleModelActivity(entry)allowing onlythinking/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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
<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:
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:
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, sharedCard,CardHeader, andCardContent. -
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:
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:
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:
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:
// 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:
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:
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:
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:
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:
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:
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:
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:
- `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:
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
AppShellwidgets and artifacts; - last-user echo, timer/spinner/label, and
stepMessagesare absent fromCentralStatus; - 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
CardHeaderbottom-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.