Skip to content
weftGitHub

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/warp

Usage

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

PropTypeDefaultDescription
labelstring'Assistant conversation'The accessible name.
titleunknown—Shown in the top bar.
barunknown—Actions at the end of the top bar.
promptunknown—The composer, pinned below the log.
childrenunknown—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