chore(aichat): display tool usage earlier (#6917)

* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

* cleaning

* cleaning

* cleaning

* fix icon

* nit

* handle error

* nit

---------

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
centdix
2025-10-23 22:14:34 +02:00
committed by GitHub
parent 26cf0ca14e
commit 949f96d66a
6 changed files with 62 additions and 28 deletions
@@ -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
}
}
@@ -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
@@ -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<boolean> {
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
})
@@ -269,7 +269,6 @@ export async function processToolCall<T>({
// 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<T> {
export interface ToolCallbacks {
setToolStatus: (id: string, metadata?: Partial<ToolDisplayMessage>) => void
removeToolStatus: (id: string) => void
requestConfirmation?: (toolId: string) => Promise<boolean>
}
+25 -24
View File
@@ -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<K extends boolean>({
}): {
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...`
})
}
}
}
@@ -207,7 +207,7 @@
{#if provider === 'anthropic'}
<Badge color="blue">
Recommended
<Tooltip class="text-blue-800 dark:text-blue-800 mt-0.5">
<Tooltip>
Anthropic models handle tool calls better than other providers, which makes them a
better choice for AI chat.
</Tooltip>