1 <script lang="ts" generics="TMeta">
2 // Generic chrome shell shared by every per-tool block under
3 // `ChatMessageToolCall/`. Owns:
4 // - the collapsible wrapper (defaults to CollapsibleContentBlock;
5 // `exec_shell_command` swaps in CollapsibleTerminalBlock via the
7 // - the icon, spinner state, and MCP favicon fallback chain;
8 // - the status subtitle pill.
9 // Components supply only their `meta`, a title snippet, and a body
10 // snippet - everything around them is this single source of truth.
12 import { Loader2, Wrench } from '@lucide/svelte';
13 import { CollapsibleContentBlock } from '$lib/components/app';
14 import { ICON_CLASS_DEFAULT, ICON_CLASS_SPIN } from '$lib/constants/css-classes';
15 import { AgenticSectionType } from '$lib/enums';
16 import { getBuiltinToolUi } from '$lib/constants/built-in-tools';
17 import { mcpStore } from '$lib/stores/mcp.svelte';
18 import type { Component, Snippet } from 'svelte';
19 import type { AgenticSection, BuiltinToolUiEntry } from '$lib/utils';
21 type ToolCallBlockMetaWithError = TMeta & { errorMessage?: string };
23 interface ToolCallCtx {
26 isStreamingCall: boolean;
27 isCodeStreaming: boolean;
31 section: AgenticSection;
35 * The per-tool meta, including any `errorMessage` field that the
36 * shared chrome uses to compute the status pill subtitle.
38 meta: ToolCallBlockMetaWithError | null | undefined;
40 * True while the tool's process is actively producing output
41 * chunks after its args finished streaming (used by
42 * `exec_shell_command`'s stdout feed).
44 extraLiveStreaming?: boolean;
46 * Swap the title-row icon for a spinning `Loader2` while the
47 * spinner is showing. Only meaningful for tools where "live"
48 * is interesting (e.g. exec_shell_command showing the in-flight
49 * process). Other tools leave it off and render the spinner
50 * inline within the body.
52 spinIconWhenActive?: boolean;
54 * Wrapper component that renders the title row and the body
55 * children. Defaults to CollapsibleContentBlock;
56 * `exec_shell_command` uses CollapsibleTerminalBlock for its
57 * terminal-style frame.
59 wrapper?: typeof CollapsibleContentBlock;
61 titleSnippet?: Snippet;
62 onToggle?: () => void;
63 children: Snippet<[TMeta | null | undefined, ToolCallCtx]>;
71 extraLiveStreaming = false,
72 spinIconWhenActive = false,
73 wrapper: Wrapper = CollapsibleContentBlock,
80 const isPending = $derived(section.type === AgenticSectionType.TOOL_CALL_PENDING);
81 const isStreamingCall = $derived(section.type === AgenticSectionType.TOOL_CALL_STREAMING);
82 const showSpinner = $derived(isPending || (isStreamingCall && isStreaming) || extraLiveStreaming);
83 const isCodeStreaming = $derived(isStreaming && (isPending || isStreamingCall));
85 const toolUi: BuiltinToolUiEntry | null = $derived(getBuiltinToolUi(section.toolName));
86 const toolIcon: Component = $derived(
87 spinIconWhenActive && showSpinner ? Loader2 : (toolUi?.icon ?? Wrench)
89 const toolIconClass = $derived(
90 spinIconWhenActive && showSpinner ? ICON_CLASS_SPIN : ICON_CLASS_DEFAULT
92 // Drop the MCP favicon while the spinner is on so the title row
93 // signals "in flight" without being overwritten by server branding.
94 const mcpServerFavicon = $derived(
95 showSpinner ? null : mcpStore.getServerFaviconForTool(section.toolName)
97 const iconUrl = $derived(
98 showSpinner || (toolUi?.icon ?? null) || !mcpServerFavicon ? null : mcpServerFavicon
101 // No subtitle while the call is in flight - the spinner already
102 // signals activity; only terminal states get a pill.
103 function subtitleFor(errorMessage?: string): string | undefined {
104 if (showSpinner) return undefined;
105 if (errorMessage) return 'failed';
106 if (isStreamingCall && !isStreaming) return 'incomplete';
110 const subtitle = $derived(subtitleFor(meta?.errorMessage));
117 iconClass={toolIconClass}
124 {@render children(meta, {