From bae87bfc47b6f76c3eb00611e2769c4d0d3d8391 Mon Sep 17 00:00:00 2001 From: Grant Doyle Date: Wed, 20 May 2026 16:39:24 -0500 Subject: [PATCH] =?UTF-8?q?fix:=20keep=20"Building=E2=80=A6"=20indicator?= =?UTF-8?q?=20visible=20during=20tool=20calls=20+=20recover=20from=20empty?= =?UTF-8?q?=20responses?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two real symptoms users hit during agentic loops: 1. The brick animation went away while tools were running. addMessageEl for the "Calling tool: X" line removed the indicator and it didn't come back until the whole iteration's tools finished. For slow stdio MCP calls (3-10s), users saw "Calling tool:" then static silence and assumed Mason was wedged. 2. Sometimes Opus 4.7 returned an *empty* text response after a tool loop — extended thinking burned through the 4096 max_tokens budget before any visible output. Mason silently rendered an empty assistant bubble and exited the loop, looking dead. Changes: - src/chat.ts: call showThinking() right after the "Calling tool:" message, so the brick stays visible while the tool actually runs. - src/chat.ts: detect empty text response in result.type === "text" and surface a clear in-chat error explaining the likely cause ("Model returned an empty response — likely hit its token budget"). Different message if there were prior tool calls (suggesting 'continue' to resume) vs not (suggesting retry or switch models). - src/main.ts: bump max_tokens 4096 -> 16384 (both chat-completions max_tokens and Responses API max_output_tokens). Gives extended- thinking models like Opus 4.7 headroom before content output, and reduces tool-call argument truncation (see prior sanitize fix). Co-authored-by: Isaac --- src/chat.ts | 32 +++++++++++++++++++++++++++++--- src/main.ts | 6 +++--- 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/src/chat.ts b/src/chat.ts index f99cbbb..8ecc8d8 100644 --- a/src/chat.ts +++ b/src/chat.ts @@ -317,14 +317,34 @@ async function chatLoop(_profile: { host?: string }): Promise { if (result.type === "text") { const messagesEl = mason.el.messages as HTMLElement | null; - (mason.history as any[]).push({ role: "assistant", content: result.content }); + const content = result.content || ""; + // If we get back an empty text response in the middle of a multi-turn + // tool loop, the model most likely truncated (e.g. Opus 4.7 burned its + // max_tokens budget on extended thinking). Surface a clear hint + // instead of silently rendering an empty bubble and exiting. + if (!content.trim()) { + const hasPriorTools = (mason.history as any[]).some( + (m: any) => m.role === "tool" || (m.role === "assistant" && Array.isArray(m.tool_calls)) + ); + const hint = hasPriorTools + ? "Model returned an empty response — likely hit its token budget mid-thinking. Try sending 'continue' to resume, or rephrase your last request to reduce upstream context." + : "Model returned an empty response. Try sending the message again or switching to a different model."; + addMessageEl("error", hint); + (mason.history as any[]).push({ role: "assistant", content: "" }); + // Remove the empty streaming bubble if one was created. + const sel = streamingEl as HTMLElement | null; + if (sel) sel.remove(); + await saveCurrentChat(); + return; + } + (mason.history as any[]).push({ role: "assistant", content }); const sel = streamingEl as HTMLElement | null; if (sel) { sel.style.whiteSpace = ""; - sel.innerHTML = renderMarkdown(result.content || ""); + sel.innerHTML = renderMarkdown(content); if (messagesEl) messagesEl.scrollTop = messagesEl.scrollHeight; } else { - addMessageEl("assistant", result.content || ""); + addMessageEl("assistant", content); } await saveCurrentChat(); return; @@ -435,6 +455,12 @@ async function chatLoop(_profile: { host?: string }): Promise { } addMessageEl("tool-call", `Calling tool: ${toolName}`); + // Show the building-bricks indicator while the tool runs. addMessageEl + // above removed any existing indicator, and tool execution can take + // multiple seconds for stdio MCP / external API calls. Without this, + // users see "Calling tool:" then static silence and assume Mason is + // stuck. + showThinking(); if (BUILTIN_TOOL_NAMES.has(toolName)) { try { diff --git a/src/main.ts b/src/main.ts index 2c1a176..94eae85 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1982,7 +1982,7 @@ ipcMain.handle( } } - body = { model, max_output_tokens: 4096, input }; + body = { model, max_output_tokens: 16384, input }; if (tools && tools.length > 0) { body.tools = tools.map((t: any) => ({ @@ -2007,14 +2007,14 @@ ipcMain.handle( const hasSystem = messages.some((m: any) => m.role === "system"); body = { model, - max_tokens: 4096, + max_tokens: 16384, messages: hasSystem ? messages : [systemMsg, ...messages], tools, tool_choice: "auto", }; console.log(`[CHAT] Sending ${tools.length} tools: ${toolNames}`); } else { - body = { model, max_tokens: 4096, messages }; + body = { model, max_tokens: 16384, messages }; } }