diff --git a/tools/server/webui/src/lib/components/app/chat/ChatMessages/AgenticContent.svelte b/tools/server/webui/src/lib/components/app/chat/ChatMessages/AgenticContent.svelte index 166aa838e1..94072390b1 100644 --- a/tools/server/webui/src/lib/components/app/chat/ChatMessages/AgenticContent.svelte +++ b/tools/server/webui/src/lib/components/app/chat/ChatMessages/AgenticContent.svelte @@ -8,6 +8,7 @@ */ import { MarkdownContent, SyntaxHighlightedCode } from '$lib/components/app'; + import { config } from '$lib/stores/settings.svelte'; import { Wrench, Loader2 } from '@lucide/svelte'; import ChevronsUpDownIcon from '@lucide/svelte/icons/chevrons-up-down'; import * as Collapsible from '$lib/components/ui/collapsible/index.js'; @@ -19,7 +20,7 @@ } interface AgenticSection { - type: 'text' | 'tool_call' | 'tool_call_pending'; + type: 'text' | 'tool_call' | 'tool_call_pending' | 'tool_call_streaming'; content: string; toolName?: string; toolArgs?: string; @@ -34,12 +35,20 @@ // Track expanded state for each tool call (default expanded) let expandedStates: Record = $state({}); - function isExpanded(index: number): boolean { - return expandedStates[index] ?? true; + // Get showToolCallInProgress setting + const showToolCallInProgress = $derived(config().showToolCallInProgress as boolean); + + function isExpanded(index: number, isPending: boolean): boolean { + // If showToolCallInProgress is enabled and tool is pending, force expand + if (showToolCallInProgress && isPending) { + return true; + } + // Otherwise use stored state, defaulting to expanded only if showToolCallInProgress is true + return expandedStates[index] ?? showToolCallInProgress; } - function toggleExpanded(index: number) { - expandedStates[index] = !isExpanded(index); + function toggleExpanded(index: number, isPending: boolean) { + expandedStates[index] = !isExpanded(index, isPending); } function parseAgenticContent(rawContent: string): AgenticSection[] { @@ -88,10 +97,20 @@ // Check for pending tool call at the end (START without END) const remainingContent = rawContent.slice(lastIndex); + + // Full pending match (has NAME and ARGS) const pendingMatch = remainingContent.match( /<<>>\n<<>>\n<<>>([\s\S]*)$/ ); + // Partial pending match (has START and NAME but ARGS still streaming) + const partialWithNameMatch = remainingContent.match( + /<<>>\n<<>>\n<<]*)$/ + ); + + // Very early match (just START marker, maybe partial NAME) + const earlyMatch = remainingContent.match(/<<>>([\s\S]*)$/); + if (pendingMatch) { // Add text before pending tool call const pendingIndex = remainingContent.indexOf('<<>>'); @@ -121,9 +140,54 @@ toolArgs, toolResult: streamingResult || undefined }); + } else if (partialWithNameMatch) { + // Has START and NAME, ARGS still streaming + const pendingIndex = remainingContent.indexOf('<<>>'); + if (pendingIndex > 0) { + const textBefore = remainingContent.slice(0, pendingIndex).trim(); + if (textBefore) { + sections.push({ type: 'text', content: textBefore }); + } + } + + sections.push({ + type: 'tool_call_streaming', + content: '', + toolName: partialWithNameMatch[1], + toolArgs: undefined, + toolResult: undefined + }); + } else if (earlyMatch) { + // Just START marker, show streaming state + const pendingIndex = remainingContent.indexOf('<<>>'); + if (pendingIndex > 0) { + const textBefore = remainingContent.slice(0, pendingIndex).trim(); + if (textBefore) { + sections.push({ type: 'text', content: textBefore }); + } + } + + // Try to extract tool name if present + const nameMatch = earlyMatch[1]?.match(/<<]+)>>>/); + + sections.push({ + type: 'tool_call_streaming', + content: '', + toolName: nameMatch?.[1], + toolArgs: undefined, + toolResult: undefined + }); } else if (lastIndex < rawContent.length) { // Add remaining text after last completed tool call - const remainingText = rawContent.slice(lastIndex).trim(); + // But strip any partial markers that might be starting + let remainingText = rawContent.slice(lastIndex).trim(); + + // Check for partial marker at the end (e.g., "<<<" or "<< + {:else if section.type === 'tool_call_streaming'} + +
+ +
+ + {#if section.toolName} + {section.toolName} + {/if} + preparing... +
+
+
{:else if section.type === 'tool_call' || section.type === 'tool_call_pending'} {@const isPending = section.type === 'tool_call_pending'} - + toggleExpanded(index)} + onclick={() => toggleExpanded(index, isPending)} >
{#if isPending} diff --git a/tools/server/webui/src/lib/components/app/chat/ChatSettings/ChatSettings.svelte b/tools/server/webui/src/lib/components/app/chat/ChatSettings/ChatSettings.svelte index f513b9e86d..decca7c52f 100644 --- a/tools/server/webui/src/lib/components/app/chat/ChatSettings/ChatSettings.svelte +++ b/tools/server/webui/src/lib/components/app/chat/ChatSettings/ChatSettings.svelte @@ -263,6 +263,11 @@ key: 'agenticFilterReasoningAfterFirstTurn', label: 'Filter reasoning after first turn', type: 'checkbox' + }, + { + key: 'showToolCallInProgress', + label: 'Show tool call in progress', + type: 'checkbox' } ] },