v1.2.0
  1. ds
  2. Agent
  3. Chat thread

Chat thread

User and assistant turns with metadata and a streaming caret.

Trace the slow checkout requests from this morning and tell me where the time goes.
Aaxonread 12 files

The p95 jump starts at 09:14. Two things overlap: the retry burst from event-relay and the new index scan in orders_read.

I traced the checkout path from the edge to Postgres. The slow span is SELECT … FOR UPDATE, not the network. It holds a row lock for 840 ms while the retry queue drains.

sonnet-4.518.4k in1.2k out42 s
Aaxonwriting

Move the drain behind the queue and keep the lock short. That takes the p95 back under 300 ms without touching the edge.

sonnet-4.519.1k instreaming
Show code
<div class="chat">
  <div class="chat-thread">
    <div class="chat-turn chat-user">
      <div class="chat-bubble">Trace the slow checkout requests from this morning and tell me where the time goes.</div>
    </div>
    <div class="chat-turn">
      <div class="chat-assistant">
        <div class="chat-author"><span class="avatar t-purple xs">A</span><b>axon</b><span>read 12 files</span></div>
        <div class="chat-body">
          <p>The p95 jump starts at 09:14. Two things overlap: the retry burst from <code>event-relay</code> and the new index scan in <code>orders_read</code>.</p>
          <p>I traced the checkout path from the edge to Postgres. The slow span is <code>SELECT … FOR UPDATE</code>, not the network. It holds a row lock for 840 ms while the retry queue drains.</p>
        </div>
        <div class="chat-meta"><span>sonnet-4.5</span><span>18.4k in</span><span>1.2k out</span><span>42 s</span></div>
      </div>
    </div>
    <div class="chat-turn">
      <div class="chat-assistant" aria-live="polite" aria-busy="true">
        <div class="chat-author"><span class="avatar t-purple xs">A</span><b>axon</b><span>writing</span></div>
        <div class="chat-body"><p>Move the drain behind the queue and keep the lock short. That takes the p95 back under 300 ms without touching the edge.<span class="chat-caret" aria-hidden="true"></span></p></div>
        <div class="chat-meta"><span>sonnet-4.5</span><span>19.1k in</span><span>streaming</span></div>
      </div>
    </div>
  </div>
</div>

A conversation is a column of turns, not a chat window with a second border. User turns sit on the right in a single quiet surface. Assistant turns run the full measure: a small author line, the prose, then the metadata that says which model answered and what it cost.

The caret is a span at the end of the last paragraph. Keep it aria-hidden and mark the live turn with aria-live="polite" on the container. It stops when the stream ends.

Compact density

Run the checkout tests.
Aaxon

14 passed, 2 skipped. The retry suite is green.

2.4 s412 tokens
Code
<div class="chat compact">
  <div class="chat-thread">
    <div class="chat-turn chat-user"><div class="chat-bubble">Run the checkout tests.</div></div>
    <div class="chat-turn">
      <div class="chat-assistant">
        <div class="chat-author"><span class="avatar t-purple xs">A</span><b>axon</b></div>
        <div class="chat-body"><p>14 passed, 2 skipped. The retry suite is green.</p></div>
        <div class="chat-meta"><span>2.4 s</span><span>412 tokens</span></div>
      </div>
    </div>
  </div>
</div>

.compact tightens the gap, the bubble padding and the type. Use it in a side panel or when a session has hundreds of short turns. The order and the metadata stay the same.

Empty, loading, error

axon

What should we work on?

A local agent with tools and a session log. Start with the repository, or ask for a change.

Reading 12 files
Tool call failed: the workspace is read-only.
Code
<div class="chat">
  <div class="chat-empty">
    <span class="chat-empty-mark">axon</span>
    <h3>What should we work on?</h3>
    <p>A local agent with tools and a session log. Start with the repository, or ask for a change.</p>
    <div class="btn-row">
      <button class="btn sm primary" type="button">Read the README</button>
      <button class="btn sm" type="button">Run the tests</button>
    </div>
  </div>
  <div class="chat-note center"><span class="chat-typing"><i></i><i></i><i></i></span> Reading 12 files</div>
  <div class="chat-note down"><svg class="i" viewBox="0 0 24 24" aria-hidden="true"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3"/><path d="M12 9v4"/><path d="M12 17h.01"/></svg><span>Tool call failed: the workspace is read-only.</span></div>
</div>

The empty state offers two things to do, not a slogan. Loading is a live note with three dots. An error is one line, in the error colour, with the cause. A failed tool call belongs in a tool call card; the chat note is for the turn itself.

API

ClassOnWhat it does
.chatdivThe conversation. Holds a thread, or an empty and note column.
.chat-threaddivThe measured column of turns, capped at --prose.
.chat-turndivOne turn. Add .chat-user for the right-aligned form.
.chat-bubbledivThe user message surface. Keeps line breaks.
.chat-assistantdivThe assistant turn: author, body, metadata.
.chat-authordivAvatar, name and the current activity.
.chat-bodydivProse. Headings, lists and inline code inherit the ds scale.
.chat-metadivModel, tokens and duration in mono, separated by middots.
.chat-caretspanThe streaming block cursor. Decorative, so hide it from assistive tech.
.chat-typingspanThree animated dots inside a live note.
.chat-emptydivThe first-run state. A mark, a question, a line and two actions.
.chat-notedivA system line under the thread. .down for errors.
.compact.chatTighter gaps, padding and type.
Do
  • Keep one measure for the thread, even on a wide screen.
  • Show the model and token counts after the turn, not before it.
  • Let the last paragraph carry the caret.
Don't
  • Nested bubbles for tool calls and reasoning. Use the inline components.
  • A second avatar on every assistant turn in a long session.
  • An empty state with no action.

Accessibility

  • The thread is plain reading order. Do not move focus when a turn arrives; announce the live turn with aria-live="polite".
  • The caret and the typing dots are decorative. Give the live note a text label, such as "Reading 12 files".
  • At 390px the bubble becomes full width and the middot separators fold away. The metadata still wraps, so no value is lost.
  • Reduced motion stops the caret and the typing dots at a readable state.