diff --git a/apps/desktop/src/app/settings/gateway-settings.test.ts b/apps/desktop/src/app/settings/gateway-settings.test.ts new file mode 100644 index 000000000000..a221a4c5acee --- /dev/null +++ b/apps/desktop/src/app/settings/gateway-settings.test.ts @@ -0,0 +1,19 @@ +import { describe, expect, it } from 'vitest' + +import { savedCloudConnectionUrl } from './gateway-settings' + +describe('savedCloudConnectionUrl', () => { + it('normalizes the URL of a persisted cloud connection', () => { + expect(savedCloudConnectionUrl({ mode: 'cloud', remoteUrl: ' HTTPS://AGENT.EXAMPLE/ ' })).toBe( + 'https://agent.example' + ) + }) + + it('does not treat a stale cloud URL on a local config as connected', () => { + expect(savedCloudConnectionUrl({ mode: 'local', remoteUrl: 'https://agent.example' })).toBe('') + }) + + it('does not treat a remote gateway URL as a connected cloud agent', () => { + expect(savedCloudConnectionUrl({ mode: 'remote', remoteUrl: 'https://agent.example' })).toBe('') + }) +}) diff --git a/apps/desktop/src/app/settings/gateway-settings.tsx b/apps/desktop/src/app/settings/gateway-settings.tsx index 637366f2d605..ca7ce2384f9c 100644 --- a/apps/desktop/src/app/settings/gateway-settings.tsx +++ b/apps/desktop/src/app/settings/gateway-settings.tsx @@ -44,6 +44,10 @@ const EMPTY_STATE: GatewaySettingsState = { cloudOrg: '' } +export function savedCloudConnectionUrl(config: Pick): string { + return config.mode === 'cloud' ? config.remoteUrl.trim().replace(/\/+$/, '').toLowerCase() : '' +} + function ModeCard({ active, description, @@ -124,6 +128,12 @@ export function GatewaySettings({ embedded = false }: { embedded?: boolean } = { const [state, setState] = useState(EMPTY_STATE) const [remoteToken, setRemoteToken] = useState('') const [lastTest, setLastTest] = useState(null) + const [connectedCloudUrl, setConnectedCloudUrl] = useState('') + + const acceptSavedConfig = (config: GatewaySettingsState) => { + setState(config) + setConnectedCloudUrl(savedCloudConnectionUrl(config)) + } // --- Hermes Cloud (cloud mode) state --- // One portal session powers discovery + the silent per-agent cascade. These @@ -191,7 +201,7 @@ export function GatewaySettings({ embedded = false }: { embedded?: boolean } = { return } - setState(config) + acceptSavedConfig(config) }) .catch(err => notifyError(err, g.failedLoad)) .finally(() => { @@ -220,7 +230,6 @@ export function GatewaySettings({ embedded = false }: { embedded?: boolean } = { // (trim, drop trailing slash, lowercase) or a host-casing difference would // silently break the connected-highlight. const normalizeCloudUrl = (url: string) => url.trim().replace(/\/+$/, '').toLowerCase() - const connectedCloudUrl = state.mode === 'cloud' ? normalizeCloudUrl(state.remoteUrl) : '' const isConnectedAgent = (agent: DesktopCloudAgent) => Boolean(connectedCloudUrl && agent.dashboardUrl && normalizeCloudUrl(agent.dashboardUrl) === connectedCloudUrl) @@ -368,7 +377,7 @@ export function GatewaySettings({ embedded = false }: { embedded?: boolean } = { ? await window.hermesDesktop.applyConnectionConfig(payload()) : await window.hermesDesktop.saveConnectionConfig(payload()) - setState(next) + acceptSavedConfig(next) setRemoteToken('') notify({ kind: 'success', @@ -404,13 +413,13 @@ export function GatewaySettings({ embedded = false }: { embedded?: boolean } = { remoteUrl: trimmedUrl }) - setState(saved) + acceptSavedConfig(saved) const result = await window.hermesDesktop.oauthLoginConnectionConfig(trimmedUrl) if (result.connected) { const refreshed = await window.hermesDesktop.getConnectionConfig(scope) - setState(refreshed) + acceptSavedConfig(refreshed) notify({ kind: 'success', title: g.signedIn, message: g.connectedTo(providerLabel) }) } else { notify({ @@ -432,7 +441,7 @@ export function GatewaySettings({ embedded = false }: { embedded?: boolean } = { try { await window.hermesDesktop.oauthLogoutConnectionConfig(trimmedUrl || undefined) const refreshed = await window.hermesDesktop.getConnectionConfig(scope) - setState(refreshed) + acceptSavedConfig(refreshed) notify({ kind: 'success', title: g.signedOutTitle, message: g.signedOutMessage }) } catch (err) { notifyError(err, g.signOutFailed) @@ -656,7 +665,7 @@ export function GatewaySettings({ embedded = false }: { embedded?: boolean } = { cloudOrg: cloudOrgRef.current ?? undefined }) - setState(next) + acceptSavedConfig(next) notify({ kind: 'success', title: g.cloudConnectedTitle, message: g.cloudConnectedTo(agent.name) }) } catch (err) { if (err && typeof err === 'object' && 'needsCloudLogin' in err) {