From 2afd17a9d722036272ea98437f23f81ccc2eec63 Mon Sep 17 00:00:00 2001 From: liuhao1024 Date: Sat, 29 Aug 2026 02:30:31 +0800 Subject: [PATCH] fix(desktop): resolve v2 registry SSH terminals from the bootstrap pool key The terminal pane's registry-scoped lookup keys sshConnections with backendScopeKey(connectionId, profile), but the pool's single writer publishes every tunnel under its per-profile bootstrap key with the registry connection stamped on the entry. The lookup can never hit, so a v2 registry SSH terminal pane stays 'pending' forever (#97345). Fall back to the entry stamped with the window's connection id - the same identity match managedSshScopeRole applies to pool entries - and return the writer's actual scope so downstream getSshConnectionState and teardown keep working. --- apps/desktop/electron/main.ts | 19 +++++++++++++-- .../electron/window-connection-route.test.ts | 18 ++++++++++++++ .../electron/window-connection-route.ts | 24 +++++++++++++++++++ 3 files changed, 59 insertions(+), 2 deletions(-) diff --git a/apps/desktop/electron/main.ts b/apps/desktop/electron/main.ts index 77a1bfe586..5116cdb7bc 100644 --- a/apps/desktop/electron/main.ts +++ b/apps/desktop/electron/main.ts @@ -411,7 +411,11 @@ import { isHermesOwnedVenvDaemon } from './venv-holder-select' import { fetchMarketplaceThemes, searchMarketplaceThemes } from './vscode-marketplace' import { createWakeIndicatorWindowController } from './wake-indicator-window' import { enumerateWindowsFrontToBack, enumerationFailed, readWindowBelow } from './window-below' -import { registrySshScopeForWindowRoute, WindowConnectionRouteRegistry } from './window-connection-route' +import { + registrySshPoolScopeByConnectionId, + registrySshScopeForWindowRoute, + WindowConnectionRouteRegistry +} from './window-connection-route' import { createWindowOpenHandler } from './window-open-policy' import { installWindowRendererLifecycle } from './window-renderer-lifecycle' import { createWindowRevealController } from './window-reveal' @@ -10315,7 +10319,18 @@ function activeSshTerminalTarget(webContentsId?: number) { const state = sshConnections.get(scope) - return state && state.ssh ? { ssh: state.ssh, scope } : 'pending' + if (state && state.ssh) { + return { ssh: state.ssh, scope } + } + + // The pool's single writer publishes under the per-profile bootstrap key + // while stamping the entry with its registry connection id (#97345), so a + // composite-key miss must still resolve the live tunnel by that identity + // instead of reporting 'pending' forever. + const pooledScope = registrySshPoolScopeByConnectionId(sshConnections, windowRoute.connectionId) + const pooledState = pooledScope === null ? null : sshConnections.get(pooledScope) + + return pooledState && pooledState.ssh ? { ssh: pooledState.ssh, scope: pooledScope } : 'pending' } const profile = windowRoute?.profile ?? primaryProfileKey() diff --git a/apps/desktop/electron/window-connection-route.test.ts b/apps/desktop/electron/window-connection-route.test.ts index 17a4a488f4..400f151db7 100644 --- a/apps/desktop/electron/window-connection-route.test.ts +++ b/apps/desktop/electron/window-connection-route.test.ts @@ -4,6 +4,7 @@ import { test } from 'vitest' import { normalizeWindowConnectionRoute, + registrySshPoolScopeByConnectionId, registrySshScopeForWindowRoute, WindowConnectionRouteRegistry } from './window-connection-route' @@ -120,3 +121,20 @@ test('uses the canonical default profile scope when a registry SSH route has no 'conn:source-b::default' ) }) + +test('recovers the bootstrap pool key a registry SSH tunnel was published under', () => { + const pool = new Map([['', { registryConnectionId: 'source-b', ssh: { alive: true } }]]) + + assert.equal(registrySshPoolScopeByConnectionId(pool, 'source-b'), '') +}) + +test('does not match another connection, an unlabelled entry, or a torn-down tunnel', () => { + const pool = new Map([ + ['research', { registryConnectionId: 'source-a', ssh: { alive: true } }], + ['', { registryConnectionId: '', ssh: { alive: true } }], + ['worker', { registryConnectionId: 'source-b' }] + ]) + + assert.equal(registrySshPoolScopeByConnectionId(pool, 'source-b'), null) + assert.equal(registrySshPoolScopeByConnectionId(pool, 'source-c'), null) +}) diff --git a/apps/desktop/electron/window-connection-route.ts b/apps/desktop/electron/window-connection-route.ts index 7c0a83cee5..806f690508 100644 --- a/apps/desktop/electron/window-connection-route.ts +++ b/apps/desktop/electron/window-connection-route.ts @@ -40,6 +40,30 @@ export function registrySshScopeForWindowRoute( return backendScopeKey(route.connectionId, route.profile) } +export interface RegistrySshPoolEntry { + registryConnectionId?: null | string + ssh?: unknown +} + +// The sshConnections pool has a single writer that publishes every tunnel under +// its per-profile bootstrap key while stamping the entry with the registry +// connection that owns it. A registry-scoped lookup under the composite +// backendScopeKey can therefore miss a live tunnel; this recovers the writer's +// actual key from the stamped identity, the same match managedSshScopeRole +// applies to pool entries. +export function registrySshPoolScopeByConnectionId( + entries: Iterable, + connectionId: string +): null | string { + for (const [scope, entry] of entries) { + if (entry?.ssh && entry.registryConnectionId === connectionId) { + return scope + } + } + + return null +} + export class WindowConnectionRouteRegistry { private readonly routes = new Map()