- ds
- Agent
- Tool call
Tool call
Inline tool use with state, duration, output and a diff slot.
bash pnpm test --filter checkout Running 12 s
RUN v3.2.1 /Users/me/work/checkout ✓ checkout/lock.test.ts (6 tests) ✓ checkout/retry.test.ts (8 tests) ↓ checkout/webhook.test.ts (2 skipped)
grep drainRetries services/checkout Done 180 ms
services/checkout/retry.ts:19: await drainRetries() services/checkout/retry.test.ts:41: expect(drainRetries).toHaveBeenCalled()
read_file services/checkout/orders_read.sql Failed 8 ms
ENOENT: no such file or directory open '/Users/me/work/checkout/services/checkout/orders_read.sql'
Show code
<details class="tcall" data-state="running" open>
<summary>
<span class="tcall-ico"><svg class="i" viewBox="0 0 24 24" aria-hidden="true"><path d="M12 19h8"/><path d="m4 17 6-6-6-6"/></svg></span>
<span class="tcall-name">bash</span>
<span class="tcall-args">pnpm test --filter checkout</span>
<span class="tcall-status"><span class="tcall-word">Running</span></span>
<span class="tcall-time">12 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>
<div class="tcall-body"><pre class="tcall-out">RUN v3.2.1 /Users/me/work/checkout
✓ checkout/lock.test.ts (6 tests)
✓ checkout/retry.test.ts (8 tests)
↓ checkout/webhook.test.ts (2 skipped)</pre></div>
</details>
<details class="tcall" 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>
<div class="tcall-body">
<pre class="tcall-out">services/checkout/retry.ts:19: await drainRetries()
services/checkout/retry.test.ts:41: expect(drainRetries).toHaveBeenCalled()</pre>
<div class="tcall-foot"><span><b>2</b> matches</span><span>3 files searched</span></div>
</div>
</details>
<details class="tcall" data-state="error" open>
<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/orders_read.sql</span>
<span class="tcall-status"><span class="tcall-word">Failed</span></span>
<span class="tcall-time">8 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">
<pre class="tcall-out">ENOENT: no such file or directory
open '/Users/me/work/checkout/services/checkout/orders_read.sql'</pre>
<div class="tcall-foot"><span>exit <b>1</b></span><span>nothing read</span></div>
</div>
</details>One tool use is one row in the conversation, not a card that interrupts it. The icon says which kind of tool, the name and the argument summary say what it touched, and the right side says how it went and how long it took. The body opens on the native details element, so it works without a script.
The left icon and the status dot share --c, set by data-state. Running also adds a thin scan line across the top. Status is always a word, never a colour alone. At 390px the duration and the status word fold away; the dot and the argument summary remain.
Diff slot
edit_file services/checkout/retry.ts Done 340 ms
| @@ -17,7 +17,8 @@ export async function charge(orderId: string) { | ||
| 17 | 17 | const order = await db.lock(orderId) |
| 18 | await drainRetries(order) | |
| 19 | await db.commit() | |
| 18 | await queue.drainInBackground(order) | |
| 19 | await db.commit() | |
| 20 | 20 | return order.id |
Show code
<details class="tcall" 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">
<div class="diff-file"><span class="path">services/checkout/retry.ts</span><span class="diff-n"><span class="add">+3</span><span class="del">−2</span></span></div>
<div class="diff-scroll"><table>
<tr class="hunk"><td colspan="3">@@ -17,7 +17,8 @@ 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="del"><td class="ln del">19</td><td class="ln"></td><td class="cd del"> await db.commit()</td></tr>
<tr class="add"><td class="ln"></td><td class="ln add">18</td><td class="cd add"> await queue.drainInBackground(order)</td></tr>
<tr class="add"><td class="ln"></td><td class="ln add">19</td><td class="cd add"> await db.commit()</td></tr>
<tr><td class="ln">20</td><td class="ln">20</td><td class="cd"> return order.id</td></tr>
</table></div>
</div>
</div>
<div class="tcall-foot"><span><b>1</b> file</span><span><b class="accent">+3</b> <b class="down">−2</b></span></div>
</div>
</details>An edit gets a diff instead of raw output. Put the existing diff inside .tcall-diff: the slot removes the diff's own frame and bleeds it to the card edge, so the change reads as part of the tool row. The footer still carries the file count and the line counts.
API
| Class | On | What it does |
|---|---|---|
.tcall | details | The row. data-state: running, ok, error. |
.tcall > summary | summary | Icon, name, arguments, status, duration, chevron. |
.tcall-ico | span | The tool glyph on a wash of the state colour. |
.tcall-name | span | The tool name, mono and ink. |
.tcall-args | span | The argument that matters: the command, the pattern, the path. Truncates. |
.tcall-status | span | A dot and a word. The word can be .tcall-word for the small-screen fold. |
.tcall-time | span | Duration in mono, tabular. |
.tcall-chev | svg | Rotates when the row opens. |
.tcall-body | div | Output, a diff slot, and a footer. |
.tcall-out | pre | Scrollable text output. |
.tcall-diff | div | Wrap a .diff here to drop its frame and bleed it to the edge. |
.tcall-foot | div | Exit code, match count, line counts. |
- Put the decision-making argument in the summary: the command or the path.
- Always show the duration once the call has finished.
- Use the diff slot for edits and the text slot for reads and commands.
- RAW JSON as the only body. Render the shape that matters.
- A spinner that is the only running signal.
- A failed row that opens to an empty body. Show the error.
Accessibility
- The row is a
detailselement. Enter and Space open it, and the summary keeps focus. - The status word is part of the summary text, so it is read with the tool name and arguments. The icon is
aria-hidden. - The scan line is decorative and stops under reduced motion. The dot still pulses only while running; the word says "Running".
- Wrap long output and keep the text selectable. Do not trap horizontal scrolling inside the row.