Skip to content

Release — replace cross-profile empty sessions on profile switch (#5460) - #5507

Merged
nesquena-hermes merged 3 commits into
masterfrom
release/stage-5460
Jul 4, 2026
Merged

nesquena-hermes merged 3 commits into
masterfrom
release/stage-5460

Conversation

@nesquena-hermes

Copy link
Copy Markdown
Collaborator

Release — replace cross-profile empty sessions on profile switch (#5460)

Ships @ruizanthony's #5460 (gate-pass, crown-jewel profile-switch surface). Fixes the upload-404-after-profile-switch bug + makes cross-profile sidebar rows open correctly.

When you switched profiles with an empty current chat, the browser could retain the previous profile's session id, so a later attachment upload posted a stale id → "Session not found" until a hard reload. The empty chat is now replaced during the switch when it belonged to a different profile. Also: clicking a session owned by another profile now switches to its owning profile before loading (instead of 404ing).

Gate (crown-jewel — full re-gate despite gate-cert pass)

Co-authored-by: ruizanthony ruizanthony@users.noreply.github.com

nesquena-hermes and others added 3 commits July 3, 2026 20:49
…open all-profile rows under owning profile

Clean rebase of ruizanthony's #5460 (rebase-first).

Co-authored-by: ruizanthony <ruizanthony@users.noreply.github.com>
@nesquena-hermes
nesquena-hermes merged commit 98a3b07 into master Jul 4, 2026
18 checks passed
@nesquena-hermes
nesquena-hermes deleted the release/stage-5460 branch July 4, 2026 03:38
@greptile-apps

greptile-apps Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This release PR ships two related profile-switching fixes: (1) empty sessions whose profile differs from the switch target are now replaced instead of retagged, preventing stale session IDs from reaching upload endpoints; (2) clicking a cross-profile sidebar row in "all-profiles" mode now switches the active profile before loading the session, preventing 404s. The "show all profiles" toggle is also persisted to localStorage and kept sticky across profile switches (no longer reset), with a new cross-tab sync handler.

  • static/panels.js: switchToProfile converts sessionInProgress to let, adds a post-switch check that compares the current session's profile against the newly-active profile and promotes to the new-session branch on mismatch; adds a dedicated _openingExistingSidebarSession fast-path that skips creating an intermediary blank chat; adds _openProfileSwitchSessionBrowser() helper called from both success branches to surface the session list on desktop (expand sidebar) and mobile (open drawer panel).
  • static/sessions.js: consolidates three duplicated session-open call sites into _openSidebarSession, which chains an optional _ensureSidebarSessionProfile profile switch before loadSession; adds _setShowAllProfiles/_restoreShowAllProfiles helpers with localStorage persistence and a storage event listener for cross-tab sync.
  • Tests: 9 test files updated/added to pin the new static contracts for profile-mismatch promotion, sidebar-open ordering, persistence, and the refactored session-click path.

Confidence Score: 4/5

Safe to merge; the core bug fixes are logically sound and the four-guard contract in switchToProfile is preserved. No cross-profile data exposure was introduced.

The empty-session promotion logic runs after the profile-switch POST resolves, so it correctly uses the server-confirmed active profile name. The _openingExistingSidebarSession snapshot at function entry prevents any mid-switch race from corrupting the flag. The one rough edge is that _openSidebarSession discards _ensureSidebarSessionProfile's return value — if the profile switch fails, loadSession is still called with the old profile cookie and the user may see a second, confusing error on top of the switch-failure toast. That is an edge case with no data-loss risk, but it is a present gap in error propagation on the changed path.

static/sessions.js — specifically the _openSidebarSession function and its handling of a failed profile switch before loadSession.

Important Files Changed

Filename Overview
static/panels.js Core profile-switch logic updated: sessionInProgress promoted to let, post-switch profile mismatch detection added, new _openingExistingSidebarSession fast-path prevents intermediary blank sessions, _openProfileSwitchSessionBrowser helper added and called from both success branches.
static/sessions.js Three duplicated session-open call sites consolidated into _openSidebarSession; _ensureSidebarSessionProfile added for cross-profile switch before loadSession; _showAllProfiles persistence via localStorage with cross-tab storage-event sync; return value of _ensureSidebarSessionProfile discarded in _openSidebarSession.
tests/test_profile_switch_ux.py New static-analysis tests pin the empty-session promotion, session-browser open ordering, and desktop/mobile helper contract; existing tests updated for renamed function paths.
tests/test_issue1611_session_profile_filtering.py Two new tests added: cross-profile switch before loadSession and localStorage persistence of the all-profiles toggle; existing import-payload assertion broadened to match refactored call site.
tests/test_issue3603_external_session_import_gate.py Import-gate assertions migrated from per-call-site pattern checks to the new shared _openSidebarSession helper; coverage preserved for all three open paths.
tests/test_issue4662_profile_switch_skeleton_static.py Skeleton-call detection updated from literal string match to regex search to accommodate optional conditional wrapper; timeoutToast window search tightened to look forward-only from the POST call.
tests/test_issue1700_parallel_profile_switch.py sessionInProgress detection updated from const to let keyword via regex search, preserving coverage of active_stream_id and pending_user_message checks.
tests/test_firefox_sidebar_scroll_stability.py Toggle onclick assertions updated to match the new _setShowAllProfiles helper signature and explicit deferWhileInteracting:false argument.
tests/test_session_lineage_collapse.py Lineage and child-session open assertions updated from loadSession to _openSidebarSession call site.
tests/test_session_touch_actions.py Gesture-finish touch-action test updated from loadSession index search to _openSidebarSession index search; dismiss-before-open ordering assertion preserved.
CHANGELOG.md Release changelog entry added for the profile-switch upload fix and cross-profile sidebar row fix.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant User
    participant UI as renderSessionListFromCache
    participant OSS as _openSidebarSession
    participant ESSP as _ensureSidebarSessionProfile
    participant STP as switchToProfile
    participant LS as loadSession

    Note over User,LS: Cross-profile sidebar row click (all-profiles mode)
    User->>UI: click session row (other profile)
    UI->>OSS: _openSidebarSession(session)
    OSS->>OSS: _isExternalSession? → import_cli (if needed)
    OSS->>ESSP: _ensureSidebarSessionProfile(session)
    ESSP->>ESSP: "_showAllProfiles && profile differs?"
    ESSP->>ESSP: "_profileSwitchOpeningExistingSession = true"
    ESSP->>STP: switchToProfile(targetProfile)
    Note over STP: _openingExistingSidebarSession=true snapshot
    STP->>STP: "sessionInProgress=true (skip intermediary blank chat)"
    STP-->>STP: POST /api/profile/switch
    STP->>STP: "sessionInProgress&&_openingExistingSidebarSession branch"
    STP->>STP: renderSessionList() + showToast()
    STP-->>ESSP: resolved
    ESSP->>ESSP: "_profileSwitchOpeningExistingSession = false (finally)"
    ESSP-->>OSS: return profileMatchesTarget
    OSS->>LS: loadSession(session.session_id, loadOpts)
    LS-->>OSS: session loaded
    OSS->>UI: renderSessionListFromCache()

    Note over User,LS: Profile chip switch with empty cross-profile session
    User->>STP: switchToProfile(newProfile)
    STP-->>STP: POST /api/profile/switch → S.activeProfile updated
    STP->>STP: "!sessionInProgress && S.session: check session.profile vs target"
    STP->>STP: "mismatch → sessionInProgress = true"
    STP->>STP: else if(sessionInProgress) branch → newSession()
    STP->>STP: renderSessionList() + _openProfileSwitchSessionBrowser()
    STP-->>User: sidebar/drawer exposed with new profile's sessions
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant User
    participant UI as renderSessionListFromCache
    participant OSS as _openSidebarSession
    participant ESSP as _ensureSidebarSessionProfile
    participant STP as switchToProfile
    participant LS as loadSession

    Note over User,LS: Cross-profile sidebar row click (all-profiles mode)
    User->>UI: click session row (other profile)
    UI->>OSS: _openSidebarSession(session)
    OSS->>OSS: _isExternalSession? → import_cli (if needed)
    OSS->>ESSP: _ensureSidebarSessionProfile(session)
    ESSP->>ESSP: "_showAllProfiles && profile differs?"
    ESSP->>ESSP: "_profileSwitchOpeningExistingSession = true"
    ESSP->>STP: switchToProfile(targetProfile)
    Note over STP: _openingExistingSidebarSession=true snapshot
    STP->>STP: "sessionInProgress=true (skip intermediary blank chat)"
    STP-->>STP: POST /api/profile/switch
    STP->>STP: "sessionInProgress&&_openingExistingSidebarSession branch"
    STP->>STP: renderSessionList() + showToast()
    STP-->>ESSP: resolved
    ESSP->>ESSP: "_profileSwitchOpeningExistingSession = false (finally)"
    ESSP-->>OSS: return profileMatchesTarget
    OSS->>LS: loadSession(session.session_id, loadOpts)
    LS-->>OSS: session loaded
    OSS->>UI: renderSessionListFromCache()

    Note over User,LS: Profile chip switch with empty cross-profile session
    User->>STP: switchToProfile(newProfile)
    STP-->>STP: POST /api/profile/switch → S.activeProfile updated
    STP->>STP: "!sessionInProgress && S.session: check session.profile vs target"
    STP->>STP: "mismatch → sessionInProgress = true"
    STP->>STP: else if(sessionInProgress) branch → newSession()
    STP->>STP: renderSessionList() + _openProfileSwitchSessionBrowser()
    STP-->>User: sidebar/drawer exposed with new profile's sessions
Loading

Reviews (1): Last reviewed commit: "docs(changelog): #5460 replace cross-pro..." | Re-trigger Greptile

Comment thread static/sessions.js
Comment on lines +1883 to +1884
await _ensureSidebarSessionProfile(session);
await loadSession(session.session_id, loadOpts);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 _ensureSidebarSessionProfile return value discarded

_ensureSidebarSessionProfile returns false for two semantically different cases: "no switch was needed" (profiles already match) and "the switch failed." When the switch fails, loadSession is still called with the original profile cookie, so the request will be rejected by the backend. The user would receive switchToProfile's own error toast and then a second, less-descriptive error from loadSession, which could be confusing. Checking the return value — or, alternatively, re-checking _profileMatchesActiveProfile after the await — would let the function bail early on a genuine switch failure.

Comment thread static/panels.js
Comment on lines +6519 to +6532
function _openProfileSwitchSessionBrowser(){
try{
const isDesktop = (typeof _isDesktopWidth === 'function') ? _isDesktopWidth() : true;
if(isDesktop){
if(typeof expandSidebar === 'function') expandSidebar();
return;
}
const sidebar=document.querySelector('.sidebar');
if(!sidebar)return;
try{if(typeof _syncMobileSidebarPanelFromMainView==='function')_syncMobileSidebarPanelFromMainView();}catch(_){}
sidebar.classList.remove('mobile-session-page');
sidebar.classList.add('mobile-panel-drawer','mobile-open');
}catch(_){}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Mobile sidebar opened via direct CSS class manipulation

The mobile path removes mobile-session-page and adds mobile-panel-drawer and mobile-open directly on the .sidebar element, bypassing any dedicated open-sidebar function that might maintain additional state (scroll position, panel-mode tracking, animation classes). If a future refactor centralises mobile sidebar state into a helper, this call site would silently fall behind. It's worth noting that closeMobileSidebar is already used elsewhere — if a symmetric openMobileSidebar (or equivalent) exists or is added, this helper should delegate to it rather than manipulating classes directly.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant