Data Display

ToolCallRail

One turn's tool calls on a rail: a one-line row per call, the first and last few with the middle folded, failures and calls that need a person always in view, and the run's end state.

import { ToolCallRail } from '@lostgradient/cinder';
timelinetool-callagent
01

Overview

Reference usage

One turn's tool calls on a rail. Each call is one line that never wraps: the tool's name as a person reads it (its identifier is a tooltip), the muted key input, → result, and the duration at the end. A long turn keeps its first and last few calls and folds the middle behind one dashed button that says what it hides. A call that failed, was denied, is waiting on a person or is still running never folds, and a failed call carries one danger line with its consequence and quiet links instead of a filled error block.

Use ToolCallRail for the tool calls of a chat turn, where the agent's reply leads and the calls follow. Use RunStepTimeline for the steps of a run with their own timing, retries and branches, and Timeline for dated events. The rail is built on Timeline's rows variant, because run pages and chat read the same kind of sequence: Timeline draws the marker column and the connector, and the rail draws what sits in each row, since a tool call has no timestamp to lead with. A folded group is one entry of its own.

Usage

svelte
<script lang="ts">
  import { ToolCallRail } from '@lostgradient/cinder';
  import type { ToolCallRailCall } from '@lostgradient/cinder';

  const calls: ToolCallRailCall[] = [
    {
      id: 'search',
      name: 'Search orders',
      identifier: 'search-orders',
      input: 'carrier Parcelly, damaged',
      result: '14 orders',
      duration: '0.6s',
      status: 'done',
    },
    {
      id: 'lookup',
      name: 'Lookup order',
      identifier: 'lookup-order',
      input: '4471',
      result: '$42.00, delivered Mon',
      duration: '0.2s',
      status: 'done',
    },
    {
      id: 'refund',
      name: 'Issue refund',
      identifier: 'issue-refund',
      input: '4480, $27.25',
      status: 'waiting',
    },
  ];
</script>

<ToolCallRail {calls} duration="1.2s" />

How many calls

  • One or two calls are plain rows: no Used N tools line, no rail connector and no fold.
  • From three calls the summary line appears (Used 3 tools) and every call still shows. It adds 1 failed and 1 waiting on you after commas, each with its dot, only when such calls exist, and the turn's duration when you pass one.
  • From eight calls the middle folds: the first 3 and the last 3 stay, with Show 2 more, 2 done: Lookup order ×2 between them. A folded group hides at least two calls, because hiding one saves nothing, so seven calls show all seven.
  • Calls that failed, were denied, wait on a person or are running are pinned: they stay on the rail wherever they fall and split the folded middle into separate groups, each counting only the calls it hides. The group's summary lists the three names that appear most (×N when more than once) and counts every other call as 6 others.
  • Show N more opens that group in place, in time order, and a Show fewer button after the last row folds every open group again.
  • Closing the summary line (open={false}) leaves the summary and the pinned calls, so nothing that needs a person is ever folded away.
  • While the turn running, the summary reads Used 12 tools so far… with a spinner and no counts, and the rail keeps the first 3 calls and the ones under way instead of the last 3.

Call states

status picks the glyph on the rail and the words:

  • done: a neutral grey dot, the result after an arrow.
  • approved: a green dot, for a call a person approved. decision writes what was decided in the person's words, without an arrow (Approved by you 10:33 AM, $18.50 refunded); without it the row shows result after an arrow like a done call.
  • running: a spinner in place of the dot, Running… in place of a result, and the elapsed time.
  • waiting: an amber shield and Waiting on you, as its own row.
  • denied: a prohibition glyph and neutral words. It is not called failed and carries no error block.
  • interrupted: a red circled cross and the result in red with no arrow, for a call the run ended on before it answered. Word it by the tool's risk: Interrupted, didn't finish for a tool that only reads, Interrupted, may have run for one that changes things. It never folds and is never a green or neutral dot.
  • failed: a red cross and the result in red. failure adds one danger-text line with an error glyph and the quiet links Details (when onSelect is set), Ask the agent to retry (onRetry) and Copy input (onCopyInput).

Selecting a row and nesting an approval

Set onSelect and every row is a button. The row whose id is selectedId is tinted and keeps the quiet focus ring while its panel is open. Call focusCall(id) on the component to hand focus back to a row when the panel closes.

A call with resultHref shows its result (the arrow and the words) as a link to that address, such as the page of a workflow the call created. A link cannot sit inside a button, so with onSelect such a row is a container with a button that covers it and the link above the button: the link is its own click target, and a click anywhere else on the row selects the call.

A call with nested: true draws the children snippet under its row: the approval a waiting call is waiting on, or the decided approval under a call that has been approved. Nothing gates it on the call's status.

svelte
<script lang="ts">
  import { ApprovalCard, ToolCallRail } from '@lostgradient/cinder';

  let selectedId = $state<string | undefined>(undefined);
</script>

<ToolCallRail
  calls={[
    {
      id: 'refund',
      name: 'Issue refund',
      identifier: 'issue-refund',
      input: '4480, $27.25',
      status: 'waiting',
      nested: true,
    },
  ]}
  {selectedId}
  onSelect={(id) => (selectedId = id)}
>
  {#snippet children(call)}
    <ApprovalCard
      layout="row"
      tool={{ name: call.identifier ?? call.id, label: call.name, risk: 'high' }}
      question="Refund order 4480 for $27.25?"
      operation={{ kind: 'other' }}
      policyVersion="policy-1"
      idempotencyKey="approval-4480"
      state="pending"
    />
  {/snippet}
</ToolCallRail>

Narrow containers

In a container narrower than 28rem (a phone) each row has two lines: the name and duration on line one, and a muted line of key input and result, cut with an ellipsis, on line two. The summary drops the duration, the dashed button keeps Show 24 more beside the short 24 done, and the failure line gives way to the call's panel.

Props

plaintext
calls        ToolCallRailCall[]      required  The calls of one turn, in order.
running      boolean                 optional  The turn is still running. Defaults to false.
duration     string                  optional  How long the turn took, formatted by the caller.
open         boolean (bindable)      optional  Whether the rail is open. Defaults to true.
selectedId   string                  optional  The call whose panel is open.
onSelect     (id) => void            optional  Makes rows buttons; fired with the call's id.
onRetry      (id) => void            optional  Shows "Ask the agent to retry" on failed calls.
onCopyInput  (id) => void            optional  Shows "Copy input" on failed calls.
endLabel     string                  optional  The run's end state on its own line ("Run finished, no reply").
footnote     string                  optional  A muted line under the rail.
label        string                  optional  Accessible name. Defaults to "Tool calls".
labels       ToolCallRailLabels      optional  The rail's words, for another language.
class        string                  optional  Extra class names.
children     Snippet<[call]>         optional  Content under the rows of calls that set `nested`.

ToolCallRailCall shape

plaintext
id          string   required  Stable identity.
name        string   required  The tool's name as a person reads it.
identifier  string   optional  The tool's identifier, in a tooltip on the name.
input       string   optional  The key input, muted and truncated.
result      string   optional  What came back, in one line.
decision    string   optional  What a person decided, in their own words and without an arrow.
resultHref  string   optional  Where the result goes; the result on the row becomes a link to it.
duration    string   optional  How long it took, formatted by the caller.
status      done | approved | running | waiting | denied | failed | interrupted   required
failure     string   optional  A failed call's consequence, in one danger line.
nested      boolean  optional  Draw `children` under this row.
Live preview

An approved call with its approval beneath it

  1. Approved: Create workflow Morning briefing → Morning briefing 0.8s

    Create workflow "Morning briefing"?

    create-workflow Changes things

    Input Name Morning briefing Schedule Every day at 9:00 AM Central time

    Approved by you 4:42 PM

02

When to use

Use when
  • Showing an agent's tool calls for one chat turn, where a failure or an approval must never fold away.
  • Opening a call's details in a side panel from a selectable row, with a failed call's next steps on the rail.
Avoid when
  • Showing the steps of a run with their own timing, retries and branches — use run-step-timeline.
  • Listing dated events — use timeline.
03

Examples

An approved call with its approval beneath it

A tool that changes things is one rail row with its approval nested under it, drawn as decided: "Approved by you, 10:42 AM" shows once, on the nested approval, and the row itself carries the result.

Collapsed to one line

Closed, the rail shows only the summary line "Used 30 tools, 1 failed, 1 waiting on you, 18s" with its chevron pointing right. The call that failed and the call waiting on you stay under it, with the failure line, its links and the approval, so nothing that needs a person is ever folded away.

A denied call

A call a person refused reads Denied, in neutral text with a prohibition glyph: it is not called Failed and carries no error block. The words are the caller's ("Denied by you 10:40 AM, nothing refunded"), and a denied call never folds away.

A turn that ends on a tool call

A finished turn whose last step is a tool call closes the rail with the run's end state, "Run finished, no reply", on its own line with a check glyph, so the turn does not look hung. A call a person approved has a green dot ("Approved by you 10:33 AM, $18.50 refunded").

A failure inside the hidden run

A failure in the middle of a run stays on the rail between two folded groups: the first 3 calls, "Show 10 more, 10 done: Lookup order ×10", the failed Send email with its danger line and links, "Show 13 more, 13 done: Check inventory ×4, Lookup customer ×4, Search orders ×3, 2 others", then the last 3 calls. Each group counts only the calls it hides.

An interrupted call

When the run ends while a call is under way, nobody knows how the call ended. The row shows a red circled cross and its result in red, with no arrow and never a green or neutral dot. The words follow the tool's risk: "Interrupted, didn't finish" for a tool that only reads, "Interrupted, may have run" for one that changes things. An interrupted call never folds away.

A turn of 30 calls

The first 3 and last 3 calls stay on the rail around one dashed "Show 24 more" button that says what it hides, counted from this turn only. The failed Send email keeps its danger line with Details, Ask the agent to retry and Copy input, and the call waiting on you stays its own row with its approval beneath it. Choosing a row selects it; "Show 24 more" opens the hidden calls in place and "Show fewer" folds them again.

At 375 px

In a narrow container each row has two lines: the name and duration on line one, and a muted line of key input and result, cut with an ellipsis, on line two. The summary reads "Used 30 tools, 1 failed, 1 waiting on you" with no duration, the dashed button says "Show 24 more" with the short "24 done", the failed row is red with a red cross and the waiting row is amber. The approval nests under the waiting row, and the failure line gives way to the panel.

Still running

While the turn runs the summary reads "Used 12 tools so far…" with a spinner and no failed or waiting counts. The rail keeps the first 3 calls, one dashed "Show 8 more" button and the call under way, which shows a spinner in place of the dot, "Running…" in place of a result and the elapsed time. A second rail shows a short turn whose last call is still running.

The selected row

The row whose panel is open is tinted and keeps the shared quiet focus ring, 2px and wrapping the whole row, while its panel is open. Choosing it again closes the panel; Esc or Close in the panel hands focus back to that row through focusCall.

Short turns and where the fold starts

One or two calls are plain rows: no "Used N tools" line, no rail and no fold. From 3 calls the summary line appears and every call still shows. From 8 calls the middle folds, with the first 3 and last 3 around "Show 2 more, 2 done: Lookup order ×2". With 7 calls hiding one row would save nothing, so all 7 show.

04

Props

Props for tool-call-rail
Name
Type
Default
Description
calls required ToolCallRailCall[] The calls of one turn, in the order they were made. One or two calls draw as plain rows with no summary and no rail; from three the summary line and the rail appear; from eight the middle folds, when at least two calls would hide.
running boolean false Whether the turn is still running. The summary reads "Used 12 tools so far…" with a spinner and no failed or waiting counts, and the rail keeps the first calls and the ones under way rather than the last three.
duration text How long the whole turn took, formatted by the caller ("18s"), at the end of the summary line. Left out in a narrow container.
open bindable boolean true Whether the rail is open. Closed, the summary line alone shows, with the calls that failed, were denied or need a person still under it. Bindable.
selectedId text The call whose panel is open: its row is tinted and keeps the quiet focus ring.
onSelect ((id: string) => void)undefined Fired with a call's id when its row is chosen. Rows are buttons only when this is set.
onRetry ((id: string) => void)undefined Fired when the person asks the agent to retry a failed call. The link shows only when this is set.
onCopyInput ((id: string) => void)undefined Fired when the person copies a failed call's input. The link shows only when this is set.
endLabel text The run's end state, on its own line after the last row, in neutral text with a check glyph ("Run finished, no reply"). Set it when the turn ends on a tool call, so it does not look hung.
footnote text A muted line under the rail ("Failures and calls that need you stay visible wherever they fall.").
label text 'Tool calls' An accessible name for the rail.
labels { /** The summary line. Defaults to "Used 30 tools", or "Used 12 tools so far…" while the turn runs. */ used?: ((count: number, running: boolean) => string)undefined; /** The summary line's failed count. Defaults to "1 failed". */ failed?: ((count: number) => string) | undefined; /** The summary line's waiting count. Defaults to "1 waiting on you". */ waiting?: ((count: number) => string)undefined; /** The button over a folded group. Defaults to "Show 24 more". */ showMore?: ((count: number) => string)undefined; /** The summary beside the group button. Defaults to "24 done: Lookup order ×10, 6 others". */ folded?: ((count: number, names: string) => string)undefined; /** The summary beside the group button in a narrow container. Defaults to "24 done". */ foldedShort?: ((count: number) => string)undefined; /** The button that folds every open group again. Defaults to "Show fewer". */ showFewer?: stringundefined; /** The result of a `running` call. Defaults to "Running…". */ running?: stringundefined; /** The result of a `waiting` call. Defaults to "Waiting on you". */ waitingResult?: string
6 more members undefined; /** The result of a `denied` call that has none. Defaults to "Denied". */ denied?: stringundefined; /** The result of an `interrupted` call that has none. Defaults to "Interrupted". */ interrupted?: stringundefined; /** The link on a failed call that opens its details. Defaults to "Details". */ details?: stringundefined; /** The link on a failed call that asks the agent to retry. Defaults to "Ask the agent to retry". */ retry?: stringundefined; /** The link on a failed call that copies its input. Defaults to "Copy input". */ copyInput?: stringundefined; }
The rail's words.
children Snippet<[ToolCallRailCall]>undefined Content under a call's row: the approval a waiting call is waiting on. Drawn for the calls that set nested.