feat: add finalizeIntermediaryMessage for StreamingProcessor (#5333) * feat: add finalizeIntermediaryMessage for streaming tool call chains Add a lighter finalization method on StreamingProcessor that runs before tool invocation. This ensures CHARACTER_MESSAGE_RENDERED is emitted for streamed text messages before the tool call chain continues, allowing extensions (TTS, image forwarding, etc.) to process intermediary messages. finalizeIntermediaryMessage performs essential processing: - Code block styling (addCopyToCodeBlocks) - Reasoning handler finalization - Logprobs saving - Image attachment processing - Reasoning signature storage - MESSAGE_RECEIVED and CHARACTER_MESSAGE_RENDERED events Without the heavier onFinishStreaming operations: - UI unlock (markUIGenStopped) - Auto-swipe - Sound playback - Chat saving - Swipe counter update * fix: return Error objects from invokeFunctionTool and create error invocations invokeFunctionTool previously called .toString() on caught errors, converting them to plain strings. This made the 'instanceof Error' check in invokeFunctionTools dead code - errors silently became successful invocations, showToolCallError never fired, and the result.errors array was always empty. Changes: - Return Error objects directly from invokeFunctionTool - Create error invocations with error: true flag when tools fail - Add error field to ToolInvocation typedef * fix: record failed stealth tools in stealthCalls and preserve signature/reasoning on error invocations When a stealth tool errors, its name was not added to stealthCalls, causing shouldStopGeneration to evaluate as false. This led to an incorrect recursive Generate('normal') call instead of stopping generation as the stealth tool contract requires ('no follow-up generation'). Also preserve toolCall.signature and reasoningText on error invocations to match the success path, preventing Gemini/OpenRouter multi-turn tool context from breaking when a tool call fails. * Clarify method comments * fix: initialize error property in ToolInvocation to false * Apply review suggestions * Make options object required * fix: remove unnecessary return statement in updateSwipeCounter method * refactor: split tool call error handling into separate PR (#5351) --------- Co-authored-by: Cohee <18619528+Cohee1207@users.noreply.github.com>

94139f465ea3a13c46fb2ac79f70fe8c3d4508e5

Xiangzhe <32761048+xz-dev@users.noreply.github.com>

Signed
1 files changed, +20 -2Ignore whitespace
public/script.js+20 -2
@@ -3662,7 +3662,16 @@ class StreamingProcessor {
3662 }3662 }
3663 }3663 }
36643664
3665 async onFinishStreaming(messageId, text) {3665 /**
3666 * Finalizes an intermediary message after generation is complete, or a tool call is performed.
3667 * Performs essential message processing (code blocks, reasoning, swipes, attachments, events)
3668 * without the heavier finish operations (UI unlock - optional, auto-swipe, sound, save chat).
3669 * @param {number} messageId - The message ID to finalize.
3670 * @param {string} text - The message text.
3671 * @param {Object} options - Additional options for finalization.
3672 * @param {boolean} options.unlockUI - Whether to unlock the generation UI.
3673 */
3674 async finalizeIntermediaryMessage(messageId, text, { unlockUI = true }) {
3666 await this.onProgressStreaming(messageId, text, true);3675 await this.onProgressStreaming(messageId, text, true);
3667 const messageElement = chatElement.find(`.mes[mesid="${messageId}"]`);3676 const messageElement = chatElement.find(`.mes[mesid="${messageId}"]`);
3668 const message = chat[messageId];3677 const message = chat[messageId];
@@ -3701,7 +3710,9 @@ class StreamingProcessor {
3701 message.extra.reasoning_signature = this.reasoningSignature;3710 message.extra.reasoning_signature = this.reasoningSignature;
3702 }3711 }
37033712
3704 this.markUIGenStopped();3713 if (unlockUI) {
3714 this.markUIGenStopped();
3715 }
37053716
3706 if (this.type !== 'impersonate') {3717 if (this.type !== 'impersonate') {
3707 await eventSource.emit(event_types.MESSAGE_RECEIVED, this.messageId, this.type);3718 await eventSource.emit(event_types.MESSAGE_RECEIVED, this.messageId, this.type);
@@ -3711,6 +3722,10 @@ class StreamingProcessor {
3711 }3722 }
37123723
3713 updateSwipeCounter(messageId, { message, messageElement });3724 updateSwipeCounter(messageId, { message, messageElement });
3725 }
3726
3727 async onFinishStreaming(messageId, text) {
3728 await this.finalizeIntermediaryMessage(messageId, text, { unlockUI: true });
37143729
3715 const isAborted = this.abortController.signal.aborted;3730 const isAborted = this.abortController.signal.aborted;
3716 if (!isAborted && power_user.auto_swipe && generatedTextFiltered(text)) {3731 if (!isAborted && power_user.auto_swipe && generatedTextFiltered(text)) {
@@ -5306,6 +5321,9 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
5306 const hasToolCalls = ToolManager.hasToolCalls(streamingProcessor.toolCalls);5321 const hasToolCalls = ToolManager.hasToolCalls(streamingProcessor.toolCalls);
5307 const shouldDeleteMessage = type !== 'swipe' && ['', '...'].includes(lastMessage?.mes) && !lastMessage?.extra?.reasoning && ['', '...'].includes(streamingProcessor?.result);5322 const shouldDeleteMessage = type !== 'swipe' && ['', '...'].includes(lastMessage?.mes) && !lastMessage?.extra?.reasoning && ['', '...'].includes(streamingProcessor?.result);
5308 hasToolCalls && shouldDeleteMessage && await deleteLastMessage();5323 hasToolCalls && shouldDeleteMessage && await deleteLastMessage();
5324 if (hasToolCalls && !shouldDeleteMessage) {
5325 await streamingProcessor.finalizeIntermediaryMessage(streamingProcessor.messageId, getMessage, { unlockUI: false });
5326 }
5309 const invocationResult = await ToolManager.invokeFunctionTools(streamingProcessor.toolCalls, {5327 const invocationResult = await ToolManager.invokeFunctionTools(streamingProcessor.toolCalls, {
5310 reasoningText: streamingProcessor.reasoningHandler.reasoning,5328 reasoningText: streamingProcessor.reasoningHandler.reasoning,
5311 });5329 });