Agent work
Tool calls
Collapsible tool-call cards with input, output, approval, and a shimmering group.
Starts when it scrolls into view
NORD FROST · DARK · RADIUS 6/10 · BASE 16PX · SHADOW
- Type
ToolCall- Source
tool_call.rs- Story
tool-calls- Demo height
- 654 px
- Events
- ToolCallEvent, ToolGroupEvent
Code
Cut from the gallery story this page runs, so it stays true.
let running = self.sim.answer.is_streaming();
let read = Progressive::complete(
ToolInvocation::new("read-pricing", "read_file")
.summary("pricing.md")
.input("{\n \"path\": \"pricing.md\",\n \"range\": [1, 214]\n}")
.output(
"Read **214 lines**.\n\n| Supplier | Unit cost | Tier |\n|---|---|---|\n| Alpenrose Dairy | $3.12 | 500+ |\n| Tillamook | $3.36 | 500+ |",
)
.elapsed(Duration::from_millis(340)),
);
let search_call = ToolInvocation::new("search-suppliers", "web_search")
.summary("alpenrose wholesale pricing")
.icon(IconName::Globe)
.input("{\n \"query\": \"alpenrose wholesale pricing\",\n \"limit\": 3\n}");
let search = if running {
Progressive::running(search_call)
} else {
Progressive::complete(
search_call
.output(
"3 results — alpenrose.com, dairyreport.org, nwfoodtrade.com",
)
.elapsed(Duration::from_millis(1800)),
)
};
let email = Progressive::pending(
ToolInvocation::new("send-confirmations", "send_email")
.summary("3 suppliers")
.input(
"{\n \"to\": [\"orders@alpenrose.com\", \"sales@tillamook.com\"],\n \"subject\": \"Order confirmation — week 35\"\n}",
)
.approval(self.tool_approval),
);
let failed = Progressive::failed(
ToolInvocation::new("query-prices", "query_db")
.summary("prices-db")
.input("SELECT unit_cost FROM prices WHERE week = 35")
.input_language("sql")
.elapsed(Duration::from_millis(2100)),
"Connection timed out after 2s",
);
let configure = |mut call: ToolCall, id: &str| {
if let Some(open) = self.tool_call_open.get(id) {
call = call.open(*open);
}
call
};
let mut group = ToolGroup::new("tool-group").count(2).active(running);
if let Some(open) = self.tool_group_open {
group = group.open(open);
}
v_flex()
.gap_4()
.child(
group
.on_event(cx.listener(|this, event: &ToolGroupEvent, _, cx| {
let ToolGroupEvent::Toggled { open, .. } = event;
this.tool_group_open = Some(*open);
cx.notify();
}))
.child(
configure(ToolCall::new(&read), "read-pricing").on_event(
cx.listener(Self::handle_tool_call_event),
),
)
.child(
configure(ToolCall::new(&search), "search-suppliers")
.on_event(cx.listener(Self::handle_tool_call_event)),
),
)
.child(
configure(ToolCall::new(&email), "send-confirmations")
.on_event(cx.listener(Self::handle_tool_call_event)),
)
.child(
configure(ToolCall::new(&failed), "query-prices")
.on_event(cx.listener(Self::handle_tool_call_event)),
)
Events and ownership
- Who owns the state
- ToolCall renders state supplied by the application; it does not own durable work.
- What it emits
- Interactive intent is reported through the typed ToolCallEvent and ToolGroupEvent contracts and stable application IDs.
- What it means
- Collapsible tool-call cards with input, output, approval, and a shimmering group.
- Overflow and motion
- Growing content remains reachable in a bounded vertical surface; reduced motion preserves a useful state.
Event types
- ToolCallEvent
- ToolGroupEvent
What the demo does and does not show
The demo above is the same Rust as the native component, compiled to WebAssembly and drawn on a WebGPU canvas — not a mock-up, a recording, or a re-implementation. What it paints is what the component paints: layout, colour from the live theme, and motion.
The live browser specimen requires WebGPU; the native component remains the authoritative runtime.
A canvas cannot stand in for the platform, so treat everything that goes through the operating system as unproven here: keyboard handling and shortcuts, screen reader semantics, text input and IME, the clipboard, file drops, and the frame rate you would get on your own GPU. Judge those in a native build. If your browser has no WebGPU, the frame says so rather than showing an empty box.