From 0ff097439e4d19f7b19f12fc74c3eab8e9b58446 Mon Sep 17 00:00:00 2001 From: Brooklyn Nicholson Date: Fri, 10 Jul 2026 01:47:46 -0500 Subject: [PATCH] refactor(desktop): DRY the cloud helpers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tighten the salvaged Hermes Cloud code with no behavior change: - main: one `trimCloudOrg` projection reused by the success-echo and the 409 org list (drop the duplicated map), and a `cloudLoginError()` factory for the three needsCloudLogin throw sites. - renderer: a `cloudLoginLapsed()` predicate for the duplicated needsCloudLogin→signed-out check. --- apps/desktop/electron/main.ts | 45 ++++++++++--------- .../src/app/settings/gateway-settings.tsx | 8 +++- 2 files changed, 29 insertions(+), 24 deletions(-) diff --git a/apps/desktop/electron/main.ts b/apps/desktop/electron/main.ts index da40595bfbc..3517975d738 100644 --- a/apps/desktop/electron/main.ts +++ b/apps/desktop/electron/main.ts @@ -5552,6 +5552,20 @@ function openPortalLoginWindow() { }) } +// A "portal session is gone, re-login" error the renderer flips to signed-out +// on (via the `needsCloudLogin` tag). `cause` chains the underlying 401 when +// the session lapsed mid-flight. +function cloudLoginError(message, cause?) { + const err = new Error(message) as any + err.needsCloudLogin = true + + if (cause) { + err.cause = cause + } + + return err +} + // Discover the hosted (Hermes Cloud) agents the signed-in user can see. Calls // the NAS trimmed-summary endpoint over the partition-bound net, so the portal // session cookie is attached automatically (no bearer needed — NAS accepts the @@ -5564,11 +5578,9 @@ async function discoverCloudAgents(org?: string) { const portalBaseUrl = resolvePortalBaseUrl() if (!(await hasLivePortalSession())) { - const err = new Error( + throw cloudLoginError( 'You are not signed in to Hermes Cloud. Open Settings → Gateway, choose Hermes Cloud, and sign in.' - ) as any - err.needsCloudLogin = true - throw err + ) } const orgQuery = org ? `?org=${encodeURIComponent(org)}` : '' @@ -5583,10 +5595,7 @@ async function discoverCloudAgents(org?: string) { // A 401 means the portal session lapsed between the liveness check and the // call — surface it as a re-login, not a generic failure. if (error && error.statusCode === 401) { - const err = new Error('Your Hermes Cloud session has expired. Open Settings → Gateway and sign in again.') as any - err.needsCloudLogin = true - err.cause = error - throw err + throw cloudLoginError('Your Hermes Cloud session has expired. Open Settings → Gateway and sign in again.', error) } // A 409 means we're a multi-org user who hasn't picked an org. The body @@ -5607,8 +5616,10 @@ async function discoverCloudAgents(org?: string) { return { agents: trimCloudAgents(body), org: trimCloudOrg(body?.org) } } -// Project a NAS response org ({ id, slug, name, isPersonal }) to the trimmed -// shape the renderer persists, or null when absent/malformed. +// Project a NAS org row ({ id, slug, name, isPersonal, role }) to the trimmed +// shape the renderer consumes/persists, or null when absent/malformed. Shared +// by the success-body echo (trimCloudOrg) and the 409 org list, so the two can +// never drift. function trimCloudOrg(org) { if (!org || typeof org !== 'object' || typeof org.id !== 'string') { return null @@ -5646,15 +5657,7 @@ function parseOrgSelectionError(error) { return null } - return parsed.orgs - .filter(o => o && typeof o === 'object' && typeof o.id === 'string') - .map(o => ({ - id: o.id, - slug: typeof o.slug === 'string' ? o.slug : null, - name: typeof o.name === 'string' ? o.name : o.id, - isPersonal: Boolean(o.isPersonal), - role: typeof o.role === 'string' ? o.role : 'MEMBER' - })) + return parsed.orgs.map(trimCloudOrg).filter(Boolean) } // Project NAS's agent rows to the trimmed DTO the renderer consumes. @@ -5688,9 +5691,7 @@ async function cloudAgentSilentSignIn(dashboardUrl) { // interactive prompt rather than a silent cascade. Discovery already gates on // this, but a selection can arrive after the session lapsed. if (!(await hasLivePortalSession())) { - const err = new Error('Your Hermes Cloud session has expired. Sign in to Hermes Cloud again.') as any - err.needsCloudLogin = true - throw err + throw cloudLoginError('Your Hermes Cloud session has expired. Sign in to Hermes Cloud again.') } await openOauthLoginWindow(baseUrl, { silent: true }) diff --git a/apps/desktop/src/app/settings/gateway-settings.tsx b/apps/desktop/src/app/settings/gateway-settings.tsx index 0ac9d520218..5e9e91dedc1 100644 --- a/apps/desktop/src/app/settings/gateway-settings.tsx +++ b/apps/desktop/src/app/settings/gateway-settings.tsx @@ -428,6 +428,10 @@ export function GatewaySettings() { // --- Hermes Cloud handlers --- + // A rejected cloud IPC call tagged `needsCloudLogin` means the portal session + // lapsed — treat it as signed-out so the panel falls back to the sign-in view. + const cloudLoginLapsed = (err: unknown) => Boolean(err && typeof err === 'object' && 'needsCloudLogin' in err) + // Pull the discovered agent list over the shared portal session. Tolerant of // a lapsed session: a needsCloudLogin error flips us back to signed-out. // `org` scopes discovery for multi-org users; when discovery comes back with @@ -475,7 +479,7 @@ export function GatewaySettings() { setCloudDiscover('error') // A lapsed/absent portal session means we're effectively signed out. - if (err && typeof err === 'object' && 'needsCloudLogin' in err) { + if (cloudLoginLapsed(err)) { setCloudSignedIn(false) } @@ -644,7 +648,7 @@ export function GatewaySettings() { setState(next) notify({ kind: 'success', title: g.cloudConnectedTitle, message: g.cloudConnectedTo(agent.name) }) } catch (err) { - if (err && typeof err === 'object' && 'needsCloudLogin' in err) { + if (cloudLoginLapsed(err)) { setCloudSignedIn(false) }