Conversation
The scrolling chat: turns, an optional title bar, and the prompt pinned below.
SourcePackage @kerf-js/veneer
Preview
tsx
import { derived, state } from '@kerf-js/core'import { Assistant, Conversation, Prompt, Suggestions, UserMessage, foldAnswer} from '@kerf-js/veneer'import type { State } from '@kerf-js/core'import type { AgentEvent, Answer } from '@kerf-js/veneer'import type { AgentSource, ModelOption, PromptSuggestion, ContextUse} from '@kerf-js/veneer'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 = createSender(turns) 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@kerf.dev', role: 'Owner', team: 'Platform', initials: 'ML', }, { id: 'jun', name: 'Jun Tanaka', email: 'jun@kerf.dev', role: 'Admin', team: 'Platform', initials: 'JT', }, { id: 'priya', name: 'Priya Raman', email: 'priya@kerf.dev', role: 'Member', team: 'Web', initials: 'PR', }, { id: 'tomas', name: 'Tomás Oliveira', email: 'tomas@kerf.dev', role: 'Member', team: 'Web', initials: 'TO', }, { id: 'lena', name: 'Lena Fischer', email: 'lena@kerf.dev', role: 'Member', team: 'Data', initials: 'LF', }, { id: 'noor', name: 'Noor Haddad', email: 'noor@kerf.dev', role: 'Viewer', team: 'Data', initials: 'NH', }, { id: 'sam', name: 'Sam Okafor', email: 'sam@kerf.dev', role: 'Member', team: 'Mobile', initials: 'SO', }, { id: 'ines', name: 'Inés Vidal', email: 'ines@kerf.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: 'kerf 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: 'Kerf Deep', short: 'Deep', provider: 'Hosted', description: 'Long investigations', capabilities: ['tools', 'logs'], context: '200k', cost: '$$$', }, { id: 'swift', name: 'Kerf 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' },]function createSender(turns: State<Turn[]>) { return (text: string) => { const answer = state(foldAnswer([])) turns.value = [ ...turns.value, { id: turns.value.length + 1, text, answer }, ] play(answer, ROLLBACK_ASK) }}Installation
npm install @kerf-js/veneerUsage
tsx
import { Conversation } from '@kerf-js/veneer'Empty chat
Starter cards until the first message.
tsx
import { state } from '@kerf-js/core'import { Conversation, Prompt, Stack, Suggestions, Text, UserMessage} from '@kerf-js/veneer'import type { PromptSuggestion } from '@kerf-js/veneer'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