v1.2.0
  1. ds
  2. Agent
  3. 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()
2 matches3 files searched
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'
exit 1nothing read
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
services/checkout/retry.ts+3−2
@@ -17,7 +17,8 @@ export async function charge(orderId: string) {
1717 const order = await db.lock(orderId)
18 await drainRetries(order)
19 await db.commit()
18 await queue.drainInBackground(order)
19 await db.commit()
2020 return order.id
1 file+3 −2
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

ClassOnWhat it does
.tcalldetailsThe row. data-state: running, ok, error.
.tcall > summarysummaryIcon, name, arguments, status, duration, chevron.
.tcall-icospanThe tool glyph on a wash of the state colour.
.tcall-namespanThe tool name, mono and ink.
.tcall-argsspanThe argument that matters: the command, the pattern, the path. Truncates.
.tcall-statusspanA dot and a word. The word can be .tcall-word for the small-screen fold.
.tcall-timespanDuration in mono, tabular.
.tcall-chevsvgRotates when the row opens.
.tcall-bodydivOutput, a diff slot, and a footer.
.tcall-outpreScrollable text output.
.tcall-diffdivWrap a .diff here to drop its frame and bleed it to the edge.
.tcall-footdivExit code, match count, line counts.
Do
  • 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.
Don't
  • 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 details element. 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.