Tool Card
Renders a tool call and its result from a declared intent rather than from markup.
Use when you are showing what an agent or a job actually did — a shell command, a file edit, a lookup — and want the tool to say what it is while the surface decides what it looks like.
A tool that returns HTML has picked a renderer, a theme, and a layout on the UI's behalf, and it does so in the one place — the model-facing result — where UI concerns do not belong. Declaring an intent keeps the two apart: the same call can render as a terminal block here, a single line in a compact transcript, and a jump target in an editor, without the tool knowing any of them exist.
Quick Start
<r-tool-card open></r-tool-card>const card = document.querySelector('r-tool-card');
card.call = { card: 'terminal', title: 'pnpm test', cwd: '/repo' };
card.status = 'running';
// …when the call returns
card.result = { card: 'terminal', output: '2351 passed', exitCode: 0 };
card.status = 'success';Card kinds
generic
The default, and the fallback. Title, an optional key/value table of arguments worth showing, and optional result content.
card.call = { card: 'generic', title: 'Read file', input: { path: 'src/a.ts', limit: '200' } };
card.result = { card: 'generic', content: 'export const a = 1;' };terminal
The call is a shell command. title is the command; description and cwd render above the output. A non-zero exitCode is surfaced; zero is not.
card.call = { card: 'terminal', title: 'ls -la', description: 'List the tree', cwd: '/repo' };
card.result = { card: 'terminal', output: 'total 8\ndrwxr-xr-x …', exitCode: 0 };diff
The call creates or modifies files. Each entry renders as unified-style hunks with both gutters, computed by diffLines from ranuts/utils. A null oldText means the file is being created — which is what a call-time view has, since a caller has no prior content to read.
card.call = {
card: 'diff',
title: 'Edit config',
diffs: [{ path: 'vite.config.ts', oldText: 'port: 3000\n', newText: 'port: 5173\n' }],
};Two rules that bite
These views are computed on a live call and again when a log is replayed. Everything else follows from that.
- A view is a pure function of the call's arguments (plus the result, for a result view). No I/O, no clock, no session state — otherwise a replay disagrees with what the user originally saw.
- An unrecognised card degrades, it never throws. A card kind from a newer producer, or a value mangled in storage, renders as
genericwith whatever title it has, and a malformed view renders empty. Display must not be able to break a replay.
Locations
Any locations on a call render as buttons that fire locationclick, so an editor can follow along:
card.call = { card: 'generic', title: 'Read', locations: [{ path: 'src/a.ts', line: 42 }] };
card.addEventListener('locationclick', (e) => openInEditor(e.detail.location));API Reference
Properties
| Property | Type | Default | Description |
|---|---|---|---|
call | ToolCallView | null | null | The pending view, derived from the call's arguments. |
result | ToolResultView | null | null | The completed view. Replaces the pending one. |
status | 'running' | 'success' | 'error' | 'running' | Reflected, so styling can key off it. |
open | boolean | false | Whether the body is expanded. |
sheet | string | '' | CSS injected into the element's shadow DOM. |
An unknown status value reads back as running.
Events
| Event | Detail | Fired when |
|---|---|---|
locationclick | { location: ToolLocation } | A file reference is activated. |
Parts
card, header, status, title, toggle, body, description, exit, input, output, file, path, hunk, line, locations, location.
Diff lines carry data-kind of context, added, or removed.
Accessibility
The header is a real <button type="button"> with aria-expanded, so it is reachable and operable from the keyboard without any extra wiring.
See also
- Conversation — use this as the
mounttarget for a tool-call view - ranuts/utils —
diffLines, which renders thediffcard