From cead02ee58b467bb9fc4c01be224d8bf69bde07e Mon Sep 17 00:00:00 2001 From: Aleksander Grygier Date: Mon, 12 Jan 2026 11:06:54 +0100 Subject: [PATCH] fix: Restore live reactive UI progress for tool calls --- .../webui/src/lib/clients/agentic.client.ts | 57 ++++++++++++------- 1 file changed, 36 insertions(+), 21 deletions(-) diff --git a/tools/server/webui/src/lib/clients/agentic.client.ts b/tools/server/webui/src/lib/clients/agentic.client.ts index cff14b03cd..f4223ec19d 100644 --- a/tools/server/webui/src/lib/clients/agentic.client.ts +++ b/tools/server/webui/src/lib/clients/agentic.client.ts @@ -268,6 +268,9 @@ export class AgenticClient { let lastStreamingToolCallName = ''; let lastStreamingToolCallArgsLength = 0; + // Track emitted tool call state for progressive streaming + const emittedToolCallStates = new Map(); + let turnTimings: ChatMessageTimings | undefined; const turnStats: ChatMessageAgenticTurnStats = { @@ -297,6 +300,37 @@ export class AgenticClient { onToolCallChunk: (serialized: string) => { try { turnToolCalls = JSON.parse(serialized) as ApiChatCompletionToolCall[]; + + // Emit agentic tags progressively for live UI updates + for (let i = 0; i < turnToolCalls.length; i++) { + const toolCall = turnToolCalls[i]; + const toolName = toolCall.function?.name ?? ''; + const toolArgs = toolCall.function?.arguments ?? ''; + + const state = emittedToolCallStates.get(i) || { + emittedOnce: false, + lastArgs: '' + }; + + if (!state.emittedOnce) { + // First emission: send full header + args + let output = `\n\n<<>>`; + output += `\n<<>>`; + output += `\n<<>>\n`; + output += toolArgs; + onChunk?.(output); + state.emittedOnce = true; + state.lastArgs = toolArgs; + } else if (toolArgs !== state.lastArgs) { + // Subsequent emissions: send only delta + const delta = toolArgs.slice(state.lastArgs.length); + onChunk?.(delta); + state.lastArgs = toolArgs; + } + + emittedToolCallStates.set(i, state); + } + // Update store with streaming tool call state for UI visualization // Only update when values actually change to avoid memory pressure if (turnToolCalls.length > 0 && turnToolCalls[0]?.function) { @@ -418,9 +452,9 @@ export class AgenticClient { return; } - // Start timing BEFORE emitToolCallStart to capture full perceived execution time + // Tool call tags were already emitted during streaming via onToolCallChunk + // Start timing for tool execution const toolStartTime = performance.now(); - this.emitToolCallStart(toolCall, onChunk); const mcpCall: MCPToolCall = { id: toolCall.id, @@ -551,25 +585,6 @@ export class AgenticClient { })); } - /** - * Emit tool call start marker (shows "pending" state in UI). - */ - private emitToolCallStart( - toolCall: AgenticToolCallList[number], - emit?: (chunk: string) => void - ): void { - if (!emit) return; - - const toolName = toolCall.function.name; - const toolArgs = toolCall.function.arguments; - - let output = `\n\n<<>>`; - output += `\n<<>>`; - output += `\n<<>>\n`; - output += toolArgs; - emit(output); - } - /** * Emit tool call result and end marker. */