diff --git a/tools/server/webui/src/lib/components/app/chat/ChatMessages/ChatMessageAgenticContent.svelte b/tools/server/webui/src/lib/components/app/chat/ChatMessages/ChatMessageAgenticContent.svelte
index bd78ae424a..2725a28f94 100644
--- a/tools/server/webui/src/lib/components/app/chat/ChatMessages/ChatMessageAgenticContent.svelte
+++ b/tools/server/webui/src/lib/components/app/chat/ChatMessages/ChatMessageAgenticContent.svelte
@@ -1,5 +1,6 @@
-
- {#each sectionsParsed as section, index (index)}
- {#if section.type === AgenticSectionType.TEXT}
-
-
-
- {:else if section.type === AgenticSectionType.TOOL_CALL_STREAMING}
- {@const streamingIcon = isStreaming ? Loader2 : AlertTriangle}
- {@const streamingIconClass = isStreaming ? 'h-4 w-4 animate-spin' : 'h-4 w-4 text-yellow-500'}
- {@const streamingSubtitle = isStreaming ? '' : 'incomplete'}
+{#snippet renderSection(section: typeof sectionsParsed[number], index: number)}
+ {#if section.type === AgenticSectionType.TEXT}
+
+
+
+ {:else if section.type === AgenticSectionType.TOOL_CALL_STREAMING}
+ {@const streamingIcon = isStreaming ? Loader2 : AlertTriangle}
+ {@const streamingIconClass = isStreaming ? 'h-4 w-4 animate-spin' : 'h-4 w-4 text-yellow-500'}
+ {@const streamingSubtitle = isStreaming ? '' : 'incomplete'}
-
toggleExpanded(index, section)}
- >
-
-
- Arguments:
-
- {#if isStreaming}
-
- {/if}
-
- {#if section.toolArgs}
-
- {:else if isStreaming}
-
- Receiving arguments...
-
- {:else}
-
- Response was truncated
-
- {/if}
-
-
- {:else if section.type === AgenticSectionType.TOOL_CALL || section.type === AgenticSectionType.TOOL_CALL_PENDING}
- {@const isPending = section.type === AgenticSectionType.TOOL_CALL_PENDING}
- {@const toolIcon = isPending ? Loader2 : Wrench}
- {@const toolIconClass = isPending ? 'h-4 w-4 animate-spin' : 'h-4 w-4'}
-
-
toggleExpanded(index, section)}
- >
- {#if section.toolArgs && section.toolArgs !== '{}'}
-
- {/if}
-
-
-
- Result:
-
- {#if isPending}
-
- {/if}
-
- {#if section.toolResult}
-
- {#each section.parsedLines as line, i (i)}
-
{line.text}
- {#if line.image}
-

- {/if}
- {/each}
-
- {:else if isPending}
-
- Waiting for result...
-
- {/if}
-
-
- {:else if section.type === AgenticSectionType.REASONING}
-
toggleExpanded(index, section)}
- >
-
-
- {section.content}
-
-
-
- {:else if section.type === AgenticSectionType.REASONING_PENDING}
- {@const reasoningTitle = isStreaming ? 'Reasoning...' : 'Reasoning'}
- {@const reasoningSubtitle = isStreaming ? '' : 'incomplete'}
-
-
toggleExpanded(index, section)}
- >
-
-
- {section.content}
-
-
-
- {/if}
- {/each}
-
- {#if streamingToolCall}
{}}
+ icon={streamingIcon}
+ iconClass={streamingIconClass}
+ title={section.toolName || 'Tool call'}
+ subtitle={streamingSubtitle}
+ {isStreaming}
+ onToggle={() => toggleExpanded(index, section)}
>
Arguments:
-
+
+ {#if isStreaming}
+
+ {/if}
- {#if streamingToolCall.arguments}
+ {#if section.toolArgs}
- {:else}
+ {:else if isStreaming}
Receiving arguments...
+ {:else}
+
+ Response was truncated
+
{/if}
+ {:else if section.type === AgenticSectionType.TOOL_CALL || section.type === AgenticSectionType.TOOL_CALL_PENDING}
+ {@const isPending = section.type === AgenticSectionType.TOOL_CALL_PENDING}
+ {@const toolIcon = isPending ? Loader2 : Wrench}
+ {@const toolIconClass = isPending ? 'h-4 w-4 animate-spin' : 'h-4 w-4'}
+
+
toggleExpanded(index, section)}
+ >
+ {#if section.toolArgs && section.toolArgs !== '{}'}
+
+ {/if}
+
+
+
+ Result:
+
+ {#if isPending}
+
+ {/if}
+
+ {#if section.toolResult}
+
+ {#each section.parsedLines as line, i (i)}
+
{line.text}
+ {#if line.image}
+

+ {/if}
+ {/each}
+
+ {:else if isPending}
+
+ Waiting for result...
+
+ {/if}
+
+
+ {:else if section.type === AgenticSectionType.REASONING}
+
toggleExpanded(index, section)}
+ >
+
+
+ {section.content}
+
+
+
+ {:else if section.type === AgenticSectionType.REASONING_PENDING}
+ {@const reasoningTitle = isStreaming ? 'Reasoning...' : 'Reasoning'}
+ {@const reasoningSubtitle = isStreaming ? '' : 'incomplete'}
+
+
toggleExpanded(index, section)}
+ >
+
+
+ {section.content}
+
+
+
+ {/if}
+{/snippet}
+
+
+ {#if highlightTurns && turnGroups.length > 1}
+ {#each turnGroups as turn, turnIndex (turnIndex)}
+ {@const turnStats = message?.timings?.agentic?.perTurn?.[turnIndex]}
+
+
Turn {turnIndex + 1}
+ {#each turn.sections as section, sIdx (turn.flatIndices[sIdx])}
+ {@render renderSection(section, turn.flatIndices[sIdx])}
+ {/each}
+ {#if turnStats}
+
+ 0
+ ? buildTurnAgenticTimings(turnStats)
+ : undefined}
+ initialView={ChatMessageStatsView.GENERATION}
+ hideSummary
+ />
+
+ {/if}
+
+ {/each}
+ {:else}
+ {#each sectionsParsed as section, index (index)}
+ {@render renderSection(section, index)}
+ {/each}
{/if}
@@ -280,4 +324,31 @@
.agentic-text {
width: 100%;
}
+
+ .agentic-turn {
+ position: relative;
+ border: 1.5px dashed var(--muted-foreground);
+ border-radius: 0.75rem;
+ padding: 1rem;
+ transition: background 0.1s;
+ }
+
+ .agentic-turn-label {
+ position: absolute;
+ top: -1rem;
+ left: 0.75rem;
+ padding: 0 0.375rem;
+ background: var(--background);
+ font-size: 0.7rem;
+ font-weight: 500;
+ color: var(--muted-foreground);
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+ }
+
+ .turn-stats {
+ margin-top: 0.75rem;
+ padding-top: 0.5rem;
+ border-top: 1px solid hsl(var(--muted) / 0.5);
+ }
diff --git a/tools/server/webui/src/lib/components/app/chat/ChatMessages/ChatMessageAssistant.svelte b/tools/server/webui/src/lib/components/app/chat/ChatMessages/ChatMessageAssistant.svelte
index a9889946c2..369c1c1048 100644
--- a/tools/server/webui/src/lib/components/app/chat/ChatMessages/ChatMessageAssistant.svelte
+++ b/tools/server/webui/src/lib/components/app/chat/ChatMessages/ChatMessageAssistant.svelte
@@ -12,12 +12,13 @@
import { isLoading, isChatStreaming } from '$lib/stores/chat.svelte';
import { agenticStreamingToolCall } from '$lib/stores/agentic.svelte';
import { autoResizeTextarea, copyToClipboard, isIMEComposing } from '$lib/utils';
+ import { tick } from 'svelte';
import { fade } from 'svelte/transition';
import { Check, X } from '@lucide/svelte';
import { Button } from '$lib/components/ui/button';
import { Checkbox } from '$lib/components/ui/checkbox';
import { INPUT_CLASSES } from '$lib/constants/css-classes';
- import { MessageRole, KeyboardKey } from '$lib/enums';
+ import { MessageRole, KeyboardKey, ChatMessageStatsView } from '$lib/enums';
import Label from '$lib/components/ui/label/label.svelte';
import { config } from '$lib/stores/settings.svelte';
import { isRouterMode } from '$lib/stores/server.svelte';
@@ -99,6 +100,62 @@
let currentConfig = $derived(config());
let isRouter = $derived(isRouterMode());
let showRawOutput = $state(false);
+ let activeStatsView = $state
(ChatMessageStatsView.GENERATION);
+ let statsContainerEl: HTMLDivElement | undefined = $state();
+
+ function getScrollParent(el: HTMLElement): HTMLElement | null {
+ let parent = el.parentElement;
+ while (parent) {
+ const style = getComputedStyle(parent);
+ if (/(auto|scroll)/.test(style.overflowY)) {
+ return parent;
+ }
+ parent = parent.parentElement;
+ }
+ return null;
+ }
+
+ async function handleStatsViewChange(view: ChatMessageStatsView) {
+ const el = statsContainerEl;
+ if (!el) {
+ activeStatsView = view;
+
+ return;
+ }
+
+ const scrollParent = getScrollParent(el);
+ if (!scrollParent) {
+ activeStatsView = view;
+
+ return;
+ }
+
+ const yBefore = el.getBoundingClientRect().top;
+
+ activeStatsView = view;
+
+ await tick();
+
+ const delta = el.getBoundingClientRect().top - yBefore;
+ if (delta !== 0) {
+ scrollParent.scrollTop += delta;
+ }
+
+ // Correct any drift after browser paint
+ requestAnimationFrame(() => {
+ const drift = el.getBoundingClientRect().top - yBefore;
+
+ if (Math.abs(drift) > 1) {
+ scrollParent.scrollTop += drift;
+ }
+ });
+ }
+
+ let highlightAgenticTurns = $derived(
+ hasAgenticMarkers &&
+ (currentConfig.alwaysShowAgenticTurns ||
+ activeStatsView === ChatMessageStatsView.SUMMARY)
+ );
let displayedModel = $derived(message.model ?? null);
@@ -205,6 +262,7 @@
{:else}
@@ -230,7 +288,7 @@
{#if displayedModel}
-
+
{#if isRouter}
{:else if isLoading() && currentConfig.showMessageStats}
{@const liveStats = processingState.getLiveProcessingStats()}
diff --git a/tools/server/webui/src/lib/components/app/chat/ChatMessages/ChatMessageStatistics.svelte b/tools/server/webui/src/lib/components/app/chat/ChatMessages/ChatMessageStatistics.svelte
index b47787fcdd..9a2a42289d 100644
--- a/tools/server/webui/src/lib/components/app/chat/ChatMessages/ChatMessageStatistics.svelte
+++ b/tools/server/webui/src/lib/components/app/chat/ChatMessages/ChatMessageStatistics.svelte
@@ -15,6 +15,8 @@
isProcessingPrompt?: boolean;
initialView?: ChatMessageStatsView;
agenticTimings?: ChatMessageAgenticTimings;
+ onActiveViewChange?: (view: ChatMessageStatsView) => void;
+ hideSummary?: boolean;
}
let {
@@ -25,12 +27,18 @@
isLive = false,
isProcessingPrompt = false,
initialView = ChatMessageStatsView.GENERATION,
- agenticTimings
+ agenticTimings,
+ onActiveViewChange,
+ hideSummary = false
}: Props = $props();
let activeView: ChatMessageStatsView = $state(initialView);
let hasAutoSwitchedToGeneration = $state(false);
+ $effect(() => {
+ onActiveViewChange?.(activeView);
+ });
+
// In live mode: auto-switch to GENERATION tab when prompt processing completes
$effect(() => {
if (isLive) {
@@ -177,26 +185,28 @@
-
-
-
+
-
- Agentic summary
-
-
+
+ Agentic summary
+
+
+ {/if}
{/if}
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 538070f4e0..6c8ef1516e 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
@@ -255,6 +255,11 @@
label: 'Agentic loop max turns',
type: 'input'
},
+ {
+ key: 'alwaysShowAgenticTurns',
+ label: 'Always show agentic turns in conversation',
+ type: 'checkbox'
+ },
{
key: 'agenticMaxToolPreviewLines',
label: 'Max lines per tool preview',
diff --git a/tools/server/webui/src/lib/constants/settings-config.ts b/tools/server/webui/src/lib/constants/settings-config.ts
index 569d9d1b49..a7e412d3cd 100644
--- a/tools/server/webui/src/lib/constants/settings-config.ts
+++ b/tools/server/webui/src/lib/constants/settings-config.ts
@@ -27,6 +27,7 @@ export const SETTING_CONFIG_DEFAULT: Record
=
agenticMaxTurns: 10,
agenticMaxToolPreviewLines: 25,
showToolCallInProgress: false,
+ alwaysShowAgenticTurns: false,
// make sure these default values are in sync with `common.h`
samplers: 'top_k;typ_p;top_p;min_p;temperature',
backend_sampling: false,
diff --git a/tools/server/webui/src/lib/stores/agentic.svelte.ts b/tools/server/webui/src/lib/stores/agentic.svelte.ts
index 3da57f7fd9..66376b2ba4 100644
--- a/tools/server/webui/src/lib/stores/agentic.svelte.ts
+++ b/tools/server/webui/src/lib/stores/agentic.svelte.ts
@@ -437,6 +437,8 @@ class AgenticStore {
}
if (turnToolCalls.length === 0) {
+ agenticTimings.perTurn!.push(turnStats);
+
onComplete?.(
'',
undefined,