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.createElement('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';

conversation.append(card);

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 generic with 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.

Styling

<r-tool-card> exposes 24 CSS custom properties of its own, plus the semantic tokens it reads from the theme. Set one anywhere it inherits from: :root, a wrapper, or the element:

r-tool-card {
  --ran-tool-card-io-background: var(--ran-color-bg-subtle);
}

Parts: body · exit · file · hunk · io · io-text · line · location · locations · path · row

The full list is in style tokens; which token to reach for is the design system.

See also