Skip to content

feat(phase8 #78 D8.4c): FE interactive consent + elicitation UI#1704

Merged
earayu merged 8 commits into
mainfrom
chenyexuan/phase8-task78-d84c-consent-elicitation
Apr 25, 2026
Merged

feat(phase8 #78 D8.4c): FE interactive consent + elicitation UI#1704
earayu merged 8 commits into
mainfrom
chenyexuan/phase8-task78-d84c-consent-elicitation

Conversation

@earayu
Copy link
Copy Markdown
Collaborator

@earayu earayu commented Apr 25, 2026

Summary

Body components for the `` / `` placeholders that #77 (huangheng, PR #1703) reserved on the parts renderer. Plug into the SDK-compatible slot props (`{ chatId, turnId, part }`) and wire user decisions back via `decideToolConsent()` / `submitElicitation()` from the AI SDK-compatible client API landed by #76.

PR base = `fe/d8.4b-parts-renderer` (chained on #1703 per PM lock msg=4adbf669) so the diff against that base shows only #78 lane (3 files, +523/-0). Once #1703 merges, GitHub auto-rebases this PR's base to main.

Write set (3 files)

File Type Purpose
`web/src/components/chat/consent-prompt.tsx` NEW Renders `AgentToolConsentPart`: `toolName + argsPreview + risk badge + argsHash fingerprint + Approve/Deny buttons`. Server-driven state machine; no local optimism.
`web/src/components/chat/elicitation-form.tsx` NEW Renders `AgentElicitationPart`: prompt + JSON Schema → form fields (string/number/integer/boolean/enum, format=textarea, default, required). FE-side validation + `toast.error` on retry.
`web/src/components/chat/chat-messages.tsx` MOD Imports both components; passes them to `AgentTurnRenderer` as `ConsentSlot` / `ElicitationSlot`. Renderer shell + transport hook contract untouched.

D9 §3.1 / §5.1 contract verification

# Surface Verification
1 Consent UI shows `toolName + argsPreview + risk` only — raw args never reach FE wire `consent-prompt.tsx` reads only `data.toolName` / `data.argsPreview` / `data.risk` / `data.argsHash`. BE-side `args_preview()` redaction (#75 `tools/args_cache.py`) + `argsPreview` field on `ToolConsentData` (#74 wrapped shape) ensure raw args never serialize to wire.
2 Consent decisions go to chat-scoped tenant-bound path Calls `decideToolConsent(chatId, turnId, toolCallId, decision)` → POST `/agent/chats/{chat_id}/turns/{turn_id}/consent/{tool_call_id}` (per #75 endpoint with HTTP-layer `get_turn_snapshot` ownership pre-check + service-layer `ConsentOwnershipError` defense-in-depth).
3 Elicitation form is schema-validated — FE accelerator, BE source of truth `elicitation-form.tsx::buildResponse` runs FE-side required-field + type-coercion check before submit. BE re-validates via `tools/elicitation.py::_required_fields_validator` (#75 default validator) — 422 path leaves form populated for retry.
4 Server-driven state machine Both components read `part.data.state` for current visible state. Click → `decideToolConsent` / `submitElicitation` → wait for next streamed `data-tool-consent` / `data-elicitation` part with new state to flip the UI. No local optimism.
5 Error handling: 403 / 404 / 409 / 422 All non-2xx surface via `toast.error(extractError(...))`. Form / prompt stays mounted so user can retry. `describeError` falls back to friendly message on unknown shapes.
6 State transitions render Resolved (`approved` / `denied` / `expired` for consent; `answered` / `cancelled` for elicitation) parts render via `ConsentResolvedRow` / `ElicitationResolvedRow` with status icon + tone color.

Boundary discipline

  • ❌ Does NOT change `transport / hook contract` (per PM lock msg=6e521597) — consumes `useAgentTurnStream` shape unchanged.
  • ❌ Does NOT change renderer shell (per PM lock msg=4adbf669) — only fills the slot bodies via `ConsentSlot` / `ElicitationSlot` props.
  • ❌ Does NOT change schema main design ([Features] support preview and download documents #74 final shape).
  • ❌ Does NOT touch FE seam interface — `ConsentSlotProps` / `ElicitationSlotProps` from `agent-turn-renderer.tsx` adopted as-is.

Built on

Gates

  • `yarn tsc --noEmit` on changed files: clean (8 pre-existing errors on main are unrelated)
  • `yarn lint --quiet`: clean (no warnings/errors)

Test plan

  • D9 §3.1 contract — argsPreview only, no raw args
  • D9 §3.1 contract — Approve/Deny buttons gated to `state === 'pending'`; otherwise resolved-row
  • D9 §5.1 contract — JSON Schema → form fields (string/number/integer/boolean/enum/textarea/default/required)
  • D9 §5.1 contract — server-driven state machine; no local optimism
  • Error handling — 403/404/409/422 surface via `toast.error`; form stays mounted for retry
  • Boundary — no transport / hook / renderer shell modification

🤖 Generated with Claude Code

earayu and others added 6 commits April 25, 2026 23:12
…itation/activity)

D8.4b first-cut. Replaces the legacy `AgentTurnCard` + `legacy-snapshot-shim`
projection with a renderer that consumes the new `useAgentTurnStream`
seam (D8.4a, merge `63a9d522`) directly. Each `AgentMessagePart` is
rendered by type; transient `data-activity` is surfaced through a
separate inline indicator and never persisted.

## What lands

* **NEW** `web/src/components/chat/agent-turn-renderer.tsx` — rebuilds
  the activity card from the `parts` stream. Keeps the L1 visual
  baseline (avatar + status badge + activity stream Collapsible +
  answer Card + debug Collapsible + references Sheet + feedback +
  copy) so non-technical users see the same affordance.
  * `<ToolActivityItem>` — one entry per `tool-${SafeToolName}` part;
    state-aware label / icon / debug-expand previews of input + output
    (or errorText on `output-error`).
  * `<ActivityIndicator>` — transient `data-activity` rendered inline
    above the activity stream entries; replaced on each new frame and
    never persisted.
  * `<ConsentPlaceholder>` / `<ElicitationPlaceholder>` — fallback
    rendering for `data-tool-consent` / `data-elicitation` parts when
    no interactive slot is provided. **#78 chenyexuan** plugs in
    concrete components via the new `ConsentSlot` / `ElicitationSlot`
    props on `AgentTurnRendererProps`.
  * References sheet now sources from `source-url` / `source-document`
    parts + `data-citation` content, replacing the old
    `reference_bundle` artifact path.

* `chat-messages.tsx` — `AgentTurnStreamCard` now feeds the hook
  output directly into `AgentTurnRenderer`; the
  `projectToLegacySnapshot` projection layer is gone.

* **DELETE** `web/src/components/chat/agent-turn-card.tsx`
  (1279 LOC) — replaced by the new renderer end-to-end.
* **DELETE** `web/src/features/agent-runtime/legacy-snapshot-shim.ts`
  — its only caller (`AgentTurnStreamCard`) no longer needs the
  projection. `getRunningToolName` / `projectToLegacySnapshot` /
  `LegacySnapshotShim` are dropped from the feature module
  re-exports.

## Slot props (the only seam crossing into #78 territory)

```ts
type ConsentSlotProps = {
  chatId: string;
  turnId: string;
  part: AgentToolConsentPart;
};
type ElicitationSlotProps = {
  chatId: string;
  turnId: string;
  part: AgentElicitationPart;
};

type AgentTurnRendererProps = {
  // ... part stream + status from useAgentTurnStream
  ConsentSlot?: React.ComponentType<ConsentSlotProps>;
  ElicitationSlot?: React.ComponentType<ElicitationSlotProps>;
};
```

#78 chenyexuan implements `consent-prompt.tsx` + `elicitation-form.tsx`
that conform to these prop signatures; both call
`decideToolConsent` / `submitElicitation` from the agent-runtime API
client landed in D8.4a. Optional by design — the placeholder
fallback keeps the parts visible even if a slot is not yet wired.

## i18n

Adds to `page_chat.json` (zh-CN + en-US):
* `activity_stream.tool.title` + `activity_stream.tool.state.{input-streaming|input-available|output-available|output-error}`
* `activity_stream.transient.{thinking|searching_knowledge|reading_source|comparing_results|writing_answer|waiting|completed|error}`
* `activity_stream.consent.placeholder_{title,state}`
* `activity_stream.elicitation.placeholder_state`
* `activity_stream.{completed_empty,pending_empty}`
* `answer_section.completed_empty`

## Verification

* `yarn lint` clean.
* `tsc --noEmit` clean for the touched files (the four pre-existing
  errors in `chat-input.tsx` are unrelated and untouched here).
* `yarn dev` boots in 2.8s on port 3012; `GET /`, `/auth/signin`,
  `/workspace/collections`, `/workspace` all return 200.

## Notes

* The EOF-before-terminal regression test follow-up that Weston
  flagged on D8.4a (msg=b7ae3bfd) is not bundled here — there is no
  FE test infra in the repo today, and adding `vitest` is its own
  scope. The behavior is documented at the relevant code paths in
  `stream-client.ts` + `reducer.ts`; recommend adding a dedicated
  test-infra PR after `#77/#78` land.
* No hook contract changes; `useAgentTurnStream` and the
  `AgentMessagePart` typed union are exactly as merged in
  `63a9d522`.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…storical reload

Addresses dongdong msg=97336fb9 — terminal historical AI turns reloaded
through `seedFromSnapshot()` were rendering as empty `idle` cards
because `useAgentTurnStream({ streamUrl: null })` keeps `parts: []`
and `status: 'idle'`, and the new renderer no longer reads
`baselineSnapshot.timeline / .artifacts` directly.

Fix scope: read-only synthesis of `AgentMessagePart[]` from the legacy
snapshot's artifacts (answer text → one `text` part; reference bundle
items → `source-url` + `data-citation` parts) when the hook is
dormant for a terminal turn. Backend status is mapped back to the
stream-side enum so the renderer's status branching stays consistent.

Files:
* **NEW** `web/src/features/agent-runtime/snapshot-fallback.ts` —
  `synthesizePartsFromSnapshot()` + `mapBackendTurnStatus()` +
  `isTerminalBackendStatus()` helpers. Read-only, never feeds the
  live reducer; deletes wholesale once the BE snapshot endpoint
  returns UIMessages.
* `chat-messages.tsx` — `AgentTurnStreamCard` falls back to
  synthesized parts + mapped status when `streamUrl == null` and the
  live stream has not produced anything. Live turns are unaffected.
* `features/agent-runtime/index.ts` — re-exports the fallback helpers.

Tool call timeline is intentionally NOT replayed for historical turns —
matches the legacy `agent-turn-card` behaviour, which also did not
show tool-call activity stream once the answer artifact had landed.

Verified: `yarn lint` clean; `tsc --noEmit` clean for touched files;
`yarn dev` boots in 2.6s on port 3013; `GET /`, `/auth/signin`,
`/workspace/collections`, `/workspace` all return 200.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Body components for the `<ConsentSlot>` / `<ElicitationSlot>`
placeholders that #77 (huangheng, PR #1703 head `b532abcd`) reserved
on the parts renderer. Consumes the SDK-compatible slot props
(`{ chatId, turnId, part }`) and wires the user's decision back via
`decideToolConsent()` / `submitElicitation()` from the AI SDK-
compatible client API landed by #76.

## Write set (3 files)

- NEW `web/src/components/chat/consent-prompt.tsx` -- renders one
  `AgentToolConsentPart`. Surfaces only `toolName + argsPreview +
  risk badge` (raw args never reach the FE per #75 backend
  redaction), short fingerprint of `argsHash`, plus Approve / Deny
  buttons. State machine is server-driven: clicking the button calls
  `decideToolConsent(...)` and we wait for the next streamed
  `data-tool-consent` part to flip the visible state -- no local
  optimism. Resolved (approved/denied/expired) parts render a
  compact status row.
- NEW `web/src/components/chat/elicitation-form.tsx` -- renders one
  `AgentElicitationPart`. Generates form fields from the JSON-Schema
  fragment (`string` / `number` / `integer` / `boolean` / `enum`,
  `format: textarea` for multi-line, `default` for initial state,
  `required` for FE-side gating). On submit calls
  `submitElicitation(...)` with coerced payload; on validation error
  we leave the form populated for retry. Resolved (answered /
  cancelled) renders a compact status row.
- MOD `web/src/components/chat/chat-messages.tsx` -- imports both
  components and passes them to `AgentTurnRenderer` as `ConsentSlot`
  / `ElicitationSlot`. Renderer shell + transport hook contract
  untouched.

## D9 §3.1 / §5.1 contract (renderer-side verification)

- consent UI shows only `toolName + argsPreview + risk` -- raw args
  never reach the FE wire (BE-side `args_preview()` redaction per
  #75 + `argsPreview` field on `ToolConsentData` per #74 wrapped
  shape).
- consent decisions go to chat-scoped path
  `/agent/chats/{chat_id}/turns/{turn_id}/consent/{tool_call_id}`
  -- HTTP-layer ownership pre-check + service-layer
  `ConsentOwnershipError` defense-in-depth still apply (per #75).
- elicitation form is schema-validated FE-side as a UX accelerator;
  the BE remains source of truth (`tools/elicitation.py`
  `_required_fields_validator` per #75).
- pending -> approved | denied | expired (consent) and pending ->
  answered | cancelled (elicitation) state transitions are picked
  up from the next streamed part; the visible UI is server-driven.
- Error handling: 403 (ownership) / 404 (not found) / 409 (already
  resolved) / 422 (validation) all surface via `toast.error(...)`;
  the form / prompt stays mounted so the user can retry.

## Boundary discipline

- Does NOT change `transport / hook contract` (per PM lock
  msg=6e521597) -- consumes `useAgentTurnStream` shape unchanged.
- Does NOT change renderer shell (per PM lock msg=4adbf669) --
  only fills the slot bodies via `ConsentSlot` / `ElicitationSlot`
  props.
- Does NOT change schema main design (#74 final shape).

## Built on

- #73 D8.1 wire emitter (cuiwenbo, `51137301`)
- #74 D8.2 at-rest UIMessage storage (Bryce, `e290488b`)
- #75 D8.3 backend tool lifecycle + consent/elicit endpoints + 7-point
  contract enforcement (chenyexuan, `bd4052d5`)
- #76 D8.4a SDK-compatible stream transport + `useAgentTurnStream`
  hook + client API (huangheng, `63a9d522`)
- #77 D8.4b parts renderer + `<ConsentSlot>` / `<ElicitationSlot>`
  seam (huangheng, PR #1703 head `b532abcd`) -- this PR is chained
  on top of `#1703` per PM split-write-set lock msg=4adbf669.

## Gates

- `yarn tsc --noEmit` on changed files: clean (8 pre-existing
  errors on main are unrelated to this diff -- all in
  `chat-input.tsx` / `collection-form.tsx` / `collection-provider.tsx`
  / `app/page.tsx`).
- `yarn lint --quiet`: clean (no warnings/errors).
…summary handling

Per architect msg=711f8c2f review of the prior `2effca4a` fix:

* File header now explicitly references task **#90 (D8.4d)** as the
  removal trigger — `Bryce` claimed #90 (msg=00230183) to migrate
  the snapshot endpoint to canonical UIMessage parts, after which
  this whole module deletes wholesale.
* Adds `extractErrorTextFromSnapshot()` covering the
  `error_summary` artifact, mapping its payload (`message` /
  `text` / `summary` / artifact-level summary) back into the
  renderer's `errorText` channel. The wire/at-rest contract treats
  `error` as a lifecycle marker (status + errorText), not a part,
  so this stays out of `AgentMessagePart[]`.
* `chat-messages.tsx` `AgentTurnStreamCard` chains
  `extractErrorTextFromSnapshot` ahead of `envelope.error_message`
  in the fallback path so historical FAILED turns surface the
  richer artifact text when present.

Verified: `yarn lint` clean; `tsc --noEmit` clean for the touched
files.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Base automatically changed from fe/d8.4b-parts-renderer to main April 25, 2026 15:55
earayu added 2 commits April 25, 2026 23:57
…alog

Address dongdong B1 blocker on PR #1704 (msg=e86a774b): the new
consent prompt + elicitation form rendered hardcoded English strings
(button labels, toast messages, risk badges, state labels), breaking
the zh-CN visual baseline that #77 already established for the
renderer placeholders.

## What changed

- `web/src/components/chat/consent-prompt.tsx` -- replace hardcoded
  English with `useTranslations('page_chat')`. Risk label looks up
  `activity_stream.consent.risk.{key}`; resolved-state label looks up
  `activity_stream.consent.state_label.{state}`; toast falls back to
  `activity_stream.consent.decision_failed` when the API rejection
  carries no message. Dynamic identifiers (`toolName`, `argsPreview`,
  `argsHash`) stay verbatim per dongdong's guidance.
- `web/src/components/chat/elicitation-form.tsx` -- same pattern:
  `submit` / `submitting` / `submit_failed` / `from_server` /
  `no_schema_fields` / `missing_required` / `invalid_value` /
  `select_placeholder` / `state_label` all routed through i18n.
  Schema field `title` / `description` and the prompt itself stay
  verbatim (BE-controlled identifiers).

## Catalog updates (en-US + zh-CN, both split + merged forms)

Added under `activity_stream.consent` and `activity_stream.elicitation`:

- consent: `approve` / `deny` / `approving` / `denying` /
  `args_fingerprint` / `decision_failed` / `resolved_status` /
  `risk.{writes_user_data, calls_external_api, modifies_system,
  admin_only}` / `state_label.{approved, denied, expired}`
- elicitation: `submit` / `submitting` / `submit_failed` /
  `from_server` / `no_schema_fields` / `missing_required` /
  `invalid_value` / `select_placeholder` / `resolved_status` /
  `state_label.{answered, cancelled}`

The merged `web/src/i18n/{en-US, zh-CN}.json` catalogs and the per-page
`web/src/i18n/{en-US, zh-CN}/page_chat.json` files both got the same
additions so `yarn i18n:sync` regenerates `en-US.d.json.ts` typed
catalog with the new keys.

## Boundary unchanged

- Slot props (`ConsentSlotProps` / `ElicitationSlotProps`) untouched.
- No transport / hook / renderer-shell changes.
- BE contract surface (decideToolConsent / submitElicitation /
  ToolConsentData / ElicitationData) untouched.

## Gates

- `yarn i18n:sync` regenerated typed catalog.
- `yarn tsc --noEmit` on changed files: clean (0 errors in #78
  files; 8 pre-existing main errors unrelated).
- `yarn lint --quiet`: clean (no warnings/errors).
…k78-d84c-consent-elicitation

# Conflicts:
#	web/src/components/chat/chat-messages.tsx
#	web/src/i18n/en-US.json
#	web/src/i18n/en-US/page_chat.json
#	web/src/i18n/zh-CN.json
#	web/src/i18n/zh-CN/page_chat.json
@earayu earayu merged commit 3195d18 into main Apr 25, 2026
3 of 4 checks passed
@earayu earayu deleted the chenyexuan/phase8-task78-d84c-consent-elicitation branch April 25, 2026 16:45
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