Skip to content

feat: FlowConfirmation, the approval card for agent actions #22

Description

@divyanshub024

What kind of request is this

A new component

The problem

Agent apps need an approval step: the assistant proposes an action (run a command, send an email, delete a branch) and the conversation waits until the person approves or rejects it. flow_ui has nothing for this today. Hosts hand-roll the card through FlowCustomPart, each one re-solving the same three states, and stac_ai's tool loop has nothing to standardize on.

What you'd like

A FlowConfirmation card in the assistant turn: a host-written title ("Run flutter pub get?"), an optional description, an optional detail slot for any widget (a command preview, a diff, a FlowCodeBlock), and approve/reject buttons.

Three states, per the design file. State is data, as everywhere: the host passes the status in and re-renders. The card never flips itself.

Status The card The buttons
pending open, full ink both live
approved quieted collapse to a record: check + label
rejected quieted collapse to a record: cross + label

The settled states matter as much as the pending one. The transcript keeps a scannable record of what was approved and what was refused.

API sketch

FlowConfirmation(
  title: 'Run flutter pub get?',
  description: 'The agent wants to fetch dependencies.',
  detail: FlowCodeBlock(code: 'flutter pub get', language: 'bash'),
  status: FlowConfirmationStatus.pending,
  approveLabel: 'Approve',   // host-localized, as always
  rejectLabel: 'Reject',
  onApprove: approve,        // intent out; deciding is the host's move
  onReject: reject,
  padding: ...,              // metric overrides, as everywhere
  borderRadius: ...,
  style: FlowConfirmationStyle(...),  // over FlowTheme.confirmationStyle
)

FlowConfirmationStyle (proposed fields)

Follows the component-styles convention (#21): lives in lib/src/styles/, app-wide default on FlowTheme.confirmationStyle, widget wins over theme field by field, tokens under both.

  • backgroundColor: card fill. Defaults to the error card's 2% ink wash.
  • borderColor: card hairline. Defaults to outlineVariant.
  • titleStyle / descriptionStyle: merged over their role defaults.
  • approveColor: the approve accent. Defaults to primary.
  • rejectColor: the reject ink. Defaults to error.
  • recordColor: the settled check/cross and label. Defaults to onSurfaceVariant; the approved check keeps approveColor.

One new shape here: two opposed actions means two accent fields. First style class where that happens, so it should be a deliberate call.

Decisions to settle

  1. Ship a FlowConfirmationPart too, rendered by FlowMessage with intent threaded through FlowThread. Same precedent as FlowErrorPart: the confirmation lives in message data, survives restore, and reaches the thread without messageBuilder.
  2. Settled cards keep the detail. Only the button row collapses into the record; the evidence of what was approved should not vanish with the decision.
  3. Pending is inert while the turn is still streaming. Nobody should approve an action whose description is still being written. Same reasoning as the code block hiding copy mid-stream.
  4. Expiry, timeouts, and "someone else decided" are host state. The host just passes a settled status.

Accessibility

The card announces on arrival (live region, like FlowErrorState; a permission request arrives unprompted). Labels double as the buttons' accessible names. The settled record reads as text, not disabled buttons.

Definition of done

  • FlowConfirmation + FlowConfirmationStatus, exported from the barrel
  • FlowConfirmationPart + FlowMessage/FlowThread wiring (decision 1)
  • FlowConfirmationStyle in lib/src/styles/ + FlowTheme.confirmationStyle (feat: component styles on every widget with app-wide FlowTheme defaults #21 convention)
  • Metrics baked from the Figma frame; padding/borderRadius overrides
  • No shipped strings; labels host-localized, doubling as accessible names
  • Playground demo with pending/approved/rejected variants and per-variant snippets
  • Docs page with a Restyling section; README row; roadmap flipped; CHANGELOG entry
  • Dogfooded in the example's Gemini flow as a mock tool-call turn

What you're doing instead

A custom FlowCustomPart with a host-built widget. Works, but every host redoes the states, the record, and the accessibility story. For reference: assistant-ui ships this as its tool approval UI, and Claude and Cursor both converge on the same pending/approved/rejected card.

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions