Merge pull request #6001 from nesquena/stage/5994-fork-streaming

Release exp: fork-from-here during active streaming (#5994, closes #5993)
This commit is contained in:
nesquena-hermes authored and GitHub committed 2026-07-12 19:37:25 -07:00
commit 1ab1540ecd
3 files changed
+24 -4

No files matched your search

+2
View File
@@ -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:<slug>` 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)
+16 -2
View File
@@ -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;
+6 -2
View File
@@ -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();