From 5023053ebfdb2343d5cbd4839cecadcd381ab80f Mon Sep 17 00:00:00 2001 From: kertisalex Date: Sun, 12 Jul 2026 21:34:47 +0200 Subject: [PATCH 1/3] fix: allow fork from here during active streaming MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Removes the blanket S.busy guard from forkFromMessage() that silently swallowed all fork clicks while the agent was generating a response. Instead, only blocks fork when the clicked message itself is the one currently being streamed (msg._live), showing a clear toast. Skips _ensureAllMessagesLoaded() during streaming to avoid visual flicker — absoluteKeepCount is captured before any async work so the fork data is unaffected. --- static/commands.js | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/static/commands.js b/static/commands.js index 74e33ab6..ace3c71c 100644 --- a/static/commands.js +++ b/static/commands.js @@ -1944,7 +1944,18 @@ 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; + if(_msg&&_msg._live){ + 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 +1970,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; From 90ee9484e02aeed38bfd6aa9ff1e63c040557f51 Mon Sep 17 00:00:00 2001 From: kertisalex Date: Mon, 13 Jul 2026 00:42:24 +0200 Subject: [PATCH 2/3] fix: also block fork on pending user message during streaming Extends the live-message guard to reject both _live assistant rows and _pending user rows that haven't been committed server-side yet. Adds a belt-and-suspenders check: when S.busy is true, the last message in S.messages is also blocked regardless of flags, closing a timing window where _live might be momentarily absent on the streaming row. The user's optimistically-pushed message (messages.js:1603) now carries _pending:true so forkFromMessage can distinguish it from committed messages. --- static/commands.js | 3 ++- static/messages.js | 8 ++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/static/commands.js b/static/commands.js index ace3c71c..18728773 100644 --- a/static/commands.js +++ b/static/commands.js @@ -1951,7 +1951,8 @@ async function forkFromMessage(msgIdx){ // waiting for the stream to finish. if(S.busy){ const _msg=(Array.isArray(S.messages)&&S.messages[msgIdx-1])||null; - if(_msg&&_msg._live){ + 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; } 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(); From 4b72dea9ab5889f9817de01988c401b1b2193ce2 Mon Sep 17 00:00:00 2001 From: nesquena-hermes Date: Mon, 13 Jul 2026 02:20:55 +0000 Subject: [PATCH 3/3] Release exp: fork-from-here during active streaming (#5994, closes #5993). Thanks @kertisalex. Past committed messages fork during a live response; only the streaming message + just-sent uncommitted prompt are blocked (toast). Codex SAFE round2 (off-by-one fixed, _isLastMsg busy-gated, _pending not carried forward), stream-gate GREEN (hide_all RED was load flake, isolated re-run clean), 99 tests. --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) 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)