fix: streaming race conditions (#631) + blank-page workspace binding (#804)

Closes #631. Closes #804.

Bug A (thinking card below answer / double render / stuck cursor): trailing rAF after 'done'
inserted a duplicate live-turn wrapper into already-settled DOM. Fixed via _streamFinalized flag
+ cancelAnimationFrame in all terminal handlers (done/apperror/cancel/_handleStreamError) +
_scheduleRender guard. All three reported symptoms were the same root cause.

Bug B (accumulator reset): original fix reset assistantText/reasoningText inside _wireSSE on reconnect.
Reverted — server uses one-shot queue.Queue(), no replay on reconnect, reset would wipe valid
pre-drop content causing data loss. Bug A fix alone resolves all symptoms.

#804 (blank page workspace): syncWorkspaceDisplays uses S._profileDefaultWorkspace as fallback;
workspace chip enabled when hasWorkspace (not hasSession); promptNewFile/promptNewFolder/
switchToWorkspace/promptWorkspacePath auto-create session on blank page; boot.js hydrates
_profileDefaultWorkspace from /api/settings before any session exists.

Opus max-effort review + Nathan independent review + full browser QA. 1765/1765 tests.
This commit is contained in:
nesquena-hermes
2026-04-21 18:47:40 -07:00
committed by GitHub
parent c3807482be
commit 859602340e
9 changed files with 431 additions and 11 deletions

View File

@@ -537,8 +537,12 @@ function getWorkspaceFriendlyName(path){
function syncWorkspaceDisplays(){
const hasSession=!!(S.session&&S.session.workspace);
const ws=hasSession?S.session.workspace:'';
const label=hasSession?getWorkspaceFriendlyName(ws):t('no_workspace');
// Fall back to the profile default workspace when no session is active yet.
// S._profileDefaultWorkspace is set during boot and profile switches from /api/settings.
const defaultWs=(typeof S._profileDefaultWorkspace==='string'&&S._profileDefaultWorkspace)||'';
const ws=hasSession?S.session.workspace:(defaultWs||'');
const hasWorkspace=!!(ws);
const label=hasWorkspace?getWorkspaceFriendlyName(ws):t('no_workspace');
const sidebarName=$('sidebarWsName');
const sidebarPath=$('sidebarWsPath');
@@ -548,13 +552,13 @@ function syncWorkspaceDisplays(){
const composerChip=$('composerWorkspaceChip');
const composerLabel=$('composerWorkspaceLabel');
const composerDropdown=$('composerWsDropdown');
if(!hasSession && composerDropdown) composerDropdown.classList.remove('open');
if(!hasWorkspace && composerDropdown) composerDropdown.classList.remove('open');
// Only show workspace label once boot has finished to prevent
// flash of "No workspace" before the saved session finishes loading.
if(composerLabel) composerLabel.textContent=S._bootReady?label:'';
if(composerChip){
composerChip.disabled=!hasSession;
composerChip.title=hasSession?ws:t('no_workspace');
composerChip.disabled=!hasWorkspace;
composerChip.title=hasWorkspace?ws:t('no_workspace');
composerChip.classList.toggle('active',!!(composerDropdown&&composerDropdown.classList.contains('open')));
}
}
@@ -738,7 +742,16 @@ async function removeWorkspace(path){
}
async function promptWorkspacePath(){
if(!S.session)return;
// Opus review Q6: if called from blank page (no session), auto-create one first.
if(!S.session){
const ws=(typeof S._profileDefaultWorkspace==='string'&&S._profileDefaultWorkspace)||'';
if(!ws)return;
try{
const r=await api('/api/session/new',{method:'POST',body:JSON.stringify({workspace:ws})});
if(r&&r.session){S.session=r.session;S.messages=[];if(typeof syncTopbar==='function')syncTopbar();if(typeof renderMessages==='function')renderMessages();if(typeof renderSessionList==='function')await renderSessionList();}
}catch(e){showToast(t('workspace_switch_failed')+e.message);return;}
if(!S.session)return;
}
const value=await showPromptDialog({
title:t('workspace_switch_prompt_title'),
message:t('workspace_switch_prompt_message'),
@@ -764,7 +777,17 @@ async function promptWorkspacePath(){
}
async function switchToWorkspace(path,name){
if(!S.session)return;
// Opus review Q6: if called from blank page, auto-create a session bound to
// the requested workspace so the switch doesn't silently no-op.
if(!S.session){
const ws=path||(typeof S._profileDefaultWorkspace==='string'&&S._profileDefaultWorkspace)||'';
if(!ws){showToast(t('no_workspace'));return;}
try{
const r=await api('/api/session/new',{method:'POST',body:JSON.stringify({workspace:ws})});
if(r&&r.session){S.session=r.session;S.messages=[];if(typeof syncTopbar==='function')syncTopbar();if(typeof renderMessages==='function')renderMessages();if(typeof renderSessionList==='function')await renderSessionList();}
}catch(e){if(typeof setStatus==='function')setStatus(t('switch_failed')+e.message);return;}
if(!S.session)return;
}
if(S.busy){
showToast(t('workspace_busy_switch'));
return;