From 949f96d66af0229cea03fadf1d1edd5c2f64fd26 Mon Sep 17 00:00:00 2001 From: centdix <40307056+centdix@users.noreply.github.com> Date: Thu, 23 Oct 2025 22:14:34 +0200 Subject: [PATCH] chore(aichat): display tool usage earlier (#6917) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(copilot): display tool calls immediately in loading state during streaming Display tool calls in loading state as soon as they are parsed during OpenAI streaming, rather than waiting until processToolCall is invoked. Changes: - parseOpenAICompletion: Track initialized tool calls and display them immediately when we have complete tool info (id + function.name) - processToolCall: Updated comment to clarify it merges with existing loading state set during parsing This provides better UX by showing tool execution progress progressively as the stream is parsed. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude * feat(copilot): display Anthropic tool calls immediately in loading state Apply the same immediate tool call display pattern to Anthropic streaming that was implemented for OpenAI. Changes: - parseAnthropicCompletion: Display tool calls immediately in loading state when tool_use blocks are received in the message event This ensures consistent UX across both OpenAI and Anthropic providers, showing tool execution progress as soon as tool calls are detected. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude * feat(copilot): show Anthropic tool calls even earlier with temp displays Display temporary loading states for Anthropic tool calls as soon as inputJson events are received (when tool input starts streaming), then replace them with real tool displays when complete tool_use blocks arrive in the message event. Changes: - ToolCallbacks: Added removeToolStatus method to clean up temp displays - AIChatManager: Implemented removeToolStatus to remove tool messages from displayMessages array - anthropic.ts: * Display temp tool on first inputJson event (earliest indicator) * Flush pending text message before showing temp tool (proper ordering) * Remove temp display when complete tool_use block arrives * Replace with real tool display via preAction This provides the earliest possible feedback for Anthropic tool calls, showing loading states as soon as the model starts generating tool inputs rather than waiting for complete blocks. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude * cleaning * cleaning * cleaning * fix icon * nit * handle error * nit --------- Co-authored-by: Claude --- .../copilot/chat/AIChatManager.svelte.ts | 12 ++++- .../copilot/chat/ToolExecutionDisplay.svelte | 2 +- .../lib/components/copilot/chat/anthropic.ts | 23 +++++++++ .../src/lib/components/copilot/chat/shared.ts | 2 +- frontend/src/lib/components/copilot/lib.ts | 49 ++++++++++--------- .../workspaceSettings/AISettings.svelte | 2 +- 6 files changed, 62 insertions(+), 28 deletions(-) diff --git a/frontend/src/lib/components/copilot/chat/AIChatManager.svelte.ts b/frontend/src/lib/components/copilot/chat/AIChatManager.svelte.ts index 940ee5e6d6..53cd319d55 100644 --- a/frontend/src/lib/components/copilot/chat/AIChatManager.svelte.ts +++ b/frontend/src/lib/components/copilot/chat/AIChatManager.svelte.ts @@ -480,7 +480,8 @@ class AIChatManager { reply += token }, onMessageEnd: () => {}, - setToolStatus: () => {} + setToolStatus: () => {}, + removeToolStatus: () => {} }, systemMessage } @@ -680,6 +681,15 @@ class AIChatManager { this.displayMessages.push(newMessage) } }, + removeToolStatus: (id) => { + const existingIdx = this.displayMessages.findIndex( + (m) => m.role === 'tool' && m.tool_call_id === id + ) + if (existingIdx !== -1) { + this.displayMessages.splice(existingIdx, 1) + this.displayMessages = [...this.displayMessages] + } + }, requestConfirmation: this.requestConfirmation } } diff --git a/frontend/src/lib/components/copilot/chat/ToolExecutionDisplay.svelte b/frontend/src/lib/components/copilot/chat/ToolExecutionDisplay.svelte index 4a417dad31..5db4f97857 100644 --- a/frontend/src/lib/components/copilot/chat/ToolExecutionDisplay.svelte +++ b/frontend/src/lib/components/copilot/chat/ToolExecutionDisplay.svelte @@ -12,7 +12,7 @@ let { message }: Props = $props() - let isExpanded = $state(message.showDetails || (message.isLoading && message.needsConfirmation)) + let isExpanded = $derived(message.showDetails || (message.isLoading && message.needsConfirmation)) const hasParameters = $derived( message.parameters !== undefined && Object.keys(message.parameters).length > 0 diff --git a/frontend/src/lib/components/copilot/chat/anthropic.ts b/frontend/src/lib/components/copilot/chat/anthropic.ts index 645cc40644..b752cefd5c 100644 --- a/frontend/src/lib/components/copilot/chat/anthropic.ts +++ b/frontend/src/lib/components/copilot/chat/anthropic.ts @@ -14,6 +14,7 @@ import type { import type { MessageStream } from '@anthropic-ai/sdk/lib/MessageStream' import { getProviderAndCompletionConfig, workspaceAIClients } from '../lib' import { processToolCall, type Tool, type ToolCallbacks } from './shared' +import { generateRandomString } from '$lib/utils' export async function getAnthropicCompletion( messages: ChatCompletionMessageParam[], @@ -60,6 +61,16 @@ export async function parseAnthropicCompletion( ): Promise { let toolCallsToProcess: ChatCompletionMessageFunctionToolCall[] = [] let error = null + let tempToolId: string | undefined = undefined + + // When we receive a JSON input, we need to show a temporary tool call in loading state + completion.on('inputJson', (_: string) => { + if (!tempToolId) { + callbacks.onMessageEnd() + tempToolId = `temp-${generateRandomString(12)}` + callbacks.setToolStatus(tempToolId, { isLoading: true, content: 'Calling tool...' }) + } + }) // Handle text streaming completion.on('text', (textDelta: string, _textSnapshot: string) => { @@ -75,6 +86,11 @@ export async function parseAnthropicCompletion( addedMessages.push(assistantMessage) callbacks.onMessageEnd() } else if (block.type === 'tool_use') { + // Remove temp display if it exists + if (tempToolId) { + callbacks.removeToolStatus(tempToolId) + } + // Convert Anthropic tool calls to OpenAI format for compatibility toolCallsToProcess.push({ id: block.id, @@ -91,10 +107,17 @@ export async function parseAnthropicCompletion( } } } + + // Clear temp tracking after processing + tempToolId = undefined }) // Handle errors completion.on('error', (e: any) => { + if (tempToolId) { + callbacks.removeToolStatus(tempToolId) + tempToolId = undefined + } console.error('Anthropic stream error:', e) error = e }) diff --git a/frontend/src/lib/components/copilot/chat/shared.ts b/frontend/src/lib/components/copilot/chat/shared.ts index 6ab3cbfd31..655f44ddc0 100644 --- a/frontend/src/lib/components/copilot/chat/shared.ts +++ b/frontend/src/lib/components/copilot/chat/shared.ts @@ -269,7 +269,6 @@ export async function processToolCall({ // Check if tool requires confirmation const needsConfirmation = tool?.requiresConfirmation - // Add the tool to the display with appropriate status toolCallbacks.setToolStatus(toolCall.id, { ...(tool?.requiresConfirmation ? { content: tool.confirmationMessage ?? 'Waiting for confirmation...' } @@ -363,6 +362,7 @@ export interface Tool { export interface ToolCallbacks { setToolStatus: (id: string, metadata?: Partial) => void + removeToolStatus: (id: string) => void requestConfirmation?: (toolId: string) => Promise } diff --git a/frontend/src/lib/components/copilot/lib.ts b/frontend/src/lib/components/copilot/lib.ts index 7e18c29b14..e589f2d751 100644 --- a/frontend/src/lib/components/copilot/lib.ts +++ b/frontend/src/lib/components/copilot/lib.ts @@ -1,10 +1,5 @@ import type { AIProvider, AIProviderModel } from '$lib/gen' -import { - workspaceStore, - type DBSchema, - type GraphqlSchema, - type SQLSchema -} from '$lib/stores' +import { workspaceStore, type DBSchema, type GraphqlSchema, type SQLSchema } from '$lib/stores' import { buildClientSchema, printSchema } from 'graphql' import OpenAI from 'openai' import type { @@ -213,16 +208,16 @@ function getModelSpecificConfig( return { ...(modelProvider.model.endsWith('/thinking') ? { - thinking: { - type: 'enabled', - budget_tokens: 1024 - }, - model: modelProvider.model.slice(0, -9) - } + thinking: { + type: 'enabled', + budget_tokens: 1024 + }, + model: modelProvider.model.slice(0, -9) + } : { - model: modelProvider.model, - temperature: 0 - }), + model: modelProvider.model, + temperature: 0 + }), ...(tools && tools.length > 0 ? { tools } : {}), max_tokens: maxTokens } @@ -553,8 +548,8 @@ export function getProviderAndCompletionConfig({ }): { provider: AIProvider config: K extends true - ? ChatCompletionCreateParamsStreaming - : ChatCompletionCreateParamsNonStreaming + ? ChatCompletionCreateParamsStreaming + : ChatCompletionCreateParamsNonStreaming } { const modelProvider = forceModelProvider ?? getCurrentModel() const providerConfig = PROVIDER_COMPLETION_CONFIG_MAP[modelProvider.provider] @@ -612,13 +607,13 @@ export async function getNonStreamingCompletion( } const openaiClient = testOptions?.apiKey ? new OpenAI({ - baseURL: `${location.origin}${OpenAPI.BASE}/ai/proxy`, - apiKey: 'fake-key', - defaultHeaders: { - Authorization: '' // a non empty string will be unable to access Windmill backend proxy - }, - dangerouslyAllowBrowser: true - }) + baseURL: `${location.origin}${OpenAPI.BASE}/ai/proxy`, + apiKey: 'fake-key', + defaultHeaders: { + Authorization: '' // a non empty string will be unable to access Windmill backend proxy + }, + dangerouslyAllowBrowser: true + }) : workspaceAIClients.getOpenaiClient() const completion = await openaiClient.chat.completions.create(config, fetchOptions) @@ -797,6 +792,12 @@ export async function parseOpenAICompletion( if (tool && tool.preAction) { tool.preAction({ toolCallbacks: callbacks, toolId: toolCallId }) } + + // Display tool call immediately in loading state + callbacks.setToolStatus(toolCallId, { + isLoading: true, + content: `Calling ${funcName} tool...` + }) } } } diff --git a/frontend/src/lib/components/workspaceSettings/AISettings.svelte b/frontend/src/lib/components/workspaceSettings/AISettings.svelte index 0c006b2c40..0a997fd43d 100644 --- a/frontend/src/lib/components/workspaceSettings/AISettings.svelte +++ b/frontend/src/lib/components/workspaceSettings/AISettings.svelte @@ -207,7 +207,7 @@ {#if provider === 'anthropic'} Recommended - + Anthropic models handle tool calls better than other providers, which makes them a better choice for AI chat.