Skip to content

FE-556: Structured interview: client UI#20

Merged
lunelson merged 2 commits into
mainfrom
ln/fe-556-interview-client-ui
Apr 2, 2026
Merged

FE-556: Structured interview: client UI#20
lunelson merged 2 commits into
mainfrom
ln/fe-556-interview-client-ui

Conversation

@lunelson
Copy link
Copy Markdown
Contributor

@lunelson lunelson commented Apr 2, 2026

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

@linear
Copy link
Copy Markdown

linear Bot commented Apr 2, 2026

Copy link
Copy Markdown
Contributor Author

lunelson commented Apr 2, 2026

This stack of pull requests is managed by Graphite. Learn more about stacking.

@lunelson lunelson marked this pull request as ready for review April 2, 2026 12:26
@lunelson lunelson changed the title feat: structured interview client UI with turn cards and option selection FE-556: Structured interview: client UI Apr 2, 2026
@augmentcode
Copy link
Copy Markdown

augmentcode Bot commented Apr 2, 2026

🤖 Augment PR Summary

Summary: Adds a structured interview UI that renders the latest turn as a card with options and persists option selections.

Changes:

  • Enriched GET /api/projects/:id to return turns joined with their options.
  • Added POST /api/projects/:id/turns/:turnId/select to persist option selection (is_selected) and store a selection Data Part.
  • Client InterviewWorkspace now hydrates chat history from persisted assistant_parts when present (fallback to scalar question).
  • Introduced a TurnCard UI showing question, grounding (why), impact badge, and selectable options.
  • Added “safe” JSON deserializers for assistant/user parts and tests for malformed/null inputs.
  • Added DB lifecycle test to verify parts columns survive close/reopen, plus API tests for enriched state and selection.

Notes: Selection triggers a router invalidation to refresh persisted state before advancing to the next turn.

🤖 Was this summary useful? React with 👍 or 👎

Copy link
Copy Markdown

@augmentcode augmentcode Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review completed. 6 suggestions posted.

Fix All in Augment

Comment augment review to trigger a new review at any time.

Comment thread src/server/app.ts

const dataPart: DataOptionSelectionPart = {
type: 'data-option-selection',
data: { turnId, selectedOptionId: position },
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

Comment thread src/server/app.ts
};

updateTurn(db, turnId, {
answer: selected?.content ?? '',
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

Comment thread src/server/app.ts
return;
}

selectOption(db, turnId, position);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Fix This in Augment

🤖 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();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

Comment thread src/server/parts.ts
export function safeDeserializeAssistantParts(json: string | null | undefined): AssistantPart[] {
if (!json) return [];
try {
const parsed = JSON.parse(json);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

Comment thread memory/PLAN.md
- **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`
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Process note (PR metadata): the PR title doesn’t match the repo convention for stacked slices (expected FE-556: …). (Rule: AGENTS.md)

Severity: low

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

Copy link
Copy Markdown
Contributor Author

lunelson commented Apr 2, 2026

Merge activity

  • Apr 2, 6:52 PM UTC: A user started a stack merge that includes this pull request via Graphite.
  • Apr 2, 6:55 PM UTC: Graphite rebased this pull request as part of a merge.
  • Apr 2, 6:56 PM UTC: @lunelson merged this pull request with Graphite.

@lunelson lunelson changed the base branch from ln/fe-555-parts-persistence to graphite-base/20 April 2, 2026 18:53
@lunelson lunelson changed the base branch from graphite-base/20 to main April 2, 2026 18:54
lunelson added 2 commits April 2, 2026 18:55
…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
kostandinang added a commit that referenced this pull request May 19, 2026
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>
kostandinang added a commit that referenced this pull request May 20, 2026
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant