Tool

Summarize a run of tool calls in one line, and let users open it to see each search, read, and command the assistant ran.

  1. Searched usePreferences
  2. Read src/lib/preferences.ts
  3. Read src/routes/settings/+page.svelte
  4. Output
    ✓ saves the theme preference
    ✓ restores preferences after sign-in
    2 passed in 3.08s
    Input
    {
      "command": "bun test preferences",
      "cwd": "apps/web"
    }
<script lang="ts">
    import FileText from '@lucide/svelte/icons/file-text';
    import Search from '@lucide/svelte/icons/search';
    import Terminal from '@lucide/svelte/icons/terminal';
    import * as Tool from '@sivir-ui/svelte/components/tool';

    const input = JSON.stringify(
        {
            command: 'bun test preferences',
            cwd: 'apps/web'
        },
        null,
        2
    );
    const output = [
        '✓ saves the theme preference',
        '✓ restores preferences after sign-in',
        '2 passed in 3.08s'
    ].join('\n');
</script>

<Tool.Root duration={4} open class="w-full max-w-xl">
    <Tool.Trigger status="Running tests" summary="Searched once, read 2 files, ran tests" />
    <Tool.Content>
        <Tool.Call action="Searched" target="usePreferences">
            {#snippet icon()}<Search />{/snippet}
        </Tool.Call>
        <Tool.Call action="Read" target="src/lib/preferences.ts">
            {#snippet icon()}<FileText />{/snippet}
        </Tool.Call>
        <Tool.Call action="Read" target="src/routes/settings/+page.svelte">
            {#snippet icon()}<FileText />{/snippet}
        </Tool.Call>
        <Tool.Call action="Ran" target="bun test preferences" open>
            {#snippet icon()}<Terminal />{/snippet}
            <Tool.Output>
                <pre class="font-mono text-xs leading-5">{output}</pre>
            </Tool.Output>
            <Tool.Input>{input}</Tool.Input>
        </Tool.Call>
    </Tool.Content>
</Tool.Root>

Installation

Copy the command below and run it in your terminal.

bunx @sivir-ui/svelte add tool

Usage

Tool is styled to match Reasoning, so a run of tool calls sits beside a reasoning trace without a second visual language. Set running on Root while the calls run. Turning it on opens the calls and starts a timer. Trigger shows a shimmering status, such as “Running tests”, then settles into its summary. Without a summary it reads “Worked for” and the elapsed time. Pass duration in seconds to restore a saved transcript. Content mounts on its first open and stays mounted, so calls can keep arriving while it is closed.

import * as Tool from '@sivir-ui/svelte/components/tool';

<Tool.Root running={busy}>
  <Tool.Trigger status="Running tests" summary="Edited 2 files, ran tests" />
  <Tool.Content>
    <Tool.Call action="Read" target="src/lib/rate-limit.ts" />
    <Tool.Call action="Running" target="bun test" state="running" />
  </Tool.Content>
</Tool.Root>

Each Call sits on the rail with an action and a monospace target. A call with state="running" shimmers its action, and state="error" marks it failed. Calls blur in as they are added. Give a call an icon snippet to replace the dot on the rail. Give it children, usually Output and Input, and the row becomes a button that expands them.

import Terminal from '@lucide/svelte/icons/terminal';
import * as Tool from '@sivir-ui/svelte/components/tool';

<Tool.Call action="Ran" target="bun test" state="error">
  {#snippet icon()}<Terminal />{/snippet}
  <Tool.Output label="Error">
    <pre>2 tests failed</pre>
  </Tool.Output>
  <Tool.Input>{JSON.stringify({ command: 'bun test' }, null, 2)}</Tool.Input>
</Tool.Call>

Trigger shows no icon by default. Pass an icon snippet to add one, or a children snippet to render your own label. Both receive open, running, and seconds.

import Wrench from '@lucide/svelte/icons/wrench';
import * as Tool from '@sivir-ui/svelte/components/tool';

<Tool.Trigger status="Running tests">
  {#snippet icon({ running })}
    <Wrench class={running ? 'animate-pulse' : ''} />
  {/snippet}
</Tool.Trigger>

Examples

Coding agent

  1. Searched usePreferences
  2. Read src/lib/preferences.ts
  3. Read src/routes/settings/+page.svelte
  4. Output
    ✓ saves the theme preference
    ✓ restores preferences after sign-in
    2 passed in 3.08s
    Input
    {
      "command": "bun test preferences",
      "cwd": "apps/web"
    }
<script lang="ts">
    import FileText from '@lucide/svelte/icons/file-text';
    import Search from '@lucide/svelte/icons/search';
    import Terminal from '@lucide/svelte/icons/terminal';
    import * as Tool from '@sivir-ui/svelte/components/tool';

    const input = JSON.stringify(
        {
            command: 'bun test preferences',
            cwd: 'apps/web'
        },
        null,
        2
    );
    const output = [
        '✓ saves the theme preference',
        '✓ restores preferences after sign-in',
        '2 passed in 3.08s'
    ].join('\n');
</script>

<Tool.Root duration={4} open class="w-full max-w-xl">
    <Tool.Trigger status="Running tests" summary="Searched once, read 2 files, ran tests" />
    <Tool.Content>
        <Tool.Call action="Searched" target="usePreferences">
            {#snippet icon()}<Search />{/snippet}
        </Tool.Call>
        <Tool.Call action="Read" target="src/lib/preferences.ts">
            {#snippet icon()}<FileText />{/snippet}
        </Tool.Call>
        <Tool.Call action="Read" target="src/routes/settings/+page.svelte">
            {#snippet icon()}<FileText />{/snippet}
        </Tool.Call>
        <Tool.Call action="Ran" target="bun test preferences" open>
            {#snippet icon()}<Terminal />{/snippet}
            <Tool.Output>
                <pre class="font-mono text-xs leading-5">{output}</pre>
            </Tool.Output>
            <Tool.Input>{input}</Tool.Input>
        </Tool.Call>
    </Tool.Content>
</Tool.Root>

Live calls

<script lang="ts">
    import FileText from '@lucide/svelte/icons/file-text';
    import RotateCcw from '@lucide/svelte/icons/rotate-ccw';
    import Search from '@lucide/svelte/icons/search';
    import Terminal from '@lucide/svelte/icons/terminal';
    import { Button } from '@sivir-ui/svelte/components/button';
    import { Spinner } from '@sivir-ui/svelte/components/spinner';
    import * as Tool from '@sivir-ui/svelte/components/tool';
    import { onDestroy, onMount } from 'svelte';

    type Step = {
        id: string;
        status: string;
        liveAction: string;
        doneAction: string;
        target: string;
        kind: 'search' | 'read' | 'run';
        ms: number;
    };

    const steps: Step[] = [
        {
            id: 'search',
            status: 'Searching the codebase',
            liveAction: 'Searching',
            doneAction: 'Searched',
            target: 'refund_status',
            kind: 'search',
            ms: 1200
        },
        {
            id: 'read',
            status: 'Reading 1 file',
            liveAction: 'Reading',
            doneAction: 'Read',
            target: 'src/billing/refunds.ts',
            kind: 'read',
            ms: 900
        },
        {
            id: 'test',
            status: 'Running tests',
            liveAction: 'Running',
            doneAction: 'Ran',
            target: 'bun test refunds',
            kind: 'run',
            ms: 2100
        }
    ];

    let started = $state(0);
    let finished = $state(0);
    let timer: ReturnType<typeof setTimeout> | undefined;

    const running = $derived(finished < steps.length);
    const status = $derived(steps[Math.min(started, steps.length) - 1]?.status ?? 'Working');

    function next() {
        if (started > finished) {
            finished += 1;
        }
        if (started === steps.length) {
            return;
        }

        started += 1;
        timer = setTimeout(next, steps[started - 1].ms);
    }

    function replay() {
        clearTimeout(timer);
        started = 0;
        finished = 0;
        timer = setTimeout(next, 300);
    }

    onMount(() => {
        timer = setTimeout(next, 300);
    });

    onDestroy(() => {
        clearTimeout(timer);
    });
</script>

<div class="flex w-full max-w-xl flex-col items-start gap-4">
    <Tool.Root {running} class="w-full">
        <Tool.Trigger {status} summary="Searched once, read 1 file, ran tests" />
        <Tool.Content>
            {#each steps.slice(0, started) as step, index (step.id)}
                {@const live = index >= finished}
                <Tool.Call
                    action={live ? step.liveAction : step.doneAction}
                    target={step.target}
                    state={live ? 'running' : 'complete'}
                >
                    {#snippet icon()}
                        {#if live}
                            <Spinner size={14} aria-hidden="true" />
                        {:else if step.kind === 'search'}
                            <Search />
                        {:else if step.kind === 'read'}
                            <FileText />
                        {:else}
                            <Terminal />
                        {/if}
                    {/snippet}
                </Tool.Call>
            {/each}
        </Tool.Content>
    </Tool.Root>
    <Button variant="outline" size="sm" disabled={running} onclick={replay}>
        <RotateCcw aria-hidden="true" />
        Replay
    </Button>
</div>

Root times the group while running is on. Give a running call a present-tense action, such as “Reading”, and change it to “Read” when the call finishes; the label crossfades. Set the trigger status to the current step. This example swaps each call's icon for a spinner while it runs.

Failed call

  1. Ran bun run lint
  2. Error
    src/lib/refunds.ts:42:18
    Type 'string | undefined' is not assignable to type 'string'.
<script lang="ts">
    import Terminal from '@lucide/svelte/icons/terminal';
    import * as Tool from '@sivir-ui/svelte/components/tool';

    const error = [
        'src/lib/refunds.ts:42:18',
        "Type 'string | undefined' is not assignable to type 'string'."
    ].join('\n');
</script>

<Tool.Root duration={3} open class="w-full max-w-xl">
    <Tool.Trigger summary="Ran 2 commands, 1 failed" />
    <Tool.Content>
        <Tool.Call action="Ran" target="bun run lint">
            {#snippet icon()}
                <Terminal />
            {/snippet}
        </Tool.Call>
        <Tool.Call action="Ran" target="bun run build" state="error" open>
            {#snippet icon()}
                <Terminal />
            {/snippet}
            <Tool.Output label="Error">
                <pre class="font-mono text-xs leading-5 whitespace-pre-wrap">{error}</pre>
            </Tool.Output>
        </Tool.Call>
    </Tool.Content>
</Tool.Root>

Anatomy

Tool.Root - Times the group while running and controls visibility.

Tool.Trigger - Shows an optional icon, the live status, and timer, then the summary.

Tool.Content - Lists the calls on a rail.

Tool.Call - Shows one call with its icon, action, and target.

Tool.Input - Shows the arguments a call was made with.

Tool.Output - Shows what a call returned.