Skip to content
kerfjsGitHub

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

Usage

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

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