Pathfinderby Commenda
AI elements

Plan review card

Review a host-owned plan and explicitly approve or request changes.

Simulate failure
Plan

Approve this plan?

Onboard 5 entities · 6 steps

Usage

Example.tsx
import { PlanReviewCard } from '@commenda/pathfinder/plan-review-card';

<PlanReviewCard
  key={request.id}
  title="Approve this plan?"
  description="Onboard 5 entities · 6 steps"
  planConfig={{ onOpen: openPlanTab }}
  onSuggestChanges={focusComposer}
  onApprove={approvePlan}
/>;

Inline plan review

Simulate failure
Plan

Approve this plan?

Onboard 5 entities · 6 steps

Failure and retry

Simulate failure
Plan

Approve this plan?

Onboard 5 entities · 6 steps

Reset restores the example. Simulate failure retains the card for retry; no backend request is made.

API reference

PropTypeBehavior
planConfigRequiredObjectView fieldsOpens the application plan tab; label defaults to Open plan
onApproveRequiredRequestHandlerExplicit plan approval; return an attributed plan receipt
onSuggestChangesRequired() => voidOpens/focuses the host's change workflow without accepting the plan
approveLabelstringDefaults to Approve plan
titleRequiredstring—
descriptionstringSupporting guidance associated with the field.
placement"docked"|"inline"—
stateRequestStateView fields · 2 shapes—
onDismiss() => void—
classNamestringAdditional classes for the component root.
planConfigObject fields
FieldTypeRequirement
labelstringOptional
onOpen() => voidRequired
stateObject fields

Choose one of these 2 shapes. Do not combine fields excluded by the selected shape.

Option 1

FieldTypeRequirement
status"ready" | "pending" | "loading"Required

Option 2

FieldTypeRequirement
status"resolved"Required
recapResponseRecapPropsRequired

Shared request props

PropTypeBehavior
titlestringRequired card title.
descriptionstringOptional supporting text.
placement'docked' | 'inline'Docked by default; parent controls width and placement
stateRequestStateReady, loading skeleton, externally pending, or resolved with a recap
onDismiss() => voidTemporary dismissal via Not now or scoped Escape; not a submitted answer
classNamestringLayout override

RequestHandler<T> returns { status: 'accepted', recap: ResponseRecapProps } or { status: 'failed', message: string }, directly or as a Promise. A thrown error shows a retry message and preserves the draft. The host persists accepted responses; the component displays the returned receipt. state={{ status: 'resolved', recap }} restores history. Use a stable request ID as the React key, and change the key when the request changes. External pending is a rendering state; authorization and idempotency remain server responsibilities.

REQUEST-01: Docked tray uses pearl100, a raised shadow, radius16 and a white radius10 inset with 16px padding. Header40, footer at least44; controls wrap on narrow screens.

REQUEST-02: The parent places the request 12px above Composer at the same width. Inline requests are flat and fill their message column. Escape only temporarily dismisses; no global listener is installed.

REQUEST-03: Reading content does not animate. Existing Button and Popover motion respects reduced motion. Pending retains the draft and exposes a polite status. Acceptance never precedes the host result.

Design rules

  • PLAN-01: Keep the card compact: title, short scope summary, Open plan, Suggest changes, Approve plan. No Skip and no embedded plan editor.
  • PLAN-02: Opening the plan or asking for changes does not approve it. Return a kind: 'plan' recap with actor and timestamp after explicit server acceptance.

The host owns the plan version. Key each review by request/version ID so an in-flight response cannot approve a newly replaced plan.

Integration and accessibility

Buttons have explicit names and keyboard focus. No network transport or AI SDK provider is required. Requests, tool execution, permission checks, durable outcomes and composer coordination stay in the application. See AI content integration for migration mappings; application migration is outside this build.

On this page