fix: switching profiles mid-conversation starts a new session instead of cross-tagging

A session with messages belongs to the profile it was created under. Switching
profiles while a conversation is in progress should not retag that session or
update its workspace/model in place — that would corrupt the session's context.

New behavior:
- Session has NO messages (empty): profile switch updates it in place (model,
  workspace). Works exactly as before — nothing was started yet.
- Session HAS messages (in progress): profile switch calls newSession() to
  start a fresh session tagged to the new profile. The old session is left
  untouched. Toast: 'Switched to profile: X — new conversation started'.
- Agent busy: blocked as before, no change.

Also: S._profileDefaultWorkspace is now consumed (set to null) inside
newSession() after the first use, so it doesn't keep forcing the same
workspace on every subsequent new session after a switch.
This commit is contained in:
Nathan Esquenazi
2026-04-03 20:27:50 +00:00
parent 4eae6c98f9
commit da43a6a09a
2 changed files with 38 additions and 22 deletions

View File

@@ -660,35 +660,40 @@ document.addEventListener('click', e => {
async function switchToProfile(name) { async function switchToProfile(name) {
if (S.busy) { showToast('Cannot switch profiles while agent is running'); return; } if (S.busy) { showToast('Cannot switch profiles while agent is running'); return; }
// Determine whether the current session has any messages.
// A session with messages is "in progress" and belongs to the current profile —
// we must not retag it. We'll start a fresh session for the new profile instead.
const sessionInProgress = S.session && S.messages && S.messages.length > 0;
try { try {
const data = await api('/api/profile/switch', { method: 'POST', body: JSON.stringify({ name }) }); const data = await api('/api/profile/switch', { method: 'POST', body: JSON.stringify({ name }) });
S.activeProfile = data.active || name; S.activeProfile = data.active || name;
// Clear stale model pref so profile default applies
// ── Model ──────────────────────────────────────────────────────────────
localStorage.removeItem('hermes-webui-model'); localStorage.removeItem('hermes-webui-model');
// Refresh model dropdown (profile may have different provider/models)
_skillsData = null; _skillsData = null;
await populateModelDropdown(); await populateModelDropdown();
// Apply profile's default model using the smart resolver (handles id mismatches
// like 'claude-sonnet-4-6' vs 'anthropic/claude-sonnet-4.6' in the dropdown)
if (data.default_model) { if (data.default_model) {
const sel = $('modelSelect'); const sel = $('modelSelect');
const resolved = _applyModelToDropdown(data.default_model, sel); const resolved = _applyModelToDropdown(data.default_model, sel);
const modelToUse = resolved || data.default_model; const modelToUse = resolved || data.default_model;
// Also update the current session's model so syncTopbar() doesn't fight us S._pendingProfileModel = modelToUse;
if (S.session) { // Only patch the in-memory session model if we're NOT about to replace the session
if (S.session && !sessionInProgress) {
S.session.model = modelToUse; S.session.model = modelToUse;
} }
// Store as pending so syncTopbar skips its model override on the next call
S._pendingProfileModel = modelToUse;
} }
// Refresh workspace list (now profile-local)
// ── Workspace ──────────────────────────────────────────────────────────
_workspaceList = null; _workspaceList = null;
await loadWorkspaceList(); await loadWorkspaceList();
// Apply the profile's default workspace to the current session
if (data.default_workspace) { if (data.default_workspace) {
if (S.session) { // Always store the profile default for new sessions
// Update existing session's workspace to the profile default S._profileDefaultWorkspace = data.default_workspace;
if (S.session && !sessionInProgress) {
// Empty session (no messages yet) — safe to update it in place
try { try {
await api('/api/session/update', { method: 'POST', body: JSON.stringify({ await api('/api/session/update', { method: 'POST', body: JSON.stringify({
session_id: S.session.session_id, session_id: S.session.session_id,
@@ -698,21 +703,31 @@ async function switchToProfile(name) {
S.session.workspace = data.default_workspace; S.session.workspace = data.default_workspace;
} catch (_) {} } catch (_) {}
} }
// Store as the profile default so the next new session picks it up
S._profileDefaultWorkspace = data.default_workspace;
} }
// Reset profile filter and refresh session list // ── Session ────────────────────────────────────────────────────────────
_showAllProfiles = false; _showAllProfiles = false;
if (sessionInProgress) {
// The current session has messages and belongs to the previous profile.
// Start a new session for the new profile so nothing gets cross-tagged.
await newSession(false);
await renderSessionList();
showToast('Switched to profile: ' + name + ' — new conversation started');
} else {
// No messages yet — just refresh the list and topbar in place
await renderSessionList(); await renderSessionList();
syncTopbar(); syncTopbar();
// Refresh visible sidebar panels showToast('Switched to profile: ' + name);
}
// ── Sidebar panels ─────────────────────────────────────────────────────
if (_currentPanel === 'skills') await loadSkills(); if (_currentPanel === 'skills') await loadSkills();
if (_currentPanel === 'memory') await loadMemory(); if (_currentPanel === 'memory') await loadMemory();
if (_currentPanel === 'tasks') await loadCrons(); if (_currentPanel === 'tasks') await loadCrons();
if (_currentPanel === 'profiles') await loadProfilesPanel(); if (_currentPanel === 'profiles') await loadProfilesPanel();
if (_currentPanel === 'workspaces') await loadWorkspacesPanel(); if (_currentPanel === 'workspaces') await loadWorkspacesPanel();
showToast('Switched to profile: ' + name);
} catch (e) { showToast('Switch failed: ' + e.message); } } catch (e) { showToast('Switch failed: ' + e.message); }
} }

View File

@@ -13,9 +13,10 @@ async function newSession(flash){
MSG_QUEUE.length=0;updateQueueBadge(); MSG_QUEUE.length=0;updateQueueBadge();
S.toolCalls=[]; S.toolCalls=[];
clearLiveToolCards(); clearLiveToolCards();
// Use profile default workspace for new sessions after a profile switch, // Use profile default workspace for new sessions after a profile switch (one-shot),
// otherwise inherit from the current session (or let server pick the default) // otherwise inherit from the current session (or let server pick the default)
const inheritWs=S._profileDefaultWorkspace||( S.session?S.session.workspace:null); const inheritWs=S._profileDefaultWorkspace||(S.session?S.session.workspace:null);
S._profileDefaultWorkspace=null; // consume — only applies to the first new session after switch
const data=await api('/api/session/new',{method:'POST',body:JSON.stringify({model:$('modelSelect').value,workspace:inheritWs})}); const data=await api('/api/session/new',{method:'POST',body:JSON.stringify({model:$('modelSelect').value,workspace:inheritWs})});
S.session=data.session;S.messages=data.session.messages||[]; S.session=data.session;S.messages=data.session.messages||[];
if(flash)S.session._flash=true; if(flash)S.session._flash=true;