Conversation
The scrolling chat: turns, an optional title bar, and the prompt pinned below.
SourcePackage @weft-js/warp
Preview
tsx
import { derived, state } from '@weft-js/core'import type { State } from '@weft-js/core'import { Assistant, Conversation, Prompt, Suggestions, UserMessage, foldAnswer } from '@weft-js/warp'import type { AgentEvent, Answer } from '@weft-js/warp'import type { AgentSource, ContextUse, ModelOption, PromptSuggestion } from '@weft-js/warp'export function DeployHelp() { const turns = state<Turn[]>([{ id: 1, text: QUESTION, answer: state(FINISHED) }]) const last = () => turns.value[turns.value.length - 1]!.answer const busy = derived(() => ['waiting', 'streaming', 'approval'].includes(last().value.state)) const send = (text: string) => { const answer = state(foldAnswer([])) turns.value = [...turns.value, { id: turns.value.length + 1, text, answer }] play(answer, ROLLBACK_ASK) } const stop = () => { last().value = foldAnswer(last().value, { kind: 'done', stopped: true }) } const again = (t: Turn) => { t.answer.value = foldAnswer([]); play(t.answer, WHY_FAILED) } return ( <div style="block-size:620px"> <Conversation title="mobile-bff deploy" prompt={ <Prompt busy={busy.value} models={MODELS} model="swift" commands={COMMANDS} mentions={MENTIONS} context={CONTEXT} tools={['slash']} onSubmit={send} onStop={stop}> {!busy.value && <Suggestions items={CHIPS} variant="chips" style="margin:0 0 10px" onPick={s => send(s.title)} />} </Prompt> }> {turns.value.map(t => ( <div key={t.id} style="display:contents"> <UserMessage text={t.text} arrive={t.id > 1} /> <Assistant n={t.id} quiet={t.id === 1} answer={t.answer.value} onApprove={() => play(t.answer, ROLLBACK_RUN)} onDeny={() => play(t.answer, ROLLBACK_DENIED)} onFeedback={() => {}} onRegenerate={() => again(t)} /> </div> ))} </Conversation> </div> )}const PEOPLE = [ { id: 'mara', name: 'Mara Lindqvist', email: 'mara@weft.dev', role: 'Owner', team: 'Platform', initials: 'ML' }, { id: 'jun', name: 'Jun Tanaka', email: 'jun@weft.dev', role: 'Admin', team: 'Platform', initials: 'JT' }, { id: 'priya', name: 'Priya Raman', email: 'priya@weft.dev', role: 'Member', team: 'Web', initials: 'PR' }, { id: 'tomas', name: 'Tomás Oliveira', email: 'tomas@weft.dev', role: 'Member', team: 'Web', initials: 'TO' }, { id: 'lena', name: 'Lena Fischer', email: 'lena@weft.dev', role: 'Member', team: 'Data', initials: 'LF' }, { id: 'noor', name: 'Noor Haddad', email: 'noor@weft.dev', role: 'Viewer', team: 'Data', initials: 'NH' }, { id: 'sam', name: 'Sam Okafor', email: 'sam@weft.dev', role: 'Member', team: 'Mobile', initials: 'SO' }, { id: 'ines', name: 'Inés Vidal', email: 'ines@weft.dev', role: 'Viewer', team: 'Mobile', initials: 'IV' },]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 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' },]const MODELS: ModelOption[] = [ { id: 'deep', name: 'weft Deep', short: 'Deep', provider: 'Hosted', description: 'Long investigations', capabilities: ['tools', 'logs'], context: '200k', cost: '$$$' }, { id: 'swift', name: 'weft Swift', short: 'Swift', provider: 'Hosted', description: 'Fast, good at config', capabilities: ['tools'], context: '200k', cost: '$$' }, { id: 'local', name: 'On-prem small', short: 'Small', provider: 'Your cluster', description: 'Stays on your network', context: '32k', cost: '$' },]const COMMANDS: PromptSuggestion[] = [ { value: '/logs', description: 'Read a deploy\'s log' }, { value: '/rollback', description: 'Go back to an earlier deploy' }, { value: '/status', description: 'Show every project\'s status' },]const MENTIONS: PromptSuggestion[] = PEOPLE.slice(0, 4).map(p => ({ value: `@${p.id}`, description: p.name }))const CONTEXT: ContextUse = { used: 46000, limit: 200000, parts: [{ label: 'Logs', tokens: 22000 }, { label: 'History', tokens: 16000 }, { label: 'Tools', tokens: 8000 }] }const QUESTION = 'Why did the mobile-bff deploy fail?'const FINISHED = foldAnswer(WHY_FAILED)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)}type Turn = { id: number; text: string; answer: State<Answer> }const CHIPS = [{ title: 'Roll it back' }, { title: 'Show me the failing check' }]Installation
npm install @weft-js/warpUsage
tsx
import { Conversation } from '@weft-js/warp'Empty chat
Starter cards until the first message.
tsx
import { state } from '@weft-js/core'import { Conversation, Prompt, Stack, Suggestions, Text, UserMessage } from '@weft-js/warp'import type { PromptSuggestion } from '@weft-js/warp'export function Starters() { const asked = state('') return ( <div style="block-size:440px"> <Conversation title="New chat" prompt={<Prompt tools={['slash']} commands={COMMANDS} onSubmit={text => (asked.value = text)} />}> {asked.value ? <UserMessage text={asked.value} arrive /> : <Stack gap="group"><Text role="heading">What do you need?</Text><Suggestions items={STARTERS} onPick={s => (asked.value = s.title)} /></Stack>} </Conversation> </div> )}const COMMANDS: PromptSuggestion[] = [ { value: '/logs', description: 'Read a deploy\'s log' }, { value: '/rollback', description: 'Go back to an earlier deploy' }, { value: '/status', description: 'Show every project\'s status' },]const STARTERS = [ { title: 'Why did my deploy fail?', description: 'Reads the last failed build' }, { title: 'Roll back mobile-bff', description: 'Asks before it changes anything' }, { title: 'What changed today?', description: 'Every deploy across your projects' }, { title: 'Which project costs the most?', description: 'From this month\'s usage' },]When to use
Use it for
- A full chat with an assistant
- Anywhere turns keep arriving and the view should follow them
Accessibility
- Tab
- Moves into the messages, then the prompt
- The log is
role="log"with live updates off: each answer announces its own end, never each word. - It follows new messages only while you are near the bottom. Otherwise Jump to latest appears.
API Reference
| Prop | Type | Default | Description |
|---|---|---|---|
label | string | 'Assistant conversation' | The accessible name. |
title | unknown | — | Shown in the top bar. |
bar | unknown | — | Actions at the end of the top bar. |
prompt | unknown | — | The composer, pinned below the log. |
children | unknown | — | What it contains. |
Parts
Every element carries a stable name, so you can style it from your own CSS: [data-scope="thread"] [data-part="root"]
rootapp-barapp-titlespacerlogjump
RelatedPromptAssistantUser message