Files
ThothII/docs/superpowers/plans/2026-07-14-workflow-ui-regressions.md
T

13 KiB

Workflow UI Regressions 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: Restore Phase 1 model activity, make join review read-only, improve CTE presentation, and prevent malformed phase summaries from crashing the UI.

Architecture: Preserve the existing Pi→backend→SSE→React pipeline while introducing one explicit activity_delta event and one explicit join-review widget contract. Enforce model-authored artifact shapes at the gate and retain defensive rendering for legacy payloads.

Tech Stack: TypeScript, Fastify, React 18, Zustand, Vitest/Testing Library, Node test runner, Pi gate extension, Docker Compose.

Global Constraints

  • UI strings remain English; persisted workspace content remains in the workspace language.
  • The harness remains the owner of workflow decisions and persistence.
  • Joins are informational and are persisted as a complete set only after Continue.
  • Other — specify is the only join-editing path and must not persist the rejected proposal.
  • F3 rewrite approval remains automatic with no reviewer widget.
  • No verbatim reasoning is persisted to session artifacts.

Task 1: Restore the Model Activity stream

Files:

  • Modify: backend/test/routes-sessions.test.ts
  • Modify: backend/test/session-bridge.test.ts
  • Modify: backend/src/routes/sessions.ts
  • Modify: backend/src/bridge/session-bridge.ts
  • Modify: frontend/src/api/types.ts
  • Modify: frontend/src/store/sessionStore.ts
  • Modify: frontend/src/store/sessionStore.test.ts
  • Modify: frontend/src/shell/ModelActivityPanel.tsx
  • Modify: frontend/src/shell/ModelActivityPanel.test.tsx

Interfaces:

  • Produces: ClientEvent/StreamEvent variant { type: "activity_delta"; text: string }.

  • Produces: Zustand activity: Entry[], consumed by ModelActivityPanel.

  • Step 1: Write backend failing tests

Add route assertions showing create passes the configured thinking level and resume passes the manifest level. Add a bridge test that emits:

rpc.emit("event", {
  type: "message_update",
  assistantMessageEvent: { type: "thinking_delta", delta: "reasoning" },
});
expect(events).toContainEqual({ type: "activity_delta", text: "reasoning" });
  • Step 2: Run backend tests and verify RED

Run: cd backend && npx vitest run test/routes-sessions.test.ts test/session-bridge.test.ts

Expected: failures show thinking is still off and no activity_delta is emitted.

  • Step 3: Implement backend event/config changes

Use thinking: s.thinking for new sessions and thinking: saved?.thinking ?? settings.thinking on resume. Extend the bridge event union and map nested thinking_delta to activity_delta without changing final text_delta behavior.

  • Step 4: Write frontend failing tests

Assert applyEvent({ type: "activity_delta", text: "reasoning" }) appends to activity and not transcript; render the activity panel after applying only activity_delta and expect the text.

  • Step 5: Run frontend tests and verify RED

Run: cd frontend && npx vitest run src/store/sessionStore.test.ts src/shell/ModelActivityPanel.test.tsx

Expected: TypeScript/test failures because the event variant and activity state do not exist.

  • Step 6: Implement frontend activity state

Add the event type, activity state, append logic mirroring streamed entry accumulation, reset it with the session, and have the panel select state.activity.

  • Step 7: Run targeted tests and typechecks

Run: cd backend && npx vitest run test/routes-sessions.test.ts test/session-bridge.test.ts && npx tsc --noEmit -p .

Run: cd frontend && npx vitest run src/store/sessionStore.test.ts src/shell/ModelActivityPanel.test.tsx && npx tsc -b

Expected: PASS.

Task 2: Replace editable join selection with read-only review

Files:

  • Modify: harness/.pi/extensions/gate/builders.js
  • Modify: harness/.pi/extensions/gate/__tests__/builders.test.js
  • Modify: harness/.pi/extensions/tht-gate.js
  • Create: harness/.pi/extensions/gate/__tests__/gate_join_review.test.js
  • Modify: harness/.pi/skills/tht-sessione/SKILL.md
  • Modify: harness/tht/decisions.py
  • Modify: harness/tht/cli/decision_cmd.py
  • Create: harness/tests/test_decision_join_set_cli.py
  • Create: frontend/src/widgets/JoinReviewWidget.tsx
  • Create: frontend/src/widgets/JoinReviewWidget.test.tsx
  • Modify: frontend/src/widgets/index.ts
  • Modify: frontend/src/api/types.ts
  • Modify: frontend/src/widgets/registry.test.tsx

Interfaces:

  • Produces: buildJoinReviewRequest({ id, phase, title, options }) with widget join-review.

  • Consumes: options { id, label, detail?, rationale? }.

  • Produces: response { id, kind: "join-review", choices: allOptionIds } on Continue.

  • Step 1: Write failing builder and gate tests

Assert the builder emits read-only option details, confirm_label: "Continue", and reserved controls. Exercise reviewer_decide with only join_modified decisions; queue a Continue response containing all ids and assert one atomic tht decision add-join-set call occurs. Queue malformed and partial responses and assert the widget is presented again. Queue control:"freetext" and assert no decision is written.

  • Step 2: Run harness tests and verify RED

Run: cd harness && node --test .pi/extensions/gate/__tests__/builders.test.js .pi/extensions/gate/__tests__/gate_join_review.test.js

Expected: builder/export/widget contract is missing and join calls still emit multiselect.

  • Step 3: Implement builder and gate routing

Add buildJoinReviewRequest. In reviewer_decide, detect a non-empty, join-only merit list:

const joinOnly = opts.length > 0 && opts.every((o) => o.decision.type === "join_modified");

Emit join-review with detail and rationale; accept only a response carrying the exact complete id set. After Continue persist all original decisions through atomic decision add-join-set, with a per-session cross-process lock covering ledger read, sequence assignment, and replacement. On free text, return feedback without persistence. Keep all other decisions on multiselect.

  • Step 4: Write failing frontend widget tests

Render two join cards and assert there are no checkboxes. Click Continue and expect all ids in the response. Open Other — specify, submit correction text, and expect a freetext control response.

  • Step 5: Run frontend widget tests and verify RED

Run: cd frontend && npx vitest run src/widgets/JoinReviewWidget.test.tsx src/widgets/registry.test.tsx

Expected: widget and registry entry are missing.

  • Step 6: Implement and register JoinReviewWidget

Render semantic cards with label, detail, and rationale, one Continue primary button, and ReservedControls. Register join-review and extend WidgetOption with optional detail and rationale strings.

  • Step 7: Update model instructions and verify targeted tests

Document that joins must be a separate join-only reviewer_decide call; the reviewer cannot remove individual joins and textual corrections require a complete revised proposal.

Run: cd harness && npm test

Run: cd frontend && npx vitest run src/widgets/JoinReviewWidget.test.tsx src/widgets/registry.test.tsx && npx tsc -b

Expected: PASS.

Task 3: Improve CTE plan layout and hide inert SQL controls

Files:

  • Modify: frontend/src/viewers/CtePlanViewer.tsx
  • Modify: frontend/src/viewers/CtePlanViewer.test.tsx
  • Modify: frontend/src/viewers/SqlViewer.tsx
  • Modify: frontend/src/viewers/SqlViewer.test.tsx
  • Modify: frontend/src/viewers/CteResultViewer.test.tsx

Interfaces:

  • SqlViewer({ blocks }) shows the layout toggle only when blocks.length > 1.

  • CTE plan data shape remains unchanged.

  • Step 1: Write failing semantic/layout tests

Assert long filter data is rendered in distinct elements labeled Column, Operator, and Value; assert card sections expose stable headings. Assert a one-block SQL viewer has no Horizontal or Vertical controls while a two-block viewer retains both.

  • Step 2: Run tests and verify RED

Run: cd frontend && npx vitest run src/viewers/CtePlanViewer.test.tsx src/viewers/SqlViewer.test.tsx src/viewers/CteResultViewer.test.tsx

Expected: structured filter labels are absent and the one-block toggle is present.

  • Step 3: Implement responsive CTE cards

Use a bordered header grid, prose blocks with leading-relaxed, metadata rows with fixed labels, break-words/font-mono for physical identifiers, and a responsive filter grid such as grid-cols-1 sm:grid-cols-[minmax(0,1fr)_auto_minmax(0,1fr)]. Keep output columns wrapping.

  • Step 4: Hide the single-block toggle

Wrap the layout control in {blocks.length > 1 && (...)} without changing multi-block state or rendering.

  • Step 5: Run targeted tests and typecheck

Run: cd frontend && npx vitest run src/viewers/CtePlanViewer.test.tsx src/viewers/SqlViewer.test.tsx src/viewers/CteResultViewer.test.tsx && npx tsc -b

Expected: PASS.

Task 4: Harden phase summaries against malformed open questions

Files:

  • Modify: harness/.pi/extensions/gate/artifact-contracts.js
  • Modify: harness/.pi/extensions/gate/__tests__/artifact_contracts.test.js
  • Modify: harness/.pi/skills/tht-sessione/SKILL.md
  • Modify: frontend/src/viewers/artifactV2.ts
  • Modify: frontend/src/viewers/PhaseSummaryViewer.tsx
  • Modify: frontend/src/viewers/PhaseSummaryViewer.test.tsx
  • Modify: frontend/src/viewers/ArtifactView.test.tsx

Interfaces:

  • Gate contract: open_questions?: string[].

  • Frontend legacy normalization: string entries pass through; objects prefer question, then label; all other values become safe text or are omitted.

  • Step 1: Write failing gate validation test

Pass the exact observed payload shape:

open_questions: [{ label: "pazienti_finale restituisce 0 righe", question: "Verificare i filtri" }]

Expect ok:false and an error naming open_questions[0].

  • Step 2: Run gate test and verify RED

Run: cd harness && node --test .pi/extensions/gate/__tests__/artifact_contracts.test.js

Expected: payload is currently accepted.

  • Step 3: Implement strict gate validation

Reject a non-array open_questions value and every non-string entry with an indexed error. Document the exact array-of-strings shape in the session skill.

  • Step 4: Write failing frontend resilience test

Render a phase-summary artifact containing the observed object and assert the screen displays Verificare i filtri and does not show the ErrorBoundary fallback.

  • Step 5: Run frontend test and verify RED

Run: cd frontend && npx vitest run src/viewers/PhaseSummaryViewer.test.tsx src/viewers/ArtifactView.test.tsx

Expected: React reports an object child/rendering failure.

  • Step 6: Implement safe legacy normalization

Add a small phaseOpenQuestionText(value: unknown): string | null helper and map/filter entries before rendering. Preserve the strict public TypeScript contract for new v2 payloads.

  • Step 7: Run targeted tests and typechecks

Run: cd harness && npm test

Run: cd frontend && npx vitest run src/viewers/PhaseSummaryViewer.test.tsx src/viewers/ArtifactView.test.tsx && npx tsc -b

Expected: PASS.

Task 5: Full verification, durable notes, and Docker deployment

Files:

  • Modify: PROJECT_STATE.md
  • Modify or create under brain/codebase/ and update brain/index.md if the vault is present.

Interfaces:

  • Running Compose services must use the newly built thothii-core:local and thothii-frontend:local image ids.

  • Step 1: Run all regression suites

Run: cd backend && npx vitest run && npx tsc --noEmit -p . && npm run build

Run: cd frontend && npx vitest run && npx tsc -b && npm run build

Run: cd harness && npm test

Expected: all pass.

  • Step 2: Update project state and durable architectural note

Record the implemented contracts, verification counts, deployment state, and the distinction between building an image and recreating a service.

  • Step 3: Build and recreate Compose services

Run: docker compose build

Run: docker compose up -d --force-recreate

Expected: both services are recreated from the new images.

  • Step 4: Verify deployed containers

Run: docker compose ps

Run: docker inspect thothii-core-1 thothii-frontend-1 --format '{{.Name}} {{.Image}} {{.State.Status}}'

Expected: both report the new image ids and running; the core healthcheck reports healthy (the frontend image has no healthcheck).

  • Step 5: Review diff and commit intentionally

Run: git diff --check && git status --short && git diff --stat

Commit only the files in this plan with a scoped message after review.