From 90ee4460cb45f46fdbcb3288beebf74c6b150e00 Mon Sep 17 00:00:00 2001 From: Brooklyn Nicholson Date: Tue, 25 Aug 2026 11:38:44 -0500 Subject: [PATCH] fix(desktop): translate sidebar recents_profile through SSH aliases Managed SSH maps a Desktop profile label onto a different remote name. The sidebar filter lives in recents_profile, so rewriting only ?profile= left those reads on the remote default and the Sessions list came back empty. Co-authored-by: noah --- .../electron/connection-config.test.ts | 23 ++++++++++++ apps/desktop/electron/connection-config.ts | 35 ++++++++++++++----- 2 files changed, 50 insertions(+), 8 deletions(-) diff --git a/apps/desktop/electron/connection-config.test.ts b/apps/desktop/electron/connection-config.test.ts index 0f7c19bcdd..9cc7cf66cf 100644 --- a/apps/desktop/electron/connection-config.test.ts +++ b/apps/desktop/electron/connection-config.test.ts @@ -504,6 +504,14 @@ test('pathForRegistryBackendRequest uses the resolved registry backend scope', ( }), '/api/fs/download?path=%2Fsrv%2Freport.pdf' ) + assert.equal( + pathForRegistryBackendRequest( + '/api/profiles/sessions/sidebar?recents_profile=research&recents_exclude=cron%2Cdesktop', + 'research', + { remoteProfile: 'remote-research' } + ), + '/api/profiles/sessions/sidebar?recents_profile=remote-research&recents_exclude=cron%2Cdesktop' + ) }) // --- pathWithGlobalRemoteProfile --- @@ -604,8 +612,23 @@ test('translateSelfProfileQuery rewrites the self-profile filter into the backen ) }) +test('translateSelfProfileQuery rewrites sidebar recents_profile aliases for managed SSH', () => { + assert.equal( + translateSelfProfileQuery( + '/api/profiles/sessions/sidebar?recents_profile=research&recents_limit=20&cron_limit=50&messaging_limit=100', + 'research', + 'remote-research' + ), + '/api/profiles/sessions/sidebar?recents_profile=remote-research&recents_limit=20&cron_limit=50&messaging_limit=100' + ) +}) + test('translateSelfProfileQuery leaves cross-profile and unfiltered paths untouched', () => { assert.equal(translateSelfProfileQuery('/api/cron/jobs?profile=all', 'mara', 'default'), '/api/cron/jobs?profile=all') + assert.equal( + translateSelfProfileQuery('/api/profiles/sessions/sidebar?recents_profile=all', 'mara', 'default'), + '/api/profiles/sessions/sidebar?recents_profile=all' + ) assert.equal( translateSelfProfileQuery('/api/cron/jobs?profile=worker', 'mara', 'default'), '/api/cron/jobs?profile=worker' diff --git a/apps/desktop/electron/connection-config.ts b/apps/desktop/electron/connection-config.ts index 590f34473b..986758a598 100644 --- a/apps/desktop/electron/connection-config.ts +++ b/apps/desktop/electron/connection-config.ts @@ -735,14 +735,23 @@ function pathWithGlobalRemoteProfile(path, profile, opts: ProfileRouteOptions = return pathWithProfileScope(path, profile) } +/** Extra profile-valued query keys, beyond `profile`, that name the same + * self-scope on a given path. The sidebar batches recents/cron/messaging + * behind `recents_profile` instead of `profile`, so an SSH alias rewrite + * that only looks at `?profile=` leaves those reads on the remote default. */ +const SELF_PROFILE_QUERY_KEYS_BY_PATH: Record = { + '/api/profiles/sessions/sidebar': ['recents_profile'] +} + /** * Translate an explicit self-profile query from a Desktop routing alias to the * backend's own profile namespace (a managed SSH `remoteProfile` can map local - * `mara` to remote `default`). Only a `?profile=` equal to the alias itself is - * rewritten; cross-profile selectors (`all`, another concrete profile) and - * unfiltered paths pass through untouched. Used by the v1 profile route above - * and by the registry SSH branch of the `hermes:api` handler — both routes - * reach a backend whose namespace is the remote profile, not the alias. + * `mara` to remote `default`). Only endpoint-declared profile-valued params + * equal to the alias itself are rewritten; cross-profile selectors (`all`, + * another concrete profile) and unfiltered paths pass through untouched. Used + * by the v1 profile route above and by the registry SSH branch of the + * `hermes:api` handler — both routes reach a backend whose namespace is the + * remote profile, not the alias. */ function translateSelfProfileQuery(path, profile, backendProfile) { const scopedProfile = connectionScopeKey(profile) @@ -766,11 +775,21 @@ function translateSelfProfileQuery(path, profile, backendProfile) { return path } - if (connectionScopeKey(parsed.searchParams.get('profile')) !== scopedProfile) { - return path + const profileQueryKeys = ['profile', ...(SELF_PROFILE_QUERY_KEYS_BY_PATH[parsed.pathname] || [])] + let changed = false + + for (const key of profileQueryKeys) { + if (connectionScopeKey(parsed.searchParams.get(key)) !== scopedProfile) { + continue + } + + parsed.searchParams.set(key, backend) + changed = true } - parsed.searchParams.set('profile', backend) + if (!changed) { + return path + } return `${parsed.pathname}${parsed.search}${parsed.hash}` }