diff --git a/apps/desktop/src/app/gateway/hooks/use-gateway-boot.ts b/apps/desktop/src/app/gateway/hooks/use-gateway-boot.ts index 10d43d99e8..ecaa77c00c 100644 --- a/apps/desktop/src/app/gateway/hooks/use-gateway-boot.ts +++ b/apps/desktop/src/app/gateway/hooks/use-gateway-boot.ts @@ -18,6 +18,7 @@ import { } from '@/store/boot' import { $gateway, + activeGatewayConnectionId, closeLegacySecondaryGateways, closeSecondaryGateways, configureGatewayRegistry, @@ -31,6 +32,7 @@ import { reportPrimaryGatewayState, setPrimaryGateway, setPrimaryGatewayConnection, + setPrimaryGatewayConnectionId, touchSecondaryGateways } from '@/store/gateway' import { registerGatewayReconnect } from '@/store/gateway-reconnect' @@ -185,6 +187,7 @@ export function useGatewayBoot({ } : null ) + setPrimaryGatewayConnectionId(next?.connectionId) } if (!desktop) { @@ -737,9 +740,17 @@ export function useGatewayBoot({ const sourceProfile = normalizeProfileKey($activeGatewayProfile.get()) - const offEvent = gateway.onEvent(event => - callbacksRef.current.handleGatewayEvent({ ...event, profile: sourceProfile }) - ) + const offEvent = gateway.onEvent(event => { + const connectionId = activeGatewayConnectionId() + const scopedEvent = { + ...event, + profile: sourceProfile, + ...(connectionId ? { connectionId } : {}) + } + + recordSessionEventScope(scopedEvent) + callbacksRef.current.handleGatewayEvent(scopedEvent) + }) // Wake signals: power resume (macOS/Windows), network coming back, and the // window regaining focus/visibility. Each nudges an immediate reconnect. diff --git a/apps/desktop/src/store/gateway-connection-scope.test.ts b/apps/desktop/src/store/gateway-connection-scope.test.ts index bd86ec20b1..ead38b7c98 100644 --- a/apps/desktop/src/store/gateway-connection-scope.test.ts +++ b/apps/desktop/src/store/gateway-connection-scope.test.ts @@ -40,14 +40,17 @@ vi.mock('@/store/session', () => ({ vi.mock('@/store/notify-baseline', () => ({ markNativeNotifyBaseline: vi.fn() })) const { + activeGatewayConnectionId, closeLegacySecondaryGateways, closeSecondaryGateways, configureGatewayRegistry, ensureGatewayForAgent, openGatewayForAgent, pruneSecondaryGateways, - setPrimaryGateway + setPrimaryGateway, + setPrimaryGatewayConnectionId } = await import('./gateway') +const { setApiRequestConnection } = await import('@/hermes') function installDesktop(): void { ;(window as unknown as { hermesDesktop: unknown }).hermesDesktop = { @@ -85,6 +88,25 @@ afterEach(() => { delete (window as unknown as { hermesDesktop?: unknown }).hermesDesktop }) +describe('primary gateway registry scope', () => { + it('publishes a registered primary connection id for ambient API/WebSocket helpers', () => { + setPrimaryGateway({ connectionState: 'open' } as never, 'default') + setPrimaryGatewayConnectionId(' homelab-ssh ') + + expect(activeGatewayConnectionId()).toBe('homelab-ssh') + expect(setApiRequestConnection).toHaveBeenLastCalledWith('homelab-ssh') + }) + + it('clears primary connection scope when the primary becomes legacy/local again', () => { + setPrimaryGateway({ connectionState: 'open' } as never, 'default') + setPrimaryGatewayConnectionId('homelab-ssh') + setPrimaryGateway({ connectionState: 'open' } as never, 'default') + + expect(activeGatewayConnectionId()).toBeNull() + expect(setApiRequestConnection).toHaveBeenLastCalledWith(null) + }) +}) + describe('pruneSecondaryGateways with registry-scoped entries', () => { it('keeps the previous source socket open when Sessions switches backends', async () => { await ensureGatewayForAgent('work', 'default') diff --git a/apps/desktop/src/store/gateway.ts b/apps/desktop/src/store/gateway.ts index 854106b11f..d646515b7b 100644 --- a/apps/desktop/src/store/gateway.ts +++ b/apps/desktop/src/store/gateway.ts @@ -226,11 +226,23 @@ export function setPrimaryGateway(gateway: HermesGateway | null, profile = 'defa g.primaryGateway = gateway g.primaryProfile = next + + if (g.activeKey === g.primaryProfile) { + setApiRequestConnection(g.primaryConnectionId) + } +} + +export function setPrimaryGatewayConnectionId(connectionId: null | string | undefined): void { + g.primaryConnectionId = (connectionId ?? '').trim() || null + + if (g.activeKey === g.primaryProfile) { + setApiRequestConnection(g.primaryConnectionId) + } } /** Publish the registry source owned by the window primary socket. */ export function setPrimaryGatewayConnection(connection: Pick | null): void { - g.primaryConnectionId = connection?.connectionId?.trim() || null + setPrimaryGatewayConnectionId(connection?.connectionId) } function isPrimaryRegistryRoute(connectionId: null | string, profile: string): boolean { @@ -276,7 +288,7 @@ export function activeGateway(): HermesGateway | null { */ export function activeGatewayConnectionId(): null | string { if (g.activeKey === g.primaryProfile) { - return null + return g.primaryConnectionId } return g.secondaries.get(g.activeKey)?.connectionId ?? null