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 — specifyis 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/StreamEventvariant{ type: "activity_delta"; text: string }. -
Produces: Zustand
activity: Entry[], consumed byModelActivityPanel. -
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 widgetjoin-review. -
Consumes: options
{ id, label, detail?, rationale? }. -
Produces: response
{ id, kind: "join-review", choices: allOptionIds }onContinue. -
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 whenblocks.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, thenlabel; 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 updatebrain/index.mdif the vault is present.
Interfaces:
-
Running Compose services must use the newly built
thothii-core:localandthothii-frontend:localimage 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.