fix(workspace): _profileDefaultWorkspace persists after newSession() (#823)
Closes #823. Separates two conflated semantics in S._profileDefaultWorkspace: - Persistent blank-page default (set by boot/settings, never nulled) - Profile-switch one-shot (now S._profileSwitchWorkspace, consumed by newSession()) newSession() priority: switchWs → current session → _profileDefaultWorkspace. switchToWorkspace() clears _profileSwitchWorkspace on explicit switch. 9 new tests. 1777/1777 suite. Browser-verified.
This commit is contained in:
@@ -1,5 +1,10 @@
|
|||||||
# Hermes Web UI -- Changelog
|
# Hermes Web UI -- Changelog
|
||||||
|
|
||||||
|
## [v0.50.139] — 2026-04-22
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- **Default workspace persists after session delete** — the blank new-chat page now shows the configured default workspace even after creating and deleting sessions. Root cause: `newSession()` consumed `S._profileDefaultWorkspace` for a one-shot profile-switch semantic, leaving it null on all subsequent returns to blank state. Fix: introduced `S._profileSwitchWorkspace` as a dedicated one-shot flag for profile switches; `S._profileDefaultWorkspace` is now persistent from boot throughout the session lifecycle. Workspace chip, `promptNewFile`, `promptNewFolder`, and `switchToWorkspace` all continue to work correctly. Closes #823. (#824)
|
||||||
|
|
||||||
## [v0.50.138] — 2026-04-22
|
## [v0.50.138] — 2026-04-22
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
@@ -809,6 +809,9 @@ async function switchToWorkspace(path,name){
|
|||||||
session_id:S.session.session_id, workspace:path, model:S.session.model
|
session_id:S.session.session_id, workspace:path, model:S.session.model
|
||||||
})});
|
})});
|
||||||
S.session.workspace=path;
|
S.session.workspace=path;
|
||||||
|
// Explicit workspace switch = user overriding any pending profile-switch default.
|
||||||
|
// Clear the one-shot flag so a subsequent newSession() inherits this choice instead.
|
||||||
|
S._profileSwitchWorkspace=null;
|
||||||
syncTopbar();
|
syncTopbar();
|
||||||
await loadDir('.');
|
await loadDir('.');
|
||||||
showToast(t('workspace_switched_to',name||getWorkspaceFriendlyName(path)));
|
showToast(t('workspace_switched_to',name||getWorkspaceFriendlyName(path)));
|
||||||
@@ -953,8 +956,12 @@ async function switchToProfile(name) {
|
|||||||
_workspaceList = null;
|
_workspaceList = null;
|
||||||
await loadWorkspaceList();
|
await loadWorkspaceList();
|
||||||
if (data.default_workspace) {
|
if (data.default_workspace) {
|
||||||
// Always store the profile default for new sessions
|
// Always store the persistent profile default — used for blank-page display
|
||||||
|
// and workspace auto-bind throughout the session lifecycle (#804, #823).
|
||||||
S._profileDefaultWorkspace = data.default_workspace;
|
S._profileDefaultWorkspace = data.default_workspace;
|
||||||
|
// Also set the one-shot flag consumed by newSession() so the first new
|
||||||
|
// session after a profile switch inherits this workspace (#424).
|
||||||
|
S._profileSwitchWorkspace = data.default_workspace;
|
||||||
|
|
||||||
if (S.session && !sessionInProgress) {
|
if (S.session && !sessionInProgress) {
|
||||||
// Empty session (no messages yet) — safe to update it in place
|
// Empty session (no messages yet) — safe to update it in place
|
||||||
|
|||||||
@@ -14,10 +14,13 @@ async function newSession(flash){
|
|||||||
updateQueueBadge();
|
updateQueueBadge();
|
||||||
S.toolCalls=[];
|
S.toolCalls=[];
|
||||||
clearLiveToolCards();
|
clearLiveToolCards();
|
||||||
// Use profile default workspace for new sessions after a profile switch (one-shot),
|
// One-shot profile-switch workspace: applied to the first new session after a profile
|
||||||
// otherwise inherit from the current session (or let server pick the default)
|
// switch, then cleared. Use a dedicated flag so S._profileDefaultWorkspace (the
|
||||||
const inheritWs=S._profileDefaultWorkspace||(S.session?S.session.workspace:null);
|
// persistent boot/settings default) is not consumed and remains available for the
|
||||||
S._profileDefaultWorkspace=null; // consume — only applies to the first new session after switch
|
// blank-page display on all subsequent returns to the empty state (#823).
|
||||||
|
const switchWs=S._profileSwitchWorkspace;
|
||||||
|
S._profileSwitchWorkspace=null;
|
||||||
|
const inheritWs=switchWs||(S.session?S.session.workspace:null)||(S._profileDefaultWorkspace||null);
|
||||||
const data=await api('/api/session/new',{method:'POST',body:JSON.stringify({model:$('modelSelect').value,workspace:inheritWs,profile:S.activeProfile||'default'})});
|
const data=await api('/api/session/new',{method:'POST',body:JSON.stringify({model:$('modelSelect').value,workspace:inheritWs,profile:S.activeProfile||'default'})});
|
||||||
S.session=data.session;S.messages=data.session.messages||[];
|
S.session=data.session;S.messages=data.session.messages||[];
|
||||||
S.lastUsage={...(data.session.last_usage||{})};
|
S.lastUsage={...(data.session.last_usage||{})};
|
||||||
|
|||||||
134
tests/test_profile_default_workspace_823.py
Normal file
134
tests/test_profile_default_workspace_823.py
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
"""Tests for #823 — _profileDefaultWorkspace persists after newSession() (#804 follow-up)
|
||||||
|
|
||||||
|
Root cause: newSession() consumed S._profileDefaultWorkspace for the one-shot
|
||||||
|
profile-switch semantic (setting it to null after the first new session). This
|
||||||
|
caused the blank-page default workspace display to regress after any session
|
||||||
|
was created and then deleted.
|
||||||
|
|
||||||
|
Fix: introduce S._profileSwitchWorkspace as the dedicated one-shot flag for
|
||||||
|
profile-switch semantics; S._profileDefaultWorkspace is now persistent.
|
||||||
|
"""
|
||||||
|
import pathlib
|
||||||
|
import re
|
||||||
|
|
||||||
|
REPO = pathlib.Path(__file__).parent.parent
|
||||||
|
|
||||||
|
|
||||||
|
def read(rel):
|
||||||
|
return (REPO / rel).read_text(encoding='utf-8')
|
||||||
|
|
||||||
|
|
||||||
|
class TestProfileDefaultWorkspacePersistence:
|
||||||
|
"""_profileDefaultWorkspace must NOT be nulled by newSession()."""
|
||||||
|
|
||||||
|
def test_new_session_does_not_null_profile_default_workspace(self):
|
||||||
|
src = read('static/sessions.js')
|
||||||
|
m = re.search(r'async function newSession\(.*?\n\}', src, re.DOTALL)
|
||||||
|
assert m, "newSession not found"
|
||||||
|
fn = m.group(0)
|
||||||
|
# The old consume pattern must be gone
|
||||||
|
assert '_profileDefaultWorkspace=null' not in fn and \
|
||||||
|
'_profileDefaultWorkspace = null' not in fn, (
|
||||||
|
"newSession must NOT null S._profileDefaultWorkspace — it is the persistent "
|
||||||
|
"boot/settings default used for blank-page display after session delete (#823)"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_new_session_uses_dedicated_switch_workspace_flag(self):
|
||||||
|
src = read('static/sessions.js')
|
||||||
|
m = re.search(r'async function newSession\(.*?\n\}', src, re.DOTALL)
|
||||||
|
assert m
|
||||||
|
fn = m.group(0)
|
||||||
|
assert '_profileSwitchWorkspace' in fn, (
|
||||||
|
"newSession must read S._profileSwitchWorkspace for the one-shot "
|
||||||
|
"profile-switch inherit, not S._profileDefaultWorkspace"
|
||||||
|
)
|
||||||
|
# It should null the switch flag, not the default
|
||||||
|
assert '_profileSwitchWorkspace=null' in fn or '_profileSwitchWorkspace = null' in fn, (
|
||||||
|
"newSession must null S._profileSwitchWorkspace after consuming it"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_new_session_still_inherits_default_workspace(self):
|
||||||
|
"""newSession must still pass a workspace to /api/session/new —
|
||||||
|
now via the _profileSwitchWorkspace → current session → _profileDefaultWorkspace chain."""
|
||||||
|
src = read('static/sessions.js')
|
||||||
|
m = re.search(r'async function newSession\(.*?\n\}', src, re.DOTALL)
|
||||||
|
assert m
|
||||||
|
fn = m.group(0)
|
||||||
|
# inheritWs must be computed and passed to /api/session/new
|
||||||
|
assert 'inheritWs' in fn or 'inherit' in fn.lower(), (
|
||||||
|
"newSession must compute an inheritWs from switch/current/default workspace"
|
||||||
|
)
|
||||||
|
assert '_profileDefaultWorkspace' in fn, (
|
||||||
|
"newSession must fall through to S._profileDefaultWorkspace as last resort"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestProfileSwitchWorkspaceSetter:
|
||||||
|
"""panels.js must set _profileSwitchWorkspace on profile switch."""
|
||||||
|
|
||||||
|
def test_panels_sets_profile_switch_workspace(self):
|
||||||
|
src = read('static/panels.js')
|
||||||
|
# Find the profile-switch workspace block
|
||||||
|
assert 'S._profileSwitchWorkspace' in src, (
|
||||||
|
"panels.js must set S._profileSwitchWorkspace during profile switch "
|
||||||
|
"so newSession() can apply it to the first new session"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_panels_still_sets_profile_default_workspace(self):
|
||||||
|
src = read('static/panels.js')
|
||||||
|
assert 'S._profileDefaultWorkspace = data.default_workspace' in src, (
|
||||||
|
"panels.js must still set S._profileDefaultWorkspace (persistent default) "
|
||||||
|
"alongside S._profileSwitchWorkspace"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_both_set_together_in_same_block(self):
|
||||||
|
src = read('static/panels.js')
|
||||||
|
default_pos = src.find('S._profileDefaultWorkspace = data.default_workspace')
|
||||||
|
switch_pos = src.find('S._profileSwitchWorkspace = data.default_workspace')
|
||||||
|
assert default_pos != -1, "S._profileDefaultWorkspace setter not found"
|
||||||
|
assert switch_pos != -1, "S._profileSwitchWorkspace setter not found"
|
||||||
|
# Both must be set within 200 chars of each other (same block)
|
||||||
|
assert abs(default_pos - switch_pos) < 300, (
|
||||||
|
"_profileDefaultWorkspace and _profileSwitchWorkspace must be set "
|
||||||
|
"together in the same profile-switch workspace block"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_switch_to_workspace_clears_profile_switch_workspace(self):
|
||||||
|
"""Opus Q4: when the user manually changes workspace, the pending one-shot
|
||||||
|
switch flag should be cleared so a subsequent newSession() inherits the
|
||||||
|
user's explicit choice rather than the stale profile-switch default."""
|
||||||
|
src = read('static/panels.js')
|
||||||
|
m = re.search(r'async function switchToWorkspace\(.*?\n\}', src, re.DOTALL)
|
||||||
|
assert m, "switchToWorkspace not found"
|
||||||
|
fn = m.group(0)
|
||||||
|
assert '_profileSwitchWorkspace=null' in fn or '_profileSwitchWorkspace = null' in fn, (
|
||||||
|
"switchToWorkspace must null S._profileSwitchWorkspace after a manual switch "
|
||||||
|
"so the next newSession() inherits the user's explicit workspace choice"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestBlankPageAfterSessionDelete:
|
||||||
|
"""After all sessions are deleted, blank page must still show default workspace."""
|
||||||
|
|
||||||
|
def test_sync_workspace_displays_reads_profile_default(self):
|
||||||
|
"""syncWorkspaceDisplays relies on S._profileDefaultWorkspace which must
|
||||||
|
still be set after a session is created and deleted."""
|
||||||
|
src = read('static/panels.js')
|
||||||
|
m = re.search(r'function syncWorkspaceDisplays\(\)\{.*?\n\}', src, re.DOTALL)
|
||||||
|
assert m, "syncWorkspaceDisplays not found"
|
||||||
|
fn = m.group(0)
|
||||||
|
assert '_profileDefaultWorkspace' in fn, (
|
||||||
|
"syncWorkspaceDisplays must read S._profileDefaultWorkspace as fallback"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_prompt_new_file_reads_profile_default(self):
|
||||||
|
"""promptNewFile on blank page reads _profileDefaultWorkspace which must
|
||||||
|
be non-null even after a newSession() + deleteSession() cycle."""
|
||||||
|
src = read('static/ui.js')
|
||||||
|
m = re.search(r'async function promptNewFile\(\)\{.*?\n\}', src, re.DOTALL)
|
||||||
|
assert m, "promptNewFile not found"
|
||||||
|
fn = m.group(0)
|
||||||
|
assert '_profileDefaultWorkspace' in fn, (
|
||||||
|
"promptNewFile must read S._profileDefaultWorkspace (must persist after newSession)"
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user