diff --git a/AIClasses/Claude/Claude.ts b/AIClasses/Claude/Claude.ts index 22e009a..6a71549 100644 --- a/AIClasses/Claude/Claude.ts +++ b/AIClasses/Claude/Claude.ts @@ -115,6 +115,13 @@ export class Claude extends BaseAIClass { this.accumulatedFunctionName = toolBlock.name || null; this.accumulatedFunctionArgs = ""; this.accumulatedFunctionId = toolBlock.id || null; + + // Return immediately to signal tool call has started + return { + content: "", + isComplete: false, + toolCallStarted: toolBlock.name + }; } } diff --git a/AIClasses/OpenAI/OpenAI.ts b/AIClasses/OpenAI/OpenAI.ts index 6e0311d..a449e45 100644 --- a/AIClasses/OpenAI/OpenAI.ts +++ b/AIClasses/OpenAI/OpenAI.ts @@ -7,7 +7,7 @@ import { AIProvider, AIProviderURL } from "Enums/ApiProvider"; import { AIToolCall } from "AIClasses/AIToolCall"; import { fromString as aiToolFromString } from "Enums/AITool"; import type { IAIToolDefinition } from "AIClasses/ToolDefinitions/IAIToolDefinition"; -import type { ResponseEvent, ResponseOutputTextDelta, ResponseOutputItemDone, ResponseErrorEvent, ResponseFailedEvent, OpenAIToolTool, ResponsesAPIInput } from "./OpenAITypes"; +import type { ResponseEvent, ResponseOutputTextDelta, ResponseOutputItemAdded, ResponseOutputItemDone, ResponseErrorEvent, ResponseFailedEvent, OpenAIToolTool, ResponsesAPIInput } from "./OpenAITypes"; import { Exception } from "Helpers/Exception"; import { ApiError, ApiErrorType } from "Types/ApiError"; import { MimeType, toMimeType } from "Enums/MimeType"; @@ -138,6 +138,21 @@ export class OpenAI extends BaseAIClass { break; } + case "response.output_item.added": { + // Function call starting - get name immediately for early UI feedback + const itemAddedEvent = event as ResponseOutputItemAdded; + + // Check if this is a function call and return tool name immediately + if (itemAddedEvent.item.type === "function_call" && itemAddedEvent.item.name) { + return { + content: "", + isComplete: false, + toolCallStarted: itemAddedEvent.item.name + }; + } + break; + } + case "response.output_item.done": { // Complete output item received - this includes function calls with name const itemDoneEvent = event as ResponseOutputItemDone; @@ -167,7 +182,6 @@ export class OpenAI extends BaseAIClass { case "response.in_progress": case "response.content_part.added": case "response.content_part.done": - case "response.output_item.added": case "response.output_text.done": case "response.web_search_call.in_progress": case "response.web_search_call.searching": diff --git a/AIClasses/OpenAI/OpenAITypes.ts b/AIClasses/OpenAI/OpenAITypes.ts index 34c4bb6..0985338 100644 --- a/AIClasses/OpenAI/OpenAITypes.ts +++ b/AIClasses/OpenAI/OpenAITypes.ts @@ -21,6 +21,19 @@ export interface ResponseToolCallArgumentsDone extends ResponseEvent { sequence_number: number; } +export interface ResponseOutputItemAdded extends ResponseEvent { + type: "response.output_item.added"; + response_id: string; + output_index: number; + item: { + type: string; + id?: string; + call_id?: string; + name?: string; + arguments?: string; + }; +} + export interface ResponseOutputItemDone extends ResponseEvent { type: "response.output_item.done"; item_id: string; diff --git a/Components/ChatArea.svelte b/Components/ChatArea.svelte index b435405..19d62b4 100644 --- a/Components/ChatArea.svelte +++ b/Components/ChatArea.svelte @@ -289,7 +289,7 @@ {/if} {/each} - + {#if isSubmitting} {/if} diff --git a/Components/ChatPlanArea.svelte b/Components/ChatPlanArea.svelte index c4763b6..ab43110 100644 --- a/Components/ChatPlanArea.svelte +++ b/Components/ChatPlanArea.svelte @@ -114,7 +114,7 @@ {#if busyPlanning}
- + {$executionPlanState.plan?.isReplan ? "Replanning in progress..." : "Planning in progress..."}
{/if} diff --git a/Components/ChatWindow.svelte b/Components/ChatWindow.svelte index 7e3b91e..78d878b 100644 --- a/Components/ChatWindow.svelte +++ b/Components/ChatWindow.svelte @@ -20,6 +20,7 @@ import type { ExecutionPlanStore } from "Stores/ExecutionPlanStore"; import type { StreamingMarkdownService } from "Services/StreamingMarkdownService"; import { HTMLService } from "Services/HTMLService"; + import { AITool, fromString } from "Enums/AITool"; const plugin: VaultkeeperAIPlugin = Resolve(Services.VaultkeeperAIPlugin); const executionPlanStore: ExecutionPlanStore = Resolve(Services.ExecutionPlanStore); @@ -121,6 +122,19 @@ currentThought = thought; } }, + onToolCallStarted: (toolName: string) => { + const tool = fromString(toolName); + switch(tool) { + case AITool.WriteVaultFile: + case AITool.PatchVaultFile: + currentThought = Copy.AIThoughtGeneratingNote; + break; + case AITool.AskUserQuestionPlanning: + case AITool.AskUserQuestionExecution: + currentThought = Copy.AIThoughtPreparingQuery; + break; + } + }, onPlanningStarted: () => { busyPlanning = true; }, diff --git a/Components/Spinner.svelte b/Components/Spinner.svelte index b913e4b..89e5b4b 100644 --- a/Components/Spinner.svelte +++ b/Components/Spinner.svelte @@ -2,6 +2,7 @@ export let width: string = "18px"; export let height: string = "18px"; export let editModeActive: boolean = false; + export let alternateBackground: boolean = false; export let spinnerElement: HTMLDivElement | undefined = undefined; @@ -11,7 +12,7 @@ style="width: {width}; height: {height};" bind:this={spinnerElement} > -
+
diff --git a/Components/ThoughtIndicator.svelte b/Components/ThoughtIndicator.svelte index ceb52de..bfaa621 100644 --- a/Components/ThoughtIndicator.svelte +++ b/Components/ThoughtIndicator.svelte @@ -1,14 +1,19 @@ {#if isVisible}
- {thought} + {thought}
{/if} @@ -33,12 +38,15 @@ z-index: 1; background: var(--background-primary-alt); border-radius: 10px; - padding: 0.75rem 1rem; + padding: 0.55rem 0.7rem; font-size: var(--font-smallest); color: var(--text-muted); font-style: italic; word-wrap: break-word; width: 100%; + display: inline-flex; + align-items: center; + gap: 0.5rem; } .ai-thought-bubble::after { diff --git a/Enums/Copy.ts b/Enums/Copy.ts index 615e25d..d584236 100644 --- a/Enums/Copy.ts +++ b/Enums/Copy.ts @@ -73,6 +73,8 @@ export enum Copy { TooltipLearnMoreFileMonitoring = "Learn more in Plugin Guide", AIThoughtMessage = "Thinking...", + AIThoughtGeneratingNote = "Generating note contents...", + AIThoughtPreparingQuery = "Preparing user query...", // Rate Limit Countdown RateLimitCountdown = "Rate limit exceeded retrying in {seconds}...", diff --git a/Services/AIServices/BaseAgent.ts b/Services/AIServices/BaseAgent.ts index 89f33dd..12781a2 100644 --- a/Services/AIServices/BaseAgent.ts +++ b/Services/AIServices/BaseAgent.ts @@ -138,6 +138,11 @@ export abstract class BaseAgent { break; } + if (chunk.toolCallStarted) { + this.debugService?.log("ToolCallStarted", `Tool call started: ${chunk.toolCallStarted}`); + callbacks.onToolCallStarted(chunk.toolCallStarted); + } + if (chunk.toolCall) { this.debugService?.log("ToolCall", `Function call captured: ${chunk.toolCall.name}`); capturedToolCall = chunk.toolCall; diff --git a/Services/ChatService.ts b/Services/ChatService.ts index b1f6562..2b4aa83 100644 --- a/Services/ChatService.ts +++ b/Services/ChatService.ts @@ -21,6 +21,7 @@ export interface IChatServiceCallbacks { onSubmit: () => void; onStreamingUpdate: (streamingMessageId: string | null) => void; onThoughtUpdate: (thought: string | null) => void; + onToolCallStarted: (toolName: string) => void; onPlanningStarted: () => void; onPlanningFinished: () => void; onUserQuestion: (question: string) => Promise; diff --git a/Services/StreamingService.ts b/Services/StreamingService.ts index fa2582f..d90e9b4 100644 --- a/Services/StreamingService.ts +++ b/Services/StreamingService.ts @@ -14,6 +14,7 @@ export interface IStreamChunk { error?: string; errorType?: ApiErrorType; toolCall?: AIToolCall; + toolCallStarted?: string; shouldContinue?: boolean; }