- ds
- Agent
- Message row
Message row
A compact transcript turn: one text column for every role, state in words, actions on hover.
Thought for 12 seconds
The p95 jump starts at 09:14. The slow span is SELECT … FOR UPDATE, not the network: it holds the row lock for 840 ms while the retry queue drains.
3 tool calls · 2.6 s
grepdrainRetries services/checkoutDone180 ms
read_fileservices/checkout/retry.tsDone9 ms
edit_fileservices/checkout/retry.tsDone340 ms
| @@ -17,4 +17,4 @@ export async function charge(orderId: string) { | ||
| 17 | 17 | const order = await db.lock(orderId) |
| 18 | await drainRetries(order) | |
| 18 | queue.drainInBackground(order.id) | |
| 19 | 19 | await db.commit() |
I moved the drain behind the queue. Two things to check before you merge:
- The retry suite, which mocks
drainRetries. - The dashboard, for a lock wait above 50 ms.
Run the checkout tests
Medium riskpnpm test --filter checkout
14 passed, 2 skipped. The lock wait is now 31 ms at p95, under the 50 ms budget
Thought for 12 seconds
The p95 jump starts at 09:14. The slow span is SELECT … FOR UPDATE, not the network: it holds the row lock for 840 ms while the retry queue drains.
3 tool calls · 2.6 s
grepdrainRetries services/checkoutDone180 ms
read_fileservices/checkout/retry.tsDone9 ms
edit_fileservices/checkout/retry.tsDone340 ms
| @@ -17,4 +17,4 @@ export async function charge(orderId: string) { | ||
| 17 | 17 | const order = await db.lock(orderId) |
| 18 | await drainRetries(order) | |
| 18 | queue.drainInBackground(order.id) | |
| 19 | 19 | await db.commit() |
I moved the drain behind the queue. Two things to check before you merge:
- The retry suite, which mocks
drainRetries. - The dashboard, for a lock wait above 50 ms.
Run the checkout tests
Medium riskpnpm test --filter checkout
14 passed, 2 skipped. The lock wait is now 31 ms at p95, under the 50 ms budget
Thought for 12 seconds
The p95 jump starts at 09:14. The slow span is SELECT … FOR UPDATE, not the network: it holds the row lock for 840 ms while the retry queue drains.
3 tool calls · 2.6 s
grepdrainRetries services/checkoutDone180 ms
read_fileservices/checkout/retry.tsDone9 ms
edit_fileservices/checkout/retry.tsDone340 ms
| @@ -17,4 +17,4 @@ export async function charge(orderId: string) { | ||
| 17 | 17 | const order = await db.lock(orderId) |
| 18 | await drainRetries(order) | |
| 18 | queue.drainInBackground(order.id) | |
| 19 | 19 | await db.commit() |
I moved the drain behind the queue. Two things to check before you merge:
- The retry suite, which mocks
drainRetries. - The dashboard, for a lock wait above 50 ms.
Run the checkout tests
Medium riskpnpm test --filter checkout
14 passed, 2 skipped. The lock wait is now 31 ms at p95, under the 50 ms budget
Show code
<div class="mrows" role="log" aria-label="Transcript">
<article class="mrow" data-role="user" aria-label="You">
<span class="mrow-ava" aria-hidden="true">R</span>
<div class="mrow-body">Trace the slow checkout requests from this morning and tell me where the time goes.</div>
<div class="mrow-actions"><button class="btn icon" type="button" aria-label="Edit and resend"><svg class="i" viewBox="0 0 24 24" aria-hidden="true"><path d="M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z"/><path d="m15 5 4 4"/></svg></button><button class="btn icon" type="button" aria-label="Copy"><svg class="i" viewBox="0 0 24 24" aria-hidden="true"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></svg></button></div>
</article>
<article class="mrow" data-role="assistant" aria-label="axon">
<span class="mrow-ava" aria-hidden="true"><svg class="i" viewBox="0 0 24 24" aria-hidden="true"><path d="M11.017 2.814a1 1 0 0 1 1.966 0l1.051 5.558a2 2 0 0 0 1.594 1.594l5.558 1.051a1 1 0 0 1 0 1.966l-5.558 1.051a2 2 0 0 0-1.594 1.594l-1.051 5.558a1 1 0 0 1-1.966 0l-1.051-5.558a2 2 0 0 0-1.594-1.594l-5.558-1.051a1 1 0 0 1 0-1.966l5.558-1.051a2 2 0 0 0 1.594-1.594z"/><path d="M20 2v4"/><path d="M22 4h-4"/><circle cx="4" cy="20" r="2"/></svg></span>
<header class="mrow-head"><b class="mrow-name">axon</b><span class="mrow-meta"><time datetime="2026-10-21T09:14:44">09:14</time><span>sonnet-4.5</span><span>42 s</span><span>19.1k in, 1.2k out</span></span></header>
<div class="mrow-body">
<details class="reasoning dense"><summary><svg class="i" viewBox="0 0 24 24" aria-hidden="true"><path d="M12 18V5"/><path d="M15 13a4.17 4.17 0 0 1-3-4 4.17 4.17 0 0 1-3 4"/><path d="M17.598 6.5A3 3 0 1 0 12 5a3 3 0 1 0-5.598 1.5"/><path d="M17.997 5.125a4 4 0 0 1 2.526 5.77"/><path d="M18 18a4 4 0 0 0 2-7.464"/><path d="M19.967 17.483A4 4 0 1 1 12 18a4 4 0 1 1-7.967-.517"/><path d="M6 18a4 4 0 0 1-2-7.464"/><path d="M6.003 5.125a4 4 0 0 0-2.526 5.77"/></svg><span class="reasoning-title">Thought for 12 seconds</span><span class="reasoning-meta">640 tokens</span><svg class="i reasoning-chev" viewBox="0 0 24 24" aria-hidden="true"><path d="m9 18 6-6-6-6"/></svg></summary><div class="reasoning-body">The slow span is the row lock. Check whether the drain runs inside the transaction.</div></details>
<p>The p95 jump starts at 09:14. The slow span is <code>SELECT … FOR UPDATE</code>, not the network: it holds the row lock for <strong>840 ms</strong> while the retry queue drains.</p>
<details class="tcalls dense" open>
<summary><svg class="i" viewBox="0 0 24 24" aria-hidden="true"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/></svg><span><b>3 tool calls</b> · 2.6 s</span><svg class="i tcall-chev" viewBox="0 0 24 24" aria-hidden="true"><path d="m9 18 6-6-6-6"/></svg></summary>
<details class="tcall dense" data-state="ok"><summary><span class="tcall-ico"><svg class="i" viewBox="0 0 24 24" aria-hidden="true"><path d="m21 21-4.34-4.34"/><circle cx="11" cy="11" r="8"/></svg></span><span class="tcall-name">grep</span><span class="tcall-args">drainRetries services/checkout</span><span class="tcall-status"><span class="tcall-word">Done</span></span><span class="tcall-time">180 ms</span><svg class="i tcall-chev" viewBox="0 0 24 24" aria-hidden="true"><path d="m9 18 6-6-6-6"/></svg></summary></details>
<details class="tcall dense" data-state="ok"><summary><span class="tcall-ico"><svg class="i" viewBox="0 0 24 24" aria-hidden="true"><path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/></svg></span><span class="tcall-name">read_file</span><span class="tcall-args">services/checkout/retry.ts</span><span class="tcall-status"><span class="tcall-word">Done</span></span><span class="tcall-time">9 ms</span><svg class="i tcall-chev" viewBox="0 0 24 24" aria-hidden="true"><path d="m9 18 6-6-6-6"/></svg></summary></details>
<details class="tcall dense" data-state="ok" open><summary><span class="tcall-ico"><svg class="i" viewBox="0 0 24 24" aria-hidden="true"><path d="M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z"/><path d="m15 5 4 4"/></svg></span><span class="tcall-name">edit_file</span><span class="tcall-args">services/checkout/retry.ts</span><span class="tcall-status"><span class="tcall-word">Done</span></span><span class="tcall-time">340 ms</span><svg class="i tcall-chev" viewBox="0 0 24 24" aria-hidden="true"><path d="m9 18 6-6-6-6"/></svg></summary><div class="tcall-body"><div class="tcall-diff"><div class="diff dense">
<div class="diff-file"><span class="path">services/checkout/retry.ts</span><span class="diff-n"><span class="add">+2</span><span class="del">−2</span></span></div>
<div class="diff-scroll"><table>
<tr class="hunk"><td colspan="3">@@ -17,4 +17,4 @@ export async function charge(orderId: string) {</td></tr>
<tr><td class="ln">17</td><td class="ln">17</td><td class="cd"> const order = await db.lock(orderId)</td></tr>
<tr class="del"><td class="ln del">18</td><td class="ln"></td><td class="cd del"> await drainRetries(order)</td></tr>
<tr class="add"><td class="ln"></td><td class="ln add">18</td><td class="cd add"> queue.drainInBackground(order.id)</td></tr>
<tr><td class="ln">19</td><td class="ln">19</td><td class="cd"> await db.commit()</td></tr>
</table></div>
</div></div></div></details>
</details>
<p>I moved the drain behind the queue. Two things to check before you merge:</p>
<ol><li>The retry suite, which mocks <code>drainRetries</code>.</li><li>The dashboard, for a lock wait above 50 ms.</li></ol>
</div>
<div class="mrow-actions"><button class="btn icon" type="button" aria-label="Copy"><svg class="i" viewBox="0 0 24 24" aria-hidden="true"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></svg></button><button class="btn icon" type="button" aria-label="Retry"><svg class="i" viewBox="0 0 24 24" aria-hidden="true"><path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"/><path d="M21 3v5h-5"/><path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16"/><path d="M8 16H3v5"/></svg></button><button class="btn icon" type="button" aria-label="Fork from here"><svg class="i" viewBox="0 0 24 24" aria-hidden="true"><line x1="6" x2="6" y1="3" y2="15"/><circle cx="18" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><path d="M18 9a9 9 0 0 1-9 9"/></svg></button></div>
</article>
<article class="mrow" data-role="assistant" aria-label="axon, needs approval">
<span class="mrow-ava" aria-hidden="true"><svg class="i" viewBox="0 0 24 24" aria-hidden="true"><path d="M11.017 2.814a1 1 0 0 1 1.966 0l1.051 5.558a2 2 0 0 0 1.594 1.594l5.558 1.051a1 1 0 0 1 0 1.966l-5.558 1.051a2 2 0 0 0-1.594 1.594l-1.051 5.558a1 1 0 0 1-1.966 0l-1.051-5.558a2 2 0 0 0-1.594-1.594l-5.558-1.051a1 1 0 0 1 0-1.966l5.558-1.051a2 2 0 0 0 1.594-1.594z"/><path d="M20 2v4"/><path d="M22 4h-4"/><circle cx="4" cy="20" r="2"/></svg></span>
<header class="mrow-head"><b class="mrow-name">axon</b><span class="mrow-meta"><span class="warn">Needs approval</span><span>09:15</span></span></header>
<div class="mrow-body">
<section class="approval dense" data-risk="medium" aria-label="Approval: run the checkout tests">
<div class="approval-head"><span class="approval-ico"><svg class="i" viewBox="0 0 24 24" aria-hidden="true"><path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z"/></svg></span><h3 class="approval-title">Run the checkout tests</h3><span class="approval-risk">Medium risk</span></div>
<pre class="approval-cmd"><code>pnpm test --filter checkout</code></pre>
<div class="approval-actions"><button class="btn primary" type="button">Allow once</button><button class="btn" type="button">Allow for session</button><button class="btn ghost danger" type="button">Deny</button></div>
</section>
</div>
</article>
<article class="mrow" data-role="assistant" data-state="streaming" aria-label="axon, writing" aria-busy="true">
<span class="mrow-ava" aria-hidden="true"><svg class="i" viewBox="0 0 24 24" aria-hidden="true"><path d="M11.017 2.814a1 1 0 0 1 1.966 0l1.051 5.558a2 2 0 0 0 1.594 1.594l5.558 1.051a1 1 0 0 1 0 1.966l-5.558 1.051a2 2 0 0 0-1.594 1.594l-1.051 5.558a1 1 0 0 1-1.966 0l-1.051-5.558a2 2 0 0 0-1.594-1.594l-5.558-1.051a1 1 0 0 1 0-1.966l5.558-1.051a2 2 0 0 0 1.594-1.594z"/><path d="M20 2v4"/><path d="M22 4h-4"/><circle cx="4" cy="20" r="2"/></svg></span>
<header class="mrow-head"><b class="mrow-name">axon</b><span class="mrow-meta"><span class="accent">Streaming</span><span>sonnet-4.5</span></span></header>
<div class="mrow-body" aria-live="polite"><p>14 passed, 2 skipped. The lock wait is now 31 ms at p95, under the 50 ms budget<span class="chat-caret" aria-hidden="true"></span></p></div>
</article>
<article class="mrow" data-role="assistant" data-state="error" aria-label="axon, failed">
<span class="mrow-ava" aria-hidden="true"><svg class="i" viewBox="0 0 24 24" aria-hidden="true"><path d="M11.017 2.814a1 1 0 0 1 1.966 0l1.051 5.558a2 2 0 0 0 1.594 1.594l5.558 1.051a1 1 0 0 1 0 1.966l-5.558 1.051a2 2 0 0 0-1.594 1.594l-1.051 5.558a1 1 0 0 1-1.966 0l-1.051-5.558a2 2 0 0 0-1.594-1.594l-5.558-1.051a1 1 0 0 1 0-1.966l5.558-1.051a2 2 0 0 0 1.594-1.594z"/><path d="M20 2v4"/><path d="M22 4h-4"/><circle cx="4" cy="20" r="2"/></svg></span>
<header class="mrow-head"><b class="mrow-name">axon</b><span class="mrow-meta"><span class="down">Failed</span><span>09:17</span></span></header>
<div class="mrow-body"><div class="chat-note down" role="alert"><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>The provider closed the stream after 30 s.</span><button class="btn mini" type="button" style="margin-inline-start:auto">Retry</button></div></div>
</article>
<article class="mrow" data-role="system" aria-label="Note">
<span class="mrow-ava" aria-hidden="true" style="--c:var(--muted);--w:var(--bg-sunk)"><svg class="i" viewBox="0 0 24 24" aria-hidden="true"><path d="M13 13.74a2 2 0 0 1-2 0L2.5 8.87a1 1 0 0 1 0-1.74L11 2.26a2 2 0 0 1 2 0l8.5 4.87a1 1 0 0 1 0 1.74z"/><path d="m20 14.285 1.5.845a1 1 0 0 1 0 1.74L13 21.74a2 2 0 0 1-2 0l-8.5-4.87a1 1 0 0 1 0-1.74l1.5-.845"/></svg></span>
<div class="mrow-body">Compacted 64 earlier turns into a 2.1k summary.</div>
</article>
<article class="mrow" data-role="user" data-state="queued" aria-label="You, queued">
<span class="mrow-ava" aria-hidden="true">R</span>
<header class="mrow-head"><b class="mrow-name">You</b><span class="mrow-meta"><span>Queued</span><span>sends when the turn ends</span></span></header>
<div class="mrow-body">Also check whether the webhook relay has the same lock pattern: services/event-relay/internal/deliver/relay_with_a_very_long_file_name_that_does_not_break.go</div>
</article>
</div>A message row is one turn of a dense transcript, the way a coding agent reads: a 16px mark in a gutter, a 16px head with the name and the facts, and the body. Every role shares one text column, so a long session reads as one straight edge. User turns are a filled row; assistant turns are open; system notes are quiet and small.
Prose is 13px on 20px lines and code 11.5px. Reasoning, tool calls, approvals and diffs sit in the body in their dense form. A run of tool calls folds into one .tcalls frame with a summary, so the transcript stays readable after forty calls.
Actions
The selected row keeps its actions in view.
The selected row keeps its actions in view.
Code
<article class="mrow" data-role="assistant" aria-selected="true" aria-label="axon">
<span class="mrow-ava" aria-hidden="true"><svg class="i" viewBox="0 0 24 24" aria-hidden="true"><path d="M11.017 2.814a1 1 0 0 1 1.966 0l1.051 5.558a2 2 0 0 0 1.594 1.594l5.558 1.051a1 1 0 0 1 0 1.966l-5.558 1.051a2 2 0 0 0-1.594 1.594l-1.051 5.558a1 1 0 0 1-1.966 0l-1.051-5.558a2 2 0 0 0-1.594-1.594l-5.558-1.051a1 1 0 0 1 0-1.966l5.558-1.051a2 2 0 0 0 1.594-1.594z"/><path d="M20 2v4"/><path d="M22 4h-4"/><circle cx="4" cy="20" r="2"/></svg></span>
<header class="mrow-head"><b class="mrow-name">axon</b><span class="mrow-meta"><time>09:14</time><span>sonnet-4.5</span><span>42 s</span></span></header>
<div class="mrow-body"><p>The selected row keeps its actions in view.</p></div>
<div class="mrow-actions"><button class="btn icon" type="button" aria-label="Copy"><svg class="i" viewBox="0 0 24 24" aria-hidden="true"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></svg></button><button class="btn icon" type="button" aria-label="Retry"><svg class="i" viewBox="0 0 24 24" aria-hidden="true"><path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"/><path d="M21 3v5h-5"/><path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16"/><path d="M8 16H3v5"/></svg></button><button class="btn" type="button"><svg class="i" viewBox="0 0 24 24" aria-hidden="true"><line x1="6" x2="6" y1="3" y2="15"/><circle cx="18" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><path d="M18 9a9 9 0 0 1-9 9"/></svg>Fork</button></div>
</article>Copy, retry and fork float in at the top right on hover or focus, on a small surface so they never move the text. A selected row (aria-selected="true") keeps them shown. On touch screens they sit under the body instead, always visible. Buttons inside are 24px.
State
State is a word in the meta, and the row repeats it: data-state="streaming" with a caret, queued dims a user turn that will send when the current one ends, error adds a 2px red rule and an inline note with Retry. Give the meta word a tone class (.ok, .warn, .down) to match.
API
| Class | On | What it does |
|---|---|---|
.mrows | div | The transcript column, 4px between rows. Give it role="log". |
.mrow | article | One turn: a 16px gutter and the text column, 6px by 8px padding, 6px radius. |
data-role | .mrow | assistant (open), user (filled, keeps line breaks), system (11px, muted). |
.mrow-ava | span | The 16px mark: an icon or one letter. Set --c and --w for its tone. |
.mrow-head | header | The 16px head. Leave it out on user turns unless there is a state to say. |
.mrow-name | b | The speaker, 12px 600. |
.mrow-meta | span | Facts in 10.5px mono with middots: time, model, duration, tokens. The last one truncates. |
.ok, .warn, .down | .mrow-meta > span | Tone for the state word. |
.mrow-body | div | Prose at 13px on 20px. Headings, lists, inline code, code blocks and tables at the dense scale. |
.mrow-actions | div | Row actions. Hidden until hover or focus; inline on touch. |
data-state | .mrow | streaming, queued (dimmed), error (a 2px red rule). |
aria-selected, aria-current | .mrow | A selected or jumped-to row: an accent wash and rule, actions shown. |
.tcalls | details, div, ul | A run of tool calls in one frame, with an optional summary that folds them. |
- One text column for every role, whatever the width.
- The model, the time and the cost in the head, once.
- Fold a run of tool calls under a count.
- Right-aligned bubbles in a workbench. They waste the left half of a wide column.
- An avatar larger than the head line.
- Actions that push the text when they appear.
Accessibility
- The column is a
role="log"; each turn is anarticlewith a name. Mark the streaming bodyaria-live="polite"and the rowaria-busy="true"until it ends. - The mark and the caret are decorative and hidden. The speaker is the visible name or the article label.
- Actions are real buttons with labels. They appear on focus as well as hover, so keyboard users reach them in order.
- Every state has a word. The rule and the colours repeat it.
- Long paths and URLs wrap anywhere in prose and scroll in code blocks, so nothing overflows at 390px.