diff --git a/apps/desktop/electron/connection-config.test.ts b/apps/desktop/electron/connection-config.test.ts index 4361678cb8fe..e8a4ca8e3803 100644 --- a/apps/desktop/electron/connection-config.test.ts +++ b/apps/desktop/electron/connection-config.test.ts @@ -35,6 +35,7 @@ import { profileHasRemoteConnection, profileRemoteOverride, profileSshOverride, + profileUsesPrimaryBackend, resolveAuthMode, resolveTestWsUrl, RT_COOKIE_VARIANTS, @@ -187,6 +188,44 @@ test('saved SSH drafts are inactive and explicit overrides take precedence', () assert.equal(profileHasRemoteConnection(config, 'coder'), true) }) +// --- profileUsesPrimaryBackend --- + +test('profileUsesPrimaryBackend keeps primary and empty scopes on the window backend', () => { + assert.equal(profileUsesPrimaryBackend('default', { primaryProfile: 'default' }), true) + assert.equal(profileUsesPrimaryBackend(' coder ', { primaryProfile: 'coder' }), true) + assert.equal(profileUsesPrimaryBackend('', { primaryProfile: 'default' }), true) +}) + +test('profileUsesPrimaryBackend shares one app-global remote across profiles', () => { + assert.equal( + profileUsesPrimaryBackend('coder', { + primaryProfile: 'default', + globalRemote: true, + profileRemoteOverride: false + }), + true + ) +}) + +test('profileUsesPrimaryBackend preserves profile overrides and local profile backends', () => { + assert.equal( + profileUsesPrimaryBackend('coder', { + primaryProfile: 'default', + globalRemote: true, + profileRemoteOverride: true + }), + false + ) + assert.equal( + profileUsesPrimaryBackend('coder', { + primaryProfile: 'default', + globalRemote: false, + profileRemoteOverride: false + }), + false + ) +}) + // --- pathWithGlobalRemoteProfile --- test('pathWithGlobalRemoteProfile appends profile in global remote mode', () => { diff --git a/apps/desktop/electron/connection-config.ts b/apps/desktop/electron/connection-config.ts index f7b1ac2e0d42..f5cd7763d7d5 100644 --- a/apps/desktop/electron/connection-config.ts +++ b/apps/desktop/electron/connection-config.ts @@ -350,6 +350,24 @@ function profileRemoteOverride(config, profile) { return { url, authMode: normAuthMode(entry.authMode), token: entry.token } } +/** + * Decide whether a profile should reuse the primary backend connection. + * + * The primary profile always owns the window backend. In app-global remote + * mode, that same backend serves every profile through request-level profile + * scoping, unless a profile has its own remote connection override. + */ +function profileUsesPrimaryBackend(profile, opts: any = {}) { + const scopedProfile = connectionScopeKey(profile) + const primaryProfile = connectionScopeKey(opts.primaryProfile) || 'default' + + if (!scopedProfile || scopedProfile === primaryProfile) { + return true + } + + return Boolean(opts.globalRemote) && !opts.profileRemoteOverride +} + /** * In global-remote mode one backend serves every Desktop profile, so REST calls * that are scoped by renderer-side `request.profile` must carry that scope as a @@ -505,6 +523,7 @@ export { profileHasRemoteConnection, profileRemoteOverride, profileSshOverride, + profileUsesPrimaryBackend, resolveAuthMode, resolveTestWsUrl, RT_COOKIE_VARIANTS, diff --git a/apps/desktop/electron/main.ts b/apps/desktop/electron/main.ts index d4cb1ccb5f5d..ed0296cf362b 100644 --- a/apps/desktop/electron/main.ts +++ b/apps/desktop/electron/main.ts @@ -63,6 +63,7 @@ import { profileHasRemoteConnection, profileRemoteOverride, profileSshOverride, + profileUsesPrimaryBackend, resolveAuthMode, resolveTestWsUrl, savedProfileSsh, @@ -7727,17 +7728,35 @@ function primaryProfileKey() { return readActiveDesktopProfile() || 'default' } -// Resolve a backend connection for the given profile. Routes the primary -// profile to startHermes() (the window backend: boot UI, bootstrap, remote -// mode), and any OTHER profile to a lazily-spawned pool backend. An empty / -// unknown profile resolves to the primary, so all legacy callers are unchanged. +// Resolve a backend connection for the given profile. The primary profile uses +// startHermes() (the window backend: boot UI, bootstrap, remote mode). Profiles +// inheriting an app-global remote share that same connection because the remote +// backend scopes their requests by profile. Other profiles use lazily-spawned +// pool backends. An empty / unknown profile resolves to the primary. async function ensureBackend(profile) { - const key = profile && String(profile).trim() ? String(profile).trim() : primaryProfileKey() + const primaryProfile = primaryProfileKey() + const key = profile && String(profile).trim() ? String(profile).trim() : primaryProfile - if (key === primaryProfileKey()) { + if (key === primaryProfile) { return startHermes() } + if ( + globalRemoteActive() && + profileUsesPrimaryBackend(key, { + primaryProfile, + globalRemote: true, + profileRemoteOverride: profileHasRemoteOverride(key) + }) + ) { + const connection = await startHermes() + + // Non-primary global-remote profiles still need their scope on the + // descriptor so renderer-side WebSocket, filesystem, and cache routing use + // the selected profile while sharing the one underlying backend. + return { ...connection, profile: key } + } + const existing = backendPool.get(key) if (existing) {