# ToolCall

> One tool invocation in a disclosure card: status dot, tool name, duration, and the request and result payloads pretty-printed on expand.

- Package: `fancy-ui-svelte` (Svelte 5 + Tailwind CSS v4)
- Category: AI Agents
- Tags: ai, agents, tool, disclosure
- Docs: https://fancy-ui.rama.app/docs/components/tool-call
- Source: https://github.com/RamaHerbin/fancy-ui/tree/main/src/lib/fancy-ui/tool-call

## Import

```svelte
<script lang="ts">
  import { ToolCall } from 'fancy-ui-svelte';
</script>
```

Setup (once per project): `npm install fancy-ui-svelte`, then add `@import "fancy-ui-svelte/tailwind.css";` after `@import "tailwindcss";` in your main CSS file. Full guide: https://fancy-ui.rama.app/llms.txt

React: `import { ToolCall } from 'fancy-ui-react';` (same prop names; `class` becomes `className`, snippets become `ReactNode`).

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `call` (required) | `ToolCallData` | — | The invocation to render: id, name, status, and whatever payloads exist so far |
| `open` | `boolean` | — | Expanded state (bindable). Left undefined the card stays collapsed, except that a call with status "error" opens itself — until the reader toggles it by hand |
| `onToggle` | `(open: boolean) => void` | — | Called on every open/close, by click or on the card's own initiative |
| `sound` | `boolean` | `false` | Plays open or close when the reader toggles the payloads, once the user has enabled sound |

## Snippets

| Snippet | Description |
| --- | --- |
| `input` | Replaces the default request rendering; receives call.input |
| `output` | Replaces the default result rendering; receives call.output |
| `icon` | Leading icon, replacing the default wrench |

## Examples

### Basic Usage

```svelte
<script lang="ts">
	import { onMount } from "svelte";
	import { ToolCall } from "fancy-ui-svelte";
	import type { ToolCallData } from "fancy-ui-svelte";

	/** How often the running call's stopwatch refreshes. */
	const TICK_MS = 250;
	/** How long a pretend call runs before another one takes its place. */
	const CYCLE_MS = 9000;
	/** What reduced motion sees instead of a clock that never settles. */
	const STILL_MS = 4200;

	let elapsedMs = $state(0);

	const search: ToolCallData = {
		id: "call_1",
		name: "search_docs",
		status: "done",
		input: { query: "retry policy", limit: 3 },
		output: { hits: 3, top: "billing/retries.md", tookMs: 812 },
		durationMs: 1400,
	};

	const fetching: ToolCallData = $derived({
		id: "call_2",
		name: "fetch_changelog",
		status: "running",
		input: { repo: "acme/billing", since: "2026-07-01" },
		durationMs: elapsedMs,
	});

	const posting: ToolCallData = {
		id: "call_3",
		name: "post_summary",
		status: "error",
		input: { channel: "#releases", blocks: 4 },
		error: "429 Too Many Requests — that channel is rate limited for another 38s.",
		durationMs: 240,
	};

	onMount(() => {
		// A clock that never stops is exactly what reduced motion is asking us not
		// to run, so it gets one plausible reading and no timer at all.
		if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
			elapsedMs = STILL_MS;
			return;
		}

		const started = Date.now();
		// Wrapping at the cycle keeps the demo honest: a tool call left open on a
		// docs page for an hour should not claim to have been running for one.
		const timer = setInterval(() => {
			elapsedMs = (Date.now() - started) % CYCLE_MS;
		}, TICK_MS);
		return () => clearInterval(timer);
	});
</script>

<div class="flex w-full max-w-xl flex-col gap-2 p-6">
	<ToolCall sound call={search} />
	<ToolCall sound call={fetching} />
	<ToolCall sound call={posting} />

	<p class="text-muted-foreground mt-2 text-xs">
		Click any header to see what went out and what came back. The failed call opened itself — but
		close it and it stays closed.
	</p>
</div>
```
