diff --git a/static/boot.js b/static/boot.js index 7f378136..ba73d693 100644 --- a/static/boot.js +++ b/static/boot.js @@ -100,8 +100,11 @@ async function _savedSessionSidebarOnlyState(sid){ // ── Mobile navigation ────────────────────────────────────────────────────── // URL prefill boot helpers. +function _prefillHasDraftText(prefillIntent){ + return !!(prefillIntent&&prefillIntent.hasText); +} function _rootPrefillNeedsFreshComposer(urlSession, savedLocal, prefillIntent){ - return !urlSession&&!!savedLocal&&!!(prefillIntent&&prefillIntent.hasText); + return !urlSession&&!!savedLocal&&_prefillHasDraftText(prefillIntent); } async function _applyComposerPrefillOnBoot(prefillIntent){ if(!prefillIntent||!prefillIntent.hasText) return; @@ -112,6 +115,12 @@ async function _applyComposerPrefillOnBoot(prefillIntent){ if(typeof autoResize==='function') autoResize(); else if(typeof updateSendBtn==='function') updateSendBtn(); } +async function _finalizeComposerPrefillOnBoot(prefillIntent){ + if(prefillIntent&&prefillIntent.hasParams&&typeof _consumeComposerPrefillParamsFromLocation==='function'){ + _consumeComposerPrefillParamsFromLocation(); + } + await _applyComposerPrefillOnBoot(prefillIntent); +} // Mobile navigation. let _workspacePanelMode='closed'; // 'closed' | 'browse' | 'preview' @@ -239,7 +248,8 @@ function handleWorkspaceClose(){ closeWorkspacePanel(); } -async function _maybeBindFreshDefaultWorkspaceSession(){ +async function _maybeBindFreshDefaultWorkspaceSession(prefillIntent=null){ + if(_prefillHasDraftText(prefillIntent)) return false; if(S.session) return false; if(_workspacePanelMode!=='browse') return false; if(!S._profileDefaultWorkspace) return false; @@ -2387,9 +2397,6 @@ window._applyTitlebarProfileVisibility=_applyTitlebarProfileVisibility; // Load send key preference let _bootSettings={}; const prefillIntent=(typeof _composerPrefillIntentFromLocation==='function')?_composerPrefillIntentFromLocation():null; - if(prefillIntent&&prefillIntent.hasParams&&typeof _consumeComposerPrefillParamsFromLocation==='function'){ - _consumeComposerPrefillParamsFromLocation(); - } try{ const s=await api('/api/settings'); _bootSettings=s; @@ -2777,7 +2784,7 @@ window._applyTitlebarProfileVisibility=_applyTitlebarProfileVisibility; await newSession(true); if(S.session) await _startBootModelDropdown(); S._bootReady=true; - syncTopbar();syncWorkspacePanelState();await renderSessionList();await _applyComposerPrefillOnBoot(prefillIntent);if(typeof startGatewaySSE==='function')startGatewaySSE();return; + syncTopbar();syncWorkspacePanelState();await renderSessionList();await _finalizeComposerPrefillOnBoot(prefillIntent);if(typeof startGatewaySSE==='function')startGatewaySSE();return; }catch(e){console.warn('[pwa] new-chat launch action failed', e);} } const savedLocal=localStorage.getItem('hermes-webui-session'); @@ -2795,7 +2802,7 @@ window._applyTitlebarProfileVisibility=_applyTitlebarProfileVisibility; S._bootReady=true; syncTopbar();syncWorkspacePanelState(); $('emptyState').style.display=''; - await renderSessionList();await _applyComposerPrefillOnBoot(prefillIntent);if(typeof startGatewaySSE==='function')startGatewaySSE(); + await renderSessionList();await _finalizeComposerPrefillOnBoot(prefillIntent);if(typeof startGatewaySSE==='function')startGatewaySSE(); return; } if(_rootPrefillNeedsFreshComposer(urlSession, savedLocal, prefillIntent)){ @@ -2804,10 +2811,10 @@ window._applyTitlebarProfileVisibility=_applyTitlebarProfileVisibility; const _ephPanelPref=localStorage.getItem('hermes-webui-workspace-panel-pref')==='open' || localStorage.getItem('hermes-webui-workspace-panel')==='open'; if(_ephPanelPref&&!_isCompactWorkspaceViewport()) _workspacePanelMode='browse'; - await _maybeBindFreshDefaultWorkspaceSession(); + await _maybeBindFreshDefaultWorkspaceSession(prefillIntent); syncTopbar();syncWorkspacePanelState(); $('emptyState').style.display=''; - await renderSessionList();await _applyComposerPrefillOnBoot(prefillIntent);if(typeof startGatewaySSE==='function')startGatewaySSE(); + await renderSessionList();await _finalizeComposerPrefillOnBoot(prefillIntent);if(typeof startGatewaySSE==='function')startGatewaySSE(); return; } await loadSession(saved, {preserveActiveInput:true}); @@ -2842,10 +2849,10 @@ window._applyTitlebarProfileVisibility=_applyTitlebarProfileVisibility; const _ephPanelPref=localStorage.getItem('hermes-webui-workspace-panel-pref')==='open' || localStorage.getItem('hermes-webui-workspace-panel')==='open'; if(_ephPanelPref&&!_isCompactWorkspaceViewport()) _workspacePanelMode='browse'; - await _maybeBindFreshDefaultWorkspaceSession(); + await _maybeBindFreshDefaultWorkspaceSession(prefillIntent); syncTopbar();syncWorkspacePanelState(); $('emptyState').style.display=''; - await renderSessionList();await _applyComposerPrefillOnBoot(prefillIntent);if(typeof startGatewaySSE==='function')startGatewaySSE(); + await renderSessionList();await _finalizeComposerPrefillOnBoot(prefillIntent);if(typeof startGatewaySSE==='function')startGatewaySSE(); return; } // Restore the panel from localStorage when the session has a workspace. @@ -2857,7 +2864,7 @@ window._applyTitlebarProfileVisibility=_applyTitlebarProfileVisibility; _workspacePanelMode='browse'; } S._bootReady=true; - syncTopbar();syncWorkspacePanelState();await renderSessionList();if(typeof startGatewaySSE==='function')startGatewaySSE();await checkInflightOnBoot(saved);await _applyComposerPrefillOnBoot(prefillIntent);return;} + syncTopbar();syncWorkspacePanelState();await renderSessionList();if(typeof startGatewaySSE==='function')startGatewaySSE();await checkInflightOnBoot(saved);await _finalizeComposerPrefillOnBoot(prefillIntent);return;} catch(e){localStorage.removeItem('hermes-webui-session');} } // no saved session - show empty state, wait for user to hit + @@ -2868,10 +2875,10 @@ window._applyTitlebarProfileVisibility=_applyTitlebarProfileVisibility; const _freshPanelPref=localStorage.getItem('hermes-webui-workspace-panel-pref')==='open' || localStorage.getItem('hermes-webui-workspace-panel')==='open'; if(_freshPanelPref&&!_isCompactWorkspaceViewport()) _workspacePanelMode='browse'; - await _maybeBindFreshDefaultWorkspaceSession(); + await _maybeBindFreshDefaultWorkspaceSession(prefillIntent); syncWorkspacePanelState(); $('emptyState').style.display=''; - await renderSessionList();await _applyComposerPrefillOnBoot(prefillIntent); + await renderSessionList();await _finalizeComposerPrefillOnBoot(prefillIntent); // Start real-time gateway session sync if setting is enabled if(typeof startGatewaySSE==='function') startGatewaySSE(); })().catch(e=>{ diff --git a/tests/test_4961_url_query_prefill.py b/tests/test_4961_url_query_prefill.py index f69280e0..78cee566 100644 --- a/tests/test_4961_url_query_prefill.py +++ b/tests/test_4961_url_query_prefill.py @@ -127,6 +127,7 @@ console.log(JSON.stringify({ cleaned, promoted })); def test_root_prefill_keeps_saved_local_sidebar_only(): source = _node_prelude() + """ +evalBoot('_prefillHasDraftText'); evalBoot('_rootPrefillNeedsFreshComposer'); const result = { savedLocalWins: _rootPrefillNeedsFreshComposer(null, 'saved-local', { hasText: true }), @@ -155,6 +156,31 @@ console.log(JSON.stringify(result)); assert load_pos > prefill_guard +def test_fresh_default_workspace_bind_skips_prefill_draft_boot(): + source = _node_prelude() + """ +(async () => { + evalBoot('_prefillHasDraftText'); + evalBoot('_maybeBindFreshDefaultWorkspaceSession'); + global.S = { session: null, _profileDefaultWorkspace: 'D:/workspace' }; + global._workspacePanelMode = 'browse'; + const calls = []; + global.newSession = async () => { calls.push('newSession'); }; + const skipped = await _maybeBindFreshDefaultWorkspaceSession({ hasText: true }); + const bound = await _maybeBindFreshDefaultWorkspaceSession({ hasText: false }); + console.log(JSON.stringify({ skipped, bound, calls })); +})().catch(err => { + console.error(err); + process.exit(1); +}); +""" + payload = json.loads(_run_node(source)) + assert payload == { + "skipped": False, + "bound": True, + "calls": ["newSession"], + } + + def test_apply_prefill_updates_composer_without_autosending(): source = _node_prelude() + """ (async () => { @@ -185,6 +211,42 @@ def test_apply_prefill_updates_composer_without_autosending(): assert payload["value"] == "second pass" +def test_terminal_prefill_step_consumes_params_after_boot_state_is_ready(): + source = _node_prelude() + """ +(async () => { + evalBoot('_applyComposerPrefillOnBoot'); + evalBoot('_finalizeComposerPrefillOnBoot'); + const counts = { autoResize: 0, updateSendBtn: 0, consume: 0, send: 0 }; + const msg = { value: '' }; + global.document = { + getElementById(id) { + return id === 'msg' ? msg : null; + } + }; + global.$ = (id) => document.getElementById(id); + global.autoResize = () => { counts.autoResize++; }; + global.updateSendBtn = () => { counts.updateSendBtn++; }; + global.send = async () => { counts.send++; }; + global._consumeComposerPrefillParamsFromLocation = () => { counts.consume++; }; + await _finalizeComposerPrefillOnBoot({ hasParams: true, hasText: true, text: 'hello world', autoSend: false }); + delete global.autoResize; + await _finalizeComposerPrefillOnBoot({ hasParams: false, hasText: true, text: 'second pass', autoSend: false }); + console.log(JSON.stringify({ counts, value: msg.value })); +})().catch(err => { + console.error(err); + process.exit(1); +}); +""" + payload = json.loads(_run_node(source)) + assert payload["counts"] == { + "autoResize": 1, + "updateSendBtn": 1, + "consume": 1, + "send": 0, + } + assert payload["value"] == "second pass" + + def test_explicit_session_url_keeps_target_session_with_prefill(): source = _node_prelude() + """ global.window = { @@ -195,6 +257,7 @@ global.window = { } }; evalSession('_sessionIdFromLocation'); +evalBoot('_prefillHasDraftText'); evalBoot('_rootPrefillNeedsFreshComposer'); console.log(JSON.stringify({ urlSession: _sessionIdFromLocation(), @@ -212,6 +275,7 @@ global.window = { } }; evalSession('_sessionIdFromLocation'); +evalBoot('_prefillHasDraftText'); evalBoot('_rootPrefillNeedsFreshComposer'); console.log(JSON.stringify({ urlSession: _sessionIdFromLocation(), @@ -223,22 +287,26 @@ console.log(JSON.stringify({ prefill_pos = BOOT_JS.find( "const prefillIntent=(typeof _composerPrefillIntentFromLocation==='function')?_composerPrefillIntentFromLocation():null;" ) - consume_pos = BOOT_JS.find("_consumeComposerPrefillParamsFromLocation();", prefill_pos) - first_await_pos = BOOT_JS.find("const s=await api('/api/settings');", consume_pos) - new_pos = BOOT_JS.find("await newSession(true);", consume_pos) - load_pos = BOOT_JS.find("await loadSession(saved, {preserveActiveInput:true});", consume_pos) + first_await_pos = BOOT_JS.find("const s=await api('/api/settings');", prefill_pos) + active_profile_pos = BOOT_JS.find( + "const activeProfileState = await _resolveActiveProfileBootstrapState();", + first_await_pos, + ) + new_pos = BOOT_JS.find("await newSession(true);", active_profile_pos) + load_pos = BOOT_JS.find("await loadSession(saved, {preserveActiveInput:true});", active_profile_pos) saved_pos = BOOT_JS.find("const saved=urlSession||savedLocal;") check_pos = BOOT_JS.find("await checkInflightOnBoot(saved);", load_pos) - apply_pos = BOOT_JS.find("await _applyComposerPrefillOnBoot(prefillIntent);", check_pos) + apply_pos = BOOT_JS.find("await _finalizeComposerPrefillOnBoot(prefillIntent);", check_pos) assert saved_pos >= 0 assert prefill_pos >= 0 - assert consume_pos > prefill_pos - assert 0 <= consume_pos < first_await_pos - assert 0 <= consume_pos < new_pos - assert 0 <= consume_pos < load_pos + assert active_profile_pos > first_await_pos + assert "_consumeComposerPrefillParamsFromLocation();" not in BOOT_JS[prefill_pos:first_await_pos] + assert 0 <= active_profile_pos < new_pos + assert 0 <= active_profile_pos < load_pos assert 0 <= check_pos < apply_pos + assert BOOT_JS.find("await _maybeBindFreshDefaultWorkspaceSession(prefillIntent);", prefill_pos) > prefill_pos zero_message_pos = BOOT_JS.find( - "await renderSessionList();await _applyComposerPrefillOnBoot(prefillIntent);if(typeof startGatewaySSE==='function')startGatewaySSE();", + "await renderSessionList();await _finalizeComposerPrefillOnBoot(prefillIntent);if(typeof startGatewaySSE==='function')startGatewaySSE();", load_pos, ) assert zero_message_pos > load_pos diff --git a/tests/test_issue4877_fresh_blank_boot_workspace_bind.py b/tests/test_issue4877_fresh_blank_boot_workspace_bind.py index 7917f3ac..e15b0bee 100644 --- a/tests/test_issue4877_fresh_blank_boot_workspace_bind.py +++ b/tests/test_issue4877_fresh_blank_boot_workspace_bind.py @@ -20,6 +20,9 @@ node_test = pytest.mark.skipif(NODE is None, reason="node not on PATH") def _extract_async_function(source: str, name: str) -> str: marker = f"async function {name}(" start = source.find(marker) + if start < 0: + marker = f"function {name}(" + start = source.find(marker) assert start >= 0, f"{name}() function must exist" brace = source.find("{", source.find(")", start)) assert brace > start, f"{name}() function body must start" @@ -79,6 +82,7 @@ def _run_node(script: str) -> dict: def _helper_driver(panel_mode: str, default_workspace: str | None, *, reject: bool = False) -> dict: + helper_dep = _extract_async_function(BOOT_JS.read_text(encoding="utf-8"), "_prefillHasDraftText") helper = _extract_async_function(BOOT_JS.read_text(encoding="utf-8"), "_maybeBindFreshDefaultWorkspaceSession") default_workspace_repr = json.dumps(default_workspace) script = textwrap.dedent( @@ -94,6 +98,7 @@ def _helper_driver(panel_mode: str, default_workspace: str | None, *, reject: bo calls.push({{arg0:a,arg1:b}}); return shouldReject ? Promise.reject(new Error('bind-failed')) : Promise.resolve({{}}); }} + {helper_dep} {helper} _maybeBindFreshDefaultWorkspaceSession().then((bound)=>{{ process.stdout.write(JSON.stringify({{bound,calls}})); @@ -150,7 +155,7 @@ def test_no_saved_session_branch_restores_panel_pref_before_bind_attempt(): fresh_pref = "if(_freshPanelPref&&!_isCompactWorkspaceViewport()) _workspacePanelMode='browse';" pref_idx = segment.find(fresh_pref) assert pref_idx >= 0, "no-saved-session path must restore panel preference" - bind_call = "await _maybeBindFreshDefaultWorkspaceSession();" + bind_call = "await _maybeBindFreshDefaultWorkspaceSession(prefillIntent);" bind_idx = segment.find(bind_call) assert bind_idx >= 0, "no-saved-session path must attempt to bind after pref restore" assert pref_idx < bind_idx, "panel preference restoration must happen before bind attempt" @@ -167,7 +172,7 @@ def test_ephemeral_blank_session_branch_restores_panel_pref_before_bind_attempt( eph_pref = "if(_ephPanelPref&&!_isCompactWorkspaceViewport()) _workspacePanelMode='browse';" pref_idx = segment.find(eph_pref) assert pref_idx >= 0, "ephemeral blank-session path must restore panel preference" - bind_call = "await _maybeBindFreshDefaultWorkspaceSession();" + bind_call = "await _maybeBindFreshDefaultWorkspaceSession(prefillIntent);" bind_idx = segment.find(bind_call) assert bind_idx >= 0, "ephemeral blank-session path must attempt to bind before returning" assert pref_idx < bind_idx, "ephemeral panel preference restoration must happen before bind attempt"