Skip to content

feat(desktop): add configurable profile switch behavior - #91574

Open
k4-br0n wants to merge 2 commits into
NousResearch:mainfrom
k4-br0n:feat/desktop-profile-switch-behavior
Open

k4-br0n wants to merge 2 commits into
NousResearch:mainfrom
k4-br0n:feat/desktop-profile-switch-behavior

Conversation

@k4-br0n

@k4-br0n k4-br0n commented Aug 21, 2026

Copy link
Copy Markdown

What does this PR do?

Desktop profile switches currently start a fresh draft and discard the foreground session. This adds an explicit client-local choice while preserving the existing behavior as the default:

  • fresh_draft remains the default.
  • restore_last_session returns to the last remembered session for the selected profile after the target source, profile, and session list are authoritative.

The restore path reuses current Desktop primitives: gateway activation epochs, exact session-owner resolution, guarded sidebar refresh, native pane focus/open behavior, and existing profile-scoped session-tile placement buckets. It does not persist or reconstruct pane trees.

Restore intent is tied to one exact profile/source activation and native navigation identity. Newer profile selections, explicit New Chat, route/session/pane navigation, ambiguous ownership, refresh failure, and backend failure cancel stale work rather than navigating later.

The PR also scopes Ctrl+Tab session candidates to the active profile. Cycling wraps within a concrete profile. In the aggregate All Profiles view, bare-ID Ctrl+Tab and numeric session switching fail closed because rows can legitimately share IDs across owners; the native sidebar remains available there.

Related work: #77957 and #60202 explore automatic restoration. This PR keeps fresh drafts as the default and makes restoration opt-in.

Related Issue

No new issue. Related: #77957, #60202.

Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue)
  • ✨ New feature (non-breaking change that adds functionality)
  • 🔒 Security fix
  • 📝 Documentation update
  • ✅ Tests (adding or improving test coverage)
  • ♻️ Refactor (no behavior change)
  • 🎯 New skill (bundled or hub)

Changes Made

  • apps/desktop/src/store/profile-switch-behavior.ts: persist the Desktop-local behavior preference and own generation/cancellation state for one profile-switch intent.
  • apps/desktop/src/store/profile.ts: cross the existing fresh-draft barrier, serialize reversal through current gateway activation, and bind restoration to the exact requested/live/descriptor identities.
  • apps/desktop/src/app/contrib/hooks/use-profile-switch-continuity.ts: capture native pane/session navigation, refresh the authoritative target list, resolve exact ownership, and focus or open the remembered session through existing paths.
  • apps/desktop/src/app/session/hooks/use-session-list-actions.ts: separate guarded data publication from loading-barrier ownership using the existing request counter and gateway-switch barrier.
  • apps/desktop/src/store/session-switcher.ts: derive immediate, held, reverse, and numeric Ctrl+Tab candidates from the canonical concrete-profile scope, close stale switcher state when scope changes, and fail closed in All Profiles where bare IDs are not authoritative.
  • Appearance settings, search metadata, and locale files expose the two profile-switch choices.
  • Behavioral tests cover stale generations, reverse selection, source/descriptor identity, native pane cancellation, guarded refresh/loading races, exact-owner restoration, and profile-bounded Ctrl+Tab cycling.

How to Test

  1. From apps/desktop, run the focused Vitest files for profile switch behavior, continuity, guarded session-list refresh, profile source selection, and session switching.
  2. In Desktop Appearance settings, choose Last session for profile switching.
  3. Open sessions in two profiles. Switch A → B → A and verify A's remembered session returns only after B/A activation and refresh settle; explicit navigation or native Terminal/Files/Preview focus during the switch must win.
  4. With multiple sessions in each profile, use Ctrl+Tab and Ctrl+Shift+Tab at both ends of the active profile's list. Cycling must wrap within that profile. In All Profiles, Ctrl+Tab and numeric session switching must no-op while the native sidebar remains available.
  5. Repeat with a shared-primary source and a profile with a dedicated remote override; restoration must use the exact owning route and fail closed when ownership is unknown.

Checklist

Code

  • I've read the Contributing Guide
  • My commit messages follow Conventional Commits (fix(scope):, feat(scope):, etc.)
  • I searched for existing PRs to make sure this isn't a duplicate
  • My PR contains only changes related to this fix/feature (no unrelated commits)
  • I've run pytest tests/ -q and all tests pass — N/A: this PR changes only Desktop TypeScript
  • I've added tests for my changes (required for bug fixes, strongly encouraged for features)
  • I've tested on my platform — the rebased current-main branch has not rerun Vitest, typecheck, build, or packaging; CI/runtime status is intentionally not claimed

Documentation & Housekeeping

  • I've updated relevant documentation (README, docs/, docstrings) — N/A; the preference is described in the Appearance UI
  • I've updated cli-config.yaml.example if I added/changed config keys — N/A; this is client-local persisted UI state
  • I've updated CONTRIBUTING.md or AGENTS.md if I changed architecture or workflows — N/A; no repository architecture/workflow contract changed
  • I've considered cross-platform impact (Windows, macOS) per the compatibility guide — renderer behavior is platform-neutral
  • I've updated tool descriptions/schemas if I changed tool behavior — N/A; no model tools changed

Screenshots / Logs

No screenshots: the change is behavioral session/profile continuity rather than a new visual surface.

AI assistance disclosure

Prepared with AI-assisted implementation and independent read-only reviews. The final two-commit branch was replayed onto current main, audited against intervening changes, and checked for scope/privacy. Vitest, typecheck, build, packaging, and full test suites were intentionally not rerun; their status remains unknown.

@alt-glitch alt-glitch added type/feature New feature or request comp/desktop Electron desktop app (apps/desktop/*) area/profiles Multi-profile isolation, HERMES_HOME scoping area/config Config system, migrations, profiles P3 Low — cosmetic, nice to have sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state labels Aug 21, 2026
@Enough1122

Copy link
Copy Markdown
Contributor

AI code review — automated review for reference; please use your judgment.

Reviewed the full diff. This is careful work: sequence-numbered intents with identity guards, an explicit committed-refresh barrier before validating the remembered session, synchronous anchor capture ahead of the gateway swap, and the serialization rewrite in profile.ts genuinely fixes a latent bug (the old code had concurrent callers both await the same gatewaySwitch promise and then both fall through to dial — the chained previousSwitch version closes that double-dial race). Test quality is well above bar (deferred-gate serialization ordering, re-click-latest-intent, cross-backend scoping). Remaining points:

  • apps/desktop/src/app/contrib/hooks/use-desktop-integrations.ts:~150-215 — the post-await identity recheck omits pendingConnectionId. After the refresh barrier resolves, the code re-validates sequence/connectionId/profile but not whether a connection became pending during the await, so a restore can still fire while a connect/deeplink flow is mid-flight (the pre-await check alone gated it). Re-read $pendingConnectionId.get() alongside latest and bail symmetrically.

  • use-desktop-integrations.ts:~168-176 — the 3-attempt retry loop has no spacing. Each false return immediately re-invokes refreshSessions(); since a false means "superseded," a tight triple-call just burns three requests in one tick. A microtask/short delay (or exiting early when the intent was superseded — detectable via sequence) would be cheaper and easier to reason about.

  • Untested lifecycle branches. The suite covers happy paths and cancellation-by-pending-connection, but not: behavior toggled to fresh_draft while an intent is pending (effect should clear it), retry exhaustion (3x stale -> intent cleared, no openSession), and refreshSessions throwing (catch path preserves the anchor). All are cheap renders away and pin the fail-safe direction.

  • profile.ts:~330-336 — transient swap-target mislabel during chained switches. $gatewaySwapTarget is set to the newest target before the chain reaches it, so while beta is still activating, the UI can display gamma's target. Probably acceptable ("latest wins"), but worth a comment so nobody treats it as a bug later.

  • Anchor-capture staleness window: registerProfileSwitchAnchorCapture swaps the closure whenever sessions/visibleStoredSessionId change; a click landing between a state update and the effect re-run captures with the previous snapshot. The consequence is benign (anchor one tile behind) but worth a one-line comment documenting that the capture is best-effort.

  • Nit: appearance-settings.tsx:~726 casts the SegmentedControl id with as ProfileSwitchBehavior; deriving options from a const tuple (satisfies readonly {id: ProfileSwitchBehavior}[]) would keep the control type-safe if ids ever drift.

The i18n sweep (5 locales + types) and settings-search wiring are complete, and the refreshSessionsCommitted boolean contract is cleanly layered under the existing refreshSessions wrapper.

@k4-br0n
k4-br0n force-pushed the feat/desktop-profile-switch-behavior branch 2 times, most recently from 57e4e97 to a863932 Compare August 26, 2026 17:14
@k4-br0n
k4-br0n force-pushed the feat/desktop-profile-switch-behavior branch from a863932 to 748a43c Compare August 27, 2026 14:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/config Config system, migrations, profiles area/profiles Multi-profile isolation, HERMES_HOME scoping comp/desktop Electron desktop app (apps/desktop/*) P3 Low — cosmetic, nice to have sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants