315 lines
13 KiB
Markdown
315 lines
13 KiB
Markdown
# 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`.
|
|
|
|
- [x] **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:
|
|
|
|
```ts
|
|
rpc.emit("event", {
|
|
type: "message_update",
|
|
assistantMessageEvent: { type: "thinking_delta", delta: "reasoning" },
|
|
});
|
|
expect(events).toContainEqual({ type: "activity_delta", text: "reasoning" });
|
|
```
|
|
|
|
- [x] **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.
|
|
|
|
- [x] **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.
|
|
|
|
- [x] **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.
|
|
|
|
- [x] **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.
|
|
|
|
- [x] **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`.
|
|
|
|
- [x] **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`.
|
|
|
|
- [x] **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.
|
|
|
|
- [x] **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`.
|
|
|
|
- [x] **Step 3: Implement builder and gate routing**
|
|
|
|
Add `buildJoinReviewRequest`. In `reviewer_decide`, detect a non-empty, join-only merit list:
|
|
|
|
```js
|
|
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`.
|
|
|
|
- [x] **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.
|
|
|
|
- [x] **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.
|
|
|
|
- [x] **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.
|
|
|
|
- [x] **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.
|
|
|
|
- [x] **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.
|
|
|
|
- [x] **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.
|
|
|
|
- [x] **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.
|
|
|
|
- [x] **Step 4: Hide the single-block toggle**
|
|
|
|
Wrap the layout control in `{blocks.length > 1 && (...)}` without changing multi-block state or
|
|
rendering.
|
|
|
|
- [x] **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.
|
|
|
|
- [x] **Step 1: Write failing gate validation test**
|
|
|
|
Pass the exact observed payload shape:
|
|
|
|
```js
|
|
open_questions: [{ label: "pazienti_finale restituisce 0 righe", question: "Verificare i filtri" }]
|
|
```
|
|
|
|
Expect `ok:false` and an error naming `open_questions[0]`.
|
|
|
|
- [x] **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.
|
|
|
|
- [x] **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.
|
|
|
|
- [x] **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.
|
|
|
|
- [x] **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.
|
|
|
|
- [x] **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.
|
|
|
|
- [x] **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.
|
|
|
|
- [x] **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.
|
|
|
|
- [x] **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.
|
|
|
|
- [x] **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.
|
|
|
|
- [x] **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).
|
|
|
|
- [x] **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.
|