FE-556: Structured interview: client UI#20
Conversation
🤖 Augment PR SummarySummary: Adds a structured interview UI that renders the latest turn as a card with options and persists option selections. Changes:
Notes: Selection triggers a router invalidation to refresh persisted state before advancing to the next turn. 🤖 Was this summary useful? React with 👍 or 👎 |
|
|
||
| const dataPart: DataOptionSelectionPart = { | ||
| type: 'data-option-selection', | ||
| data: { turnId, selectedOptionId: position }, |
There was a problem hiding this comment.
src/server/app.ts:73 — DataOptionSelection.selectedOptionId is being set to the option position; since options also have a DB id, downstream consumers may interpret this as the option row id and mis-associate selections across turns.
Severity: medium
🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.
| }; | ||
|
|
||
| updateTurn(db, turnId, { | ||
| answer: selected?.content ?? '', |
There was a problem hiding this comment.
src/server/app.ts:77 — This endpoint writes answer onto the existing turn; because conductTurn() already stores the user prompt in turn.answer, selecting an option here can overwrite prior user input and (with the client also calling /chat with the same text) risks persisting/echoing the selection twice in history.
Severity: high
🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.
| return; | ||
| } | ||
|
|
||
| selectOption(db, turnId, position); |
There was a problem hiding this comment.
src/server/app.ts:66 — If position doesn’t match any option row, selectOption() will still clear all is_selected flags and selected becomes undefined, causing answer to be updated to an empty string; this looks like a data-loss path for invalid/malicious inputs.
Severity: high
🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.
|
|
||
| const selected = lastTurn?.options.find((o) => o.position === position); | ||
| if (selected) { | ||
| await router.invalidate(); |
There was a problem hiding this comment.
src/client/routes/InterviewWorkspace.tsx:182 — After /select, router.invalidate() will cause hydrateMessages() to include the persisted selection answer, and then sendMessage({ text: selected.content }) immediately after can duplicate that answer in the UI and in the message list sent to /api/projects/:id/chat.
Severity: high
🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.
| export function safeDeserializeAssistantParts(json: string | null | undefined): AssistantPart[] { | ||
| if (!json) return []; | ||
| try { | ||
| const parsed = JSON.parse(json); |
There was a problem hiding this comment.
src/server/parts.ts:139 — safeDeserializeAssistantParts/safeDeserializeUserParts only check that the JSON parses to an array, but don’t validate element shapes against AssistantPart/UserPart (or the Zod Data Part schemas), so callers can still get structurally-invalid parts that may break later processing.
Severity: medium
🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.
| - **Verification approach**: inner — round-trip oracle for parts fidelity (I18); Zod schema validation on Data Parts (I17); unit tests for context builder output shape and equivalence (I19). → SPEC.md §Oracle Strategy (inner: fast unit tests — parts). Middle — integration: full `conductTurn()` → parts persisted → reload → hydration matches live state. Outer — manual resume test via `/cli-cdp` (reasoning + tool states visible on refresh). → SPEC.md §Acknowledged Blind Spots (parts/scalar consistency). | ||
|
|
||
| 4b. **Structured interview: client UI** — Turn card rendering (question + options + grounding + impact badge). Option selection UI using `data-option-selection` Data Part (persist `is_selected` + structured answer). Outer-loop visual verification via `/cli-cdp`. `not-started` | ||
| 4b. **Structured interview: client UI** `FE-556` — Turn card rendering (question + options + grounding + impact badge). Option selection UI using `data-option-selection` Data Part (persist `is_selected` + structured answer). Enriched API: turns with options + validated parts deserialization. Hydration from `assistant_parts`. Outer-loop visual verification via `/cli-cdp`. Also addresses review findings: validated deserialization (review #1) and DB lifecycle parts round-trip test (review #2). `done` |
There was a problem hiding this comment.
Process note (PR metadata): the PR title doesn’t match the repo convention for stacked slices (expected FE-556: …). (Rule: AGENTS.md)
Severity: low
🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.
…tion Enriched API: GET /api/projects/:id returns turns with options[] joined. New endpoint: POST /api/projects/:id/turns/:turnId/select persists option selection (is_selected + data-option-selection Data Part in user_parts + answer text). Client: TurnCard renders question, options, grounding (why), impact badge. Option click persists selection and advances interview. Hydration from persisted assistant_parts when available (reasoning, tool-invocation, text), falls back to scalar synthesis. Safe deserialization (safeDeserializeAssistantParts/UserParts) returns empty array for malformed or null input. DB lifecycle test covers parts columns through close/reopen. 11 new tests (123 total). Addresses review findings #1 and #2. Made-with: Cursor
Mark slice 4b done in PLAN.md. Update SPEC.md current coverage with new test counts (parts.test.ts 23, db.test.ts 25, app.test.ts 22). Made-with: Cursor
333d53f to
8c76739
Compare
Restores the C32 unified chat shell architecture and lands the round of UX
polish discussed in the last session.
Restored / hardened:
- Consolidated active-chat host into one SecondaryChatHost so transcript +
composer share the same useChat instance (fixes streaming when the
composer's send fired on a different hook than the transcript was reading).
- HOME-only top-strip + ChatSwitcher dropdown for every item chat
(maxVisibleItems=0).
- ChatShellAppliedToast with Undo, X dismiss, and 5s auto-dismiss.
- buildRefCodeByItemId helper threaded through the host so anchor chips show
human-meaningful refCodes instead of raw numeric ids.
Pending review:
- Bulk reconcile aria-label → "Reconcile pending reviews"; per-row → "Reconcile
pending review {id}".
- Amber pulse dot next to the collapsed count so the surface stays
discoverable while minimized.
- Row meta is now a button: clicking it navigates the workspace via
`navigate({ to: '.', hash: target_reference_code })`, reusing
StructuredListView's useGraphHashAnchor scroll mechanism.
- Row label renders `G3` (or `#20` fallback) instead of always raw id.
- Disabled when target_reference_code is null.
Turn-zero / hero:
- Moved turn-zero suggestions out of the composer overlay into the centered
SecondaryChatFreshStateHero with a contextual title ("Where would you like
to begin?" / "How would you like to change this?" / "Ask Brunch about
anything").
- Hero always renders SecondaryChatSuggestions; fresh-start chips remain as
a generic fallback when there's no pinned context.
Composer:
- ProposeChangeChips (Edit / Connect / Drill down) now render inline directly
above the textarea — no wrapping band, no background, no shadow. The
composer-sticky parent already sits above the transcript so they read as
floating chips above the scroll area.
- ComposerAnchorChip drops the «…» quote wrappers; the Highlighter icon is
the only marker. Added an X (visible on hover/focus) that locally dismisses
the highlighted-text chip without touching server-pinned pinned_span_hint.
Title strip / anchor:
- ChatSwitcher trigger now renders the active chat's anchor inline before the
label: kind-accent dot + refCode (e.g. "G1") and any additional anchored
refCodes in muted opacity. Plain mono badges — no dashed underline, no
leading "+" glyphs.
- Removed the separate `unified-chat-shell-active-anchor` pill (anchor info
now lives inside the trigger).
Shell:
- Transcript side padding bumped: compact mode `px-1.5` → `px-3`; default
`px-3` → `px-4`. Sticky-overlay band negative margins/padding match the
new edges.
Selection / impact / overlay:
- Selection menu rename Annotate → "Add to Notes" (tests updated).
- ImpactChip labels simplified to "Hard / Soft / None" (legacy overlay test
updated to match `^soft$`).
- Various comment cleanups — removed "Per user feedback" meta-narration,
consolidated multi-paragraph design comments.
Test wiring:
- Mocked `useNavigate` in pending-review-section + unified-chat-shell tests.
- Test harness for SecondaryChatCollapsible wires `onPickStartSuggestion` to
`onSubmitMessage` so the hero-mounted suggestions surface correctly in
isolated component tests.
- Fixed pre-existing TS error in pending-review tests (`source_item_kind`
is not a field).
254/254 client component tests pass; `npm run fix` clean.
Co-authored-by: Amp <amp@ampcode.com>
Restores the C32 unified chat shell architecture and lands the round of UX
polish discussed in the last session.
Restored / hardened:
- Consolidated active-chat host into one SecondaryChatHost so transcript +
composer share the same useChat instance (fixes streaming when the
composer's send fired on a different hook than the transcript was reading).
- HOME-only top-strip + ChatSwitcher dropdown for every item chat
(maxVisibleItems=0).
- ChatShellAppliedToast with Undo, X dismiss, and 5s auto-dismiss.
- buildRefCodeByItemId helper threaded through the host so anchor chips show
human-meaningful refCodes instead of raw numeric ids.
Pending review:
- Bulk reconcile aria-label → "Reconcile pending reviews"; per-row → "Reconcile
pending review {id}".
- Amber pulse dot next to the collapsed count so the surface stays
discoverable while minimized.
- Row meta is now a button: clicking it navigates the workspace via
`navigate({ to: '.', hash: target_reference_code })`, reusing
StructuredListView's useGraphHashAnchor scroll mechanism.
- Row label renders `G3` (or `#20` fallback) instead of always raw id.
- Disabled when target_reference_code is null.
Turn-zero / hero:
- Moved turn-zero suggestions out of the composer overlay into the centered
SecondaryChatFreshStateHero with a contextual title ("Where would you like
to begin?" / "How would you like to change this?" / "Ask Brunch about
anything").
- Hero always renders SecondaryChatSuggestions; fresh-start chips remain as
a generic fallback when there's no pinned context.
Composer:
- ProposeChangeChips (Edit / Connect / Drill down) now render inline directly
above the textarea — no wrapping band, no background, no shadow. The
composer-sticky parent already sits above the transcript so they read as
floating chips above the scroll area.
- ComposerAnchorChip drops the «…» quote wrappers; the Highlighter icon is
the only marker. Added an X (visible on hover/focus) that locally dismisses
the highlighted-text chip without touching server-pinned pinned_span_hint.
Title strip / anchor:
- ChatSwitcher trigger now renders the active chat's anchor inline before the
label: kind-accent dot + refCode (e.g. "G1") and any additional anchored
refCodes in muted opacity. Plain mono badges — no dashed underline, no
leading "+" glyphs.
- Removed the separate `unified-chat-shell-active-anchor` pill (anchor info
now lives inside the trigger).
Shell:
- Transcript side padding bumped: compact mode `px-1.5` → `px-3`; default
`px-3` → `px-4`. Sticky-overlay band negative margins/padding match the
new edges.
Selection / impact / overlay:
- Selection menu rename Annotate → "Add to Notes" (tests updated).
- ImpactChip labels simplified to "Hard / Soft / None" (legacy overlay test
updated to match `^soft$`).
- Various comment cleanups — removed "Per user feedback" meta-narration,
consolidated multi-paragraph design comments.
Test wiring:
- Mocked `useNavigate` in pending-review-section + unified-chat-shell tests.
- Test harness for SecondaryChatCollapsible wires `onPickStartSuggestion` to
`onSubmitMessage` so the hero-mounted suggestions surface correctly in
isolated component tests.
- Fixed pre-existing TS error in pending-review tests (`source_item_kind`
is not a field).
254/254 client component tests pass; `npm run fix` clean.
Co-authored-by: Amp <amp@ampcode.com>

feat: structured interview client UI with turn cards and option selection
Enriched API: GET /api/projects/:id returns turns with options[] joined.
New endpoint: POST /api/projects/:id/turns/:turnId/select persists
option selection (is_selected + data-option-selection Data Part in
user_parts + answer text).
Client: TurnCard renders question, options, grounding (why), impact
badge. Option click persists selection and advances interview.
Hydration from persisted assistant_parts when available (reasoning,
tool-invocation, text), falls back to scalar synthesis.
Safe deserialization (safeDeserializeAssistantParts/UserParts) returns
empty array for malformed or null input. DB lifecycle test covers
parts columns through close/reopen.
11 new tests (123 total). Addresses review findings #1 and #2.
Made-with: Cursor
spec/plan: traceability for slice 4b — mark done, update coverage
Mark slice 4b done in PLAN.md. Update SPEC.md current coverage with
new test counts (parts.test.ts 23, db.test.ts 25, app.test.ts 22).
Made-with: Cursor