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';
15 import { AgenticSectionType } from '$lib/enums';
16 import { mcpStore } from '$lib/stores/mcp.svelte';
17 import { type AgenticSection, type BuiltinToolUiEntry, getBuiltinToolUi } from '$lib/utils';
18 import type { Component, Snippet } from 'svelte';
20 type ToolCallBlockMetaWithError = TMeta & { errorMessage?: string };
22 interface ToolCallCtx {
25 isStreamingCall: boolean;
26 isCodeStreaming: boolean;
30 section: AgenticSection;
34 * The per-tool meta, including any `errorMessage` field that the
35 * shared chrome uses to compute the status pill subtitle.
37 meta: ToolCallBlockMetaWithError | null | undefined;
39 * True while the tool's process is actively producing output
40 * chunks after its args finished streaming (used by
41 * `exec_shell_command`'s stdout feed).
43 extraLiveStreaming?: boolean;
45 * Swap the title-row icon for a spinning `Loader2` while the
46 * spinner is showing. Only meaningful for tools where "live"
47 * is interesting (e.g. exec_shell_command showing the in-flight
48 * process). Other tools leave it off and render the spinner
49 * inline within the body.
51 spinIconWhenActive?: boolean;
53 * Wrapper component that renders the title row and the body
54 * children. Defaults to CollapsibleContentBlock;
55 * `exec_shell_command` uses CollapsibleTerminalBlock for its
56 * terminal-style frame.
58 wrapper?: typeof CollapsibleContentBlock;
60 titleSnippet?: Snippet;
61 onToggle?: () => void;
62 children: Snippet<[TMeta | null | undefined, ToolCallCtx]>;
67 extraLiveStreaming = false,
73 spinIconWhenActive = false,
76 wrapper: Wrapper = CollapsibleContentBlock
79 const isPending = $derived(section.type === AgenticSectionType.TOOL_CALL_PENDING);
80 const isStreamingCall = $derived(section.type === AgenticSectionType.TOOL_CALL_STREAMING);
81 const showSpinner = $derived(isPending || (isStreamingCall && isStreaming) || extraLiveStreaming);
82 const isCodeStreaming = $derived(isStreaming && (isPending || isStreamingCall));
84 const toolUi: BuiltinToolUiEntry | null = $derived(getBuiltinToolUi(section.toolName));
85 const toolIcon: Component = $derived(
86 spinIconWhenActive && showSpinner ? Loader2 : (toolUi?.icon ?? Wrench)
88 const toolIconClass = $derived(
89 spinIconWhenActive && showSpinner ? ICON_CLASS_SPIN : ICON_CLASS_DEFAULT
91 // Drop the MCP favicon while the spinner is on so the title row
92 // signals "in flight" without being overwritten by server branding.
93 const mcpServerFavicon = $derived(
94 showSpinner ? null : mcpStore.getServerFaviconForTool(section.toolName)
96 const iconUrl = $derived(
97 showSpinner || (toolUi?.icon ?? null) || !mcpServerFavicon ? null : mcpServerFavicon
100 // No subtitle while the call is in flight - the spinner already
101 // signals activity; only terminal states get a pill.
102 function subtitleFor(errorMessage?: string): string | undefined {
103 if (showSpinner) return undefined;
105 if (errorMessage) return 'failed';
107 if (isStreamingCall && !isStreaming) return 'incomplete';
112 const subtitle = $derived(subtitleFor(meta?.errorMessage));
119 iconClass={toolIconClass}
126 {@render children(meta, {