gpui-ai

Agent work

Tool calls

Collapsible tool-call cards with input, output, approval, and a shimmering group.

Tool calls — gpui-ai

Starts when it scrolls into view

Pop out

NORD FROST · DARK · RADIUS 6/10 · BASE 16PX · SHADOW

Running the real component, compiled to WebAssembly. The frame is 654 px tall, which is what this story measures at the demo width in the default type size — a narrower column or a theme that changes that size will wrap it differently.
Story
tool-calls
Demo height
654 px
Events
ToolCallEvent, ToolGroupEvent

Code

Cut from the gallery story this page runs, so it stays true.

crates/gallery/src/gallery.rsOpen in the galleryImplementation
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.