Assistant
One answer from the assistant, drawn from its stream of events.
SourcePackage @weft-js/warp
Preview
tsx
import { state } from '@weft-js/core'import { Assistant, foldAnswer } from '@weft-js/warp'import type { AgentEvent } from '@weft-js/warp'import type { AgentSource } from '@weft-js/warp'export function Answer() { return ( <Assistant answer={FINISHED} n={1} quiet onFeedback={() => {}} onRegenerate={() => {}} /> )}const DEPLOY_SOURCES: AgentSource[] = [ { title: 'd-4819 build log', path: 'mobile-bff · 15:27', snippet: 'health check failed: upstream timed out after 3000 ms in ap-south' }, { title: 'Health checks', path: 'weft docs · Deploys', snippet: 'A deploy that fails three checks in a row is held back from traffic.' }, { title: 'c44e190', path: 'mobile-bff · retry.ts · L18', snippet: 'const attempts = 5 // was 2' },]const WHY_TEXT = 'Deploy `d-4819` failed its health check. The new retry code tries five times on every timed out upstream call, so one slow request in ap-south holds a connection for 15 seconds [1]. Three failed checks in a row held the deploy back, so nobody was served the bad version [2].\n\nThe cause is one line in the retry settings [3]:\n\n```TypeScript\nconst attempts = 5 // was 2\n```\n\n- `d-4801` is still live and healthy.\n- Lowering `attempts` to 2 should get the deploy through.\n'const WHY_FAILED: AgentEvent[] = [ { kind: 'plan', steps: [{ id: 'logs', title: 'Read the build log', state: 'done' }, { id: 'diff', title: 'Compare with the last good deploy', state: 'done' }, { id: 'sum', title: 'Explain what went wrong', state: 'running' }] }, { kind: 'reasoning', at: 0, text: 'The deploy reached the health check, so the build itself is fine. The log should say which check failed and in which region.' }, { kind: 'tool', id: 'logs', name: 'read_logs', args: '{ "deploy": "d-4819", "lines": 200 }', at: 4000 }, { kind: 'tool-result', id: 'logs', output: '3 failed checks\nupstream timed out after 3000 ms (ap-south)', ms: 640 }, { kind: 'tool', id: 'diff', name: 'compare_deploys', args: '{ "from": "d-4801", "to": "d-4819" }' }, { kind: 'tool-result', id: 'diff', output: '1 file changed\nsrc/retry.ts: attempts 2 → 5', ms: 410 }, { kind: 'text', text: WHY_TEXT }, ...DEPLOY_SOURCES.map((s): AgentEvent => ({ kind: 'source', ...s })), { kind: 'done' },]const FINISHED = foldAnswer(WHY_FAILED)Installation
npm install @weft-js/warpUsage
tsx
import { Assistant } from '@weft-js/warp'<Assistant answer={FINISHED} n={1} quiet onFeedback={() => {}} onRegenerate={() => {}} />Failed
Retry runs it again.
tsx
import { state } from '@weft-js/core'import type { State } from '@weft-js/core'import { Assistant, foldAnswer } from '@weft-js/warp'import type { AgentEvent, Answer } from '@weft-js/warp'export function Failed() { const answer = state(foldAnswer([{ kind: 'text', text: 'Reading the log for `d-4819`' }, { kind: 'error', message: 'The connection dropped.' }])) return <Assistant answer={answer.value} onRegenerate={() => { answer.value = foldAnswer([]); play(answer, [{ kind: 'text', text: 'Deploy `d-4819` failed its health check in ap-south.' }, { kind: 'done' }]) }} />}function play(answer: State<Answer>, events: AgentEvent[]) { let at = 0 const next = () => { const event = events[at++] if (!event || answer.value.state === 'stopped') return answer.value = foldAnswer(answer.value, event) setTimeout(next, 450) } setTimeout(next, 450)}Stopped
What arrived is kept.
tsx
import { Assistant, foldAnswer } from '@weft-js/warp'export function Stopped() { return ( <Assistant answer={CUT_OFF} quiet /> )}const CUT_OFF = foldAnswer([{ kind: 'text', text: 'Deploy `d-4819` failed its' }, { kind: 'done', stopped: true }])Waiting for the first word
An assistant answer with no content yet shows that a reply is on its way.
tsx
import { Assistant, foldAnswer } from '@weft-js/warp'export function Waiting() { return ( <Assistant answer={WAITING} /> )}const WAITING = foldAnswer([])Waiting for approval
Approve or Deny carries on the run. You can edit the arguments first.
tsx
import { state } from '@weft-js/core'import type { State } from '@weft-js/core'import { Assistant, Button, Stack, foldAnswer } from '@weft-js/warp'import type { AgentEvent, Answer } from '@weft-js/warp'export function Approval() { const answer = state(foldAnswer(ROLLBACK_ASK)) const reset = () => (answer.value = foldAnswer(ROLLBACK_ASK)) return ( <Stack gap="related"> <Assistant answer={answer.value} quiet onApprove={() => play(answer, ROLLBACK_RUN)} onDeny={() => play(answer, ROLLBACK_DENIED)} /> {answer.value.state === 'done' && <Button emphasis="ghost" tone="neutral" size="sm" onClick={reset}>Start again</Button>} </Stack> )}const ROLLBACK_ARGS = '{\n "project": "mobile-bff",\n "to": "d-4801",\n "regions": ["ap-south"]\n}'const ROLLBACK_ASK: AgentEvent[] = [ { kind: 'reasoning', text: 'A rollback changes production, so I should check the target first and ask before running it.' }, { kind: 'tool', id: 'check', name: 'get_deploy', args: '{ "deploy": "d-4801" }' }, { kind: 'tool-result', id: 'check', output: 'd-4801 · live · healthy · 412 ms p95', ms: 280 }, { kind: 'text', text: '`d-4801` is healthy, so it is a safe place to go back to. This changes production, so I need your approval.' }, { kind: 'approval', id: 'rollback', name: 'rollback', args: ROLLBACK_ARGS },]const ROLLBACK_RUN: AgentEvent[] = [ { kind: 'tool', id: 'rollback' }, { kind: 'tool-result', id: 'rollback', output: 'mobile-bff is on d-4801 in ap-south', ms: 2100 }, { kind: 'text', text: 'Done. `mobile-bff` is back on `d-4801`, and the checks are passing.' }, { kind: 'done' },]const ROLLBACK_DENIED: AgentEvent[] = [ { kind: 'tool-result', id: 'rollback', denied: true }, { kind: 'text', text: 'Okay, I left production as it is.' }, { kind: 'done' },]function play(answer: State<Answer>, events: AgentEvent[]) { let at = 0 const next = () => { const event = events[at++] if (!event || answer.value.state === 'stopped') return answer.value = foldAnswer(answer.value, event) setTimeout(next, 450) } setTimeout(next, 450)}When to use
Use it for
- Anything the assistant says: text, reasoning, tool calls, questions and sources in one turn
Accessibility
- The end of an answer is announced once. Set
quieton turns that were already there on load, so they take no focus.
API Reference
| Prop | Type | Default | Description |
|---|---|---|---|
answer | Answer | — | The answer so far: foldAnswer(events), or the result of folding a stream. |
n | number | — | The turn number, used in its accessible name. |
arrive | boolean | false | Plays the entrance, for a turn that just started. |
quiet | boolean | false | The turn was there on load: approvals take no focus and nothing is announced. |
render | ((text: string, answer: Answer) => unknown) | — | Render a block of answer text yourself (Markdown, say); the default is Prose. |
alwaysAllow | ((tool: AgentTool) => string | undefined) | — | The label for the "always allow" box on an approval. |
branch | { index: number; count: number; onChange: (index: number) => void; } | — | Which of several answers is shown, with arrows between them. |
| Event | Type | Default | Description |
|---|---|---|---|
onApprove | ((tool: AgentTool, args: string, always: boolean) => void) | — | Called when a tool call is approved, with its arguments as edited and whether to always allow it. |
onDeny | ((tool: AgentTool) => void) | — | Called when a tool call is denied. |
onAnswer | ((id: string, answer: string | string[]) => void) | — | Called with a question's id and the answer picked or typed. |
onFeedback | ((value: Feedback, reason?: string | undefined) => void) | — | Called with up, down or null when thumbs change, and the reason picked for a thumbs down. |
onRegenerate | (() => void) | — | Adds Regenerate: ask for a new answer. |
onBranch | (() => void) | — | Adds Branch: start a new conversation from this answer. |
Parts
Every element carries a stable name, so you can style it from your own CSS: [data-scope="assistant"] [data-part="mark"]
markcontent