diff --git a/CHANGELOG.md b/CHANGELOG.md index 0743df6e..e5356272 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ ### Fixed +- **"Fork from here" works during an active response.** The fork button was silently dead while the agent was generating — clicking it did nothing. You can now fork from any past (already-committed) message while a response streams; only the currently-streaming message and the just-sent (not-yet-committed) prompt are blocked, with a clear toast. Thanks @kertisalex. (#5994, #5993) + - **No more false "No response from provider" after a completed answer that used tools.** When an assistant turn ended with a real final answer delivered as structured content following tool calls (common with reasoning models and multi-tool turns), the completion detector could miss the final text and render a spurious "No response from provider" card under the finished reply. Final-answer detection now understands structured content parts (text after tool-use boundaries), while genuine empty completions still correctly show the no-response notice, and a real terminal provider error still surfaces as an error (never masked as a final answer). Thanks @jtstothard. (#5990, #5686) - **Custom/proxy providers keep the full vendor namespace when you deliberately pick a non-default model (follow-up to the earlier #5979 fix).** The initial fix preserved `x-ai/grok-4.5` when it was your configured default or already in a warm model catalog, but a model you *selected in the picker* that wasn't your default (e.g. `x-ai/grok-composer-2.5-fast`) was still truncated to the bare `grok-composer-2.5-fast` when the models catalog was cold at send time — the normal state right after a restart, a settings save, or a cache expiry. The proxy then rejected the bare id with `HTTP 400`. For a `custom` / `custom:` proxy the resolver now remembers that you deliberately selected the model this session and preserves the full `vendor/model` id on a cold catalog, while a stale cross-provider leftover you didn't pick still has its redundant prefix stripped so it keeps routing. The send path also warms model-catalog provenance from the durable on-disk cache first (network-free, non-blocking, never triggers a live probe). Thanks @b3nw for the detailed follow-up and the CLI-vs-WebUI comparison that localized it. (#5979) diff --git a/static/commands.js b/static/commands.js index 74e33ab6..18728773 100644 --- a/static/commands.js +++ b/static/commands.js @@ -1944,7 +1944,19 @@ async function cmdBranch(args){ // count (_oldestIdx + msgIdx) BEFORE awaiting _ensureAllMessagesLoaded, // which resets _oldestIdx to 0 after its wholesale replace. See #2184. async function forkFromMessage(msgIdx){ - if(!S.session||S.busy)return; + if(!S.session)return; + // During streaming, only block fork if the clicked message is the + // currently-streaming (live) message itself. Past messages that are + // already committed server-side can be forked immediately without + // waiting for the stream to finish. + if(S.busy){ + const _msg=(Array.isArray(S.messages)&&S.messages[msgIdx-1])||null; + const _isLastMsg=msgIdx>=(Array.isArray(S.messages)?S.messages.length:0); + if((_msg&&(_msg._live||_msg._pending))||_isLastMsg){ + showToast('Cannot fork a message still being generated.',3000); + return; + } + } const readOnlySession=typeof _isReadOnlySession==='function' ? _isReadOnlySession(S.session) : !!(S.session&&(S.session.read_only||S.session.is_read_only)); @@ -1959,7 +1971,9 @@ async function forkFromMessage(msgIdx){ const absoluteKeepCount = _oldestIdx + msgIdx; // Ensure the full transcript is loaded so the forked session renders // correctly and subsequent operations see the complete history. - if(typeof _ensureAllMessagesLoaded==='function'){ + // Skip during streaming to avoid visual flicker — the fork data + // (absoluteKeepCount) was already captured above and is unaffected. + if(!S.busy && typeof _ensureAllMessagesLoaded==='function'){ await _ensureAllMessagesLoaded(); } if(!S.session || S.session.session_id !== initialSid) return; diff --git a/static/messages.js b/static/messages.js index 7036ac7d..62ef3350 100644 --- a/static/messages.js +++ b/static/messages.js @@ -1629,7 +1629,7 @@ async function send(){ // upload window. _composerDraftClearPromise / _submittedDraftFilesForClear are // set there; nothing to re-declare here. const displayText=_slashDisplayTextOverride||text||(uploaded.length?`Uploaded: ${uploadedNames.join(', ')}`:'(file upload)'); - const userMsg={role:'user',content:displayText,attachments:uploaded.length?uploadedNames:undefined,_ts:Date.now()/1000}; + const userMsg={role:'user',content:displayText,attachments:uploaded.length?uploadedNames:undefined,_ts:Date.now()/1000,_pending:true}; S.toolCalls=[]; // clear tool calls from previous turn clearLiveToolCards(); // clear any leftover live cards from last turn let optimisticMessages; @@ -7733,7 +7733,11 @@ function showClarifyCard(pending) { _clarifySessionId = sid; _clarifyId = pending.clarify_id || null; _clarifySignature = sig; - _startClarifyCountdown(pending); + if (Number(pending.timeout_seconds) > 0) { + _startClarifyCountdown(pending); + } else { + _clearClarifyCountdownTimer(); + } if (!sameClarify) { _clarifyVisibleSince = Date.now(); _clearClarifyHideTimer();