From e253b7bd5e45a3f6c9252757628cdc2e81c34f80 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Mon, 13 Jul 2026 01:08:10 -0400 Subject: [PATCH] feat(settings): show Orca Relay sign-in status Co-authored-by: Orca --- src/main/index.ts | 1 + src/main/ipc/mobile.test.ts | 6 + .../runtime/relay/relay-session-broker.ts | 3 +- src/preload/api-types.ts | 3 + src/preload/index.ts | 13 +- .../src/components/settings/MobilePane.tsx | 3 + .../MobileRelayStatusSection.test.tsx | 88 ++++++++++++++ .../settings/MobileRelayStatusSection.tsx | 112 ++++++++++++++++++ src/renderer/src/i18n/locales/en.json | 13 ++ src/renderer/src/i18n/locales/es.json | 13 ++ src/renderer/src/i18n/locales/ja.json | 13 ++ src/renderer/src/i18n/locales/ko.json | 13 ++ src/renderer/src/i18n/locales/zh.json | 13 ++ src/renderer/src/web/web-preload-api.ts | 5 +- src/shared/mobile-relay-status.ts | 3 + 15 files changed, 299 insertions(+), 3 deletions(-) create mode 100644 src/renderer/src/components/settings/MobileRelayStatusSection.test.tsx create mode 100644 src/renderer/src/components/settings/MobileRelayStatusSection.tsx create mode 100644 src/shared/mobile-relay-status.ts diff --git a/src/main/index.ts b/src/main/index.ts index a169ca4fe48..dac37e87790 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -2158,6 +2158,7 @@ app.whenReady().then(async () => { runtimeRpc, onStatus: (status) => { desktopRelayStatus = status + mainWindow?.webContents.send('mobile:relayStatusChanged', status) } }) desktopRelayService = relayService diff --git a/src/main/ipc/mobile.test.ts b/src/main/ipc/mobile.test.ts index 5db24da91ca..daedc23a9f8 100644 --- a/src/main/ipc/mobile.test.ts +++ b/src/main/ipc/mobile.test.ts @@ -240,4 +240,10 @@ describe('registerMobileHandlers', () => { ).resolves.toEqual({ revoked: true }) expect(revokeMobileDevice).toHaveBeenCalledWith('mobile-1') }) + + it('reports the current relay broker status without exposing a toggle', () => { + registerMobileHandlers({} as never, { getRelayStatus: () => 'registered' }) + + expect(handlers.get('mobile:getRelayStatus')?.()).toEqual({ status: 'registered' }) + }) }) diff --git a/src/main/runtime/relay/relay-session-broker.ts b/src/main/runtime/relay/relay-session-broker.ts index a4b7034d6fa..82124f76bff 100644 --- a/src/main/runtime/relay/relay-session-broker.ts +++ b/src/main/runtime/relay/relay-session-broker.ts @@ -8,6 +8,7 @@ import type { MobileRelayEndpoint, PairingProvisionRelayParams } from '../../../shared/mobile-relay-credential-contract' +import type { MobileRelayStatus } from '../../../shared/mobile-relay-status' import type { E2EEKeypair } from '../e2ee-keypair' import type { MobileSocketWiring } from '../rpc/mobile-socket-wiring' import { CloudRelayTransport } from '../rpc/relay-transport' @@ -21,7 +22,7 @@ import { type RelayAssignment } from './relay-http-client' -export type RelayBrokerStatus = 'connecting' | 'registered' | 'draining' | 'offline' +export type RelayBrokerStatus = MobileRelayStatus type RelayIdentity = { userId: string diff --git a/src/preload/api-types.ts b/src/preload/api-types.ts index 4babcd587bb..9dbf6891297 100644 --- a/src/preload/api-types.ts +++ b/src/preload/api-types.ts @@ -17,6 +17,7 @@ import type { } from '../shared/local-log-tail-types' import type { ReadClipboardTextOptions } from '../shared/clipboard-text' import type { AppIdentity } from '../shared/app-identity' +import type { MobileRelayStatus } from '../shared/mobile-relay-status' import type { CreateLocalOrcaProfileArgs, CreateLocalOrcaProfileResult, @@ -3143,6 +3144,8 @@ export type PreloadApi = { listRuntimeAccessGrants: () => Promise<{ grants: RuntimeAccessGrant[] }> revokeRuntimeAccess: (args: { deviceId: string }) => Promise<{ revoked: boolean }> isWebSocketReady: () => Promise<{ ready: boolean; endpoint: string | null }> + getRelayStatus: () => Promise<{ status: MobileRelayStatus }> + onRelayStatusChanged: (callback: (status: MobileRelayStatus) => void) => () => void } speech: { getCatalog: () => Promise diff --git a/src/preload/index.ts b/src/preload/index.ts index 3c7af55f0af..918b2395ec7 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -12,6 +12,7 @@ import type { TerminalPaneSplitSource } from '../shared/feature-education-teleme import type { ProjectExecutionRuntimeResolution } from '../shared/project-execution-runtime' import type { StartupCommandDelivery } from '../shared/codex-startup-delivery' import type { SleepingAgentLaunchConfig } from '../shared/agent-session-resume' +import type { MobileRelayStatus } from '../shared/mobile-relay-status' import type { BaseRefSearchResult, BaseRefDefaultResult, @@ -4263,7 +4264,17 @@ const api = { ipcRenderer.invoke('mobile:revokeRuntimeAccess', args), isWebSocketReady: (): Promise<{ ready: boolean; endpoint: string | null }> => - ipcRenderer.invoke('mobile:isWebSocketReady') + ipcRenderer.invoke('mobile:isWebSocketReady'), + + getRelayStatus: (): Promise<{ status: MobileRelayStatus }> => + ipcRenderer.invoke('mobile:getRelayStatus'), + + onRelayStatusChanged: (callback: (status: MobileRelayStatus) => void): (() => void) => { + const listener = (_event: Electron.IpcRendererEvent, status: MobileRelayStatus) => + callback(status) + ipcRenderer.on('mobile:relayStatusChanged', listener) + return () => ipcRenderer.removeListener('mobile:relayStatusChanged', listener) + } }, agentStatus: { diff --git a/src/renderer/src/components/settings/MobilePane.tsx b/src/renderer/src/components/settings/MobilePane.tsx index 62f93109850..31c9ecb40b7 100644 --- a/src/renderer/src/components/settings/MobilePane.tsx +++ b/src/renderer/src/components/settings/MobilePane.tsx @@ -11,6 +11,7 @@ import { MobileNetworkInterfaceSection } from './MobileNetworkInterfaceSection' import { MobilePairingQrSection } from './MobilePairingQrSection' import { MobilePairedDevicesSection, type PairedDevice } from './MobilePairedDevicesSection' import { MobileAutoRestoreFitSection } from './MobileAutoRestoreFitSection' +import { MobileRelayStatusSection } from './MobileRelayStatusSection' import { translate } from '@/i18n/i18n' export { getMobilePaneSearchEntries } from './mobile-pane-search' @@ -160,6 +161,8 @@ export function MobilePane(): React.JSX.Element { return (
+ + Promise +} + +const mocks = vi.hoisted(() => ({ + state: {} as MobileRelayStoreState +})) + +vi.mock('@/store', () => ({ + useAppStore: (selector: (state: MobileRelayStoreState) => unknown) => selector(mocks.state) +})) + +describe('MobileRelayStatusSection', () => { + let statusListener: ((status: MobileRelayStatus) => void) | null + const connect = vi.fn().mockResolvedValue(null) + + beforeEach(() => { + statusListener = null + connect.mockClear() + Object.defineProperty(window, 'api', { + configurable: true, + value: { + mobile: { + getRelayStatus: vi.fn().mockResolvedValue({ status: 'registered' }), + onRelayStatusChanged: vi.fn((listener: (status: MobileRelayStatus) => void) => { + statusListener = listener + return vi.fn() + }) + } + } + }) + mocks.state = { + orcaProfileAuthStatus: { + activeProfileId: 'profile-1', + configured: true, + state: 'local', + persistence: 'none' + }, + orcaProfileConnecting: false, + connectCurrentOrcaProfile: connect + } + }) + + afterEach(() => cleanup()) + + it('keeps direct pairing available while offering desktop sign-in', async () => { + const user = userEvent.setup() + render() + + expect( + screen.getByText('LAN and Tailscale pairing still work without an account.') + ).toBeVisible() + await user.click(screen.getByRole('button', { name: 'Sign in' })) + expect(connect).toHaveBeenCalledOnce() + }) + + it('shows live automatic relay status for a signed-in desktop', async () => { + mocks.state = { + orcaProfileAuthStatus: { + activeProfileId: 'profile-1', + configured: true, + state: 'connected', + persistence: 'encrypted' + }, + orcaProfileConnecting: false, + connectCurrentOrcaProfile: connect + } + render() + + await waitFor(() => expect(screen.getByText('Registered')).toBeVisible()) + statusListener?.('offline') + await waitFor(() => expect(screen.getByText('Offline')).toBeVisible()) + expect(screen.queryByRole('button', { name: 'Sign in' })).not.toBeInTheDocument() + }) +}) diff --git a/src/renderer/src/components/settings/MobileRelayStatusSection.tsx b/src/renderer/src/components/settings/MobileRelayStatusSection.tsx new file mode 100644 index 00000000000..79312e97738 --- /dev/null +++ b/src/renderer/src/components/settings/MobileRelayStatusSection.tsx @@ -0,0 +1,112 @@ +import { useEffect, useState } from 'react' +import { Loader2 } from 'lucide-react' +import { Badge } from '../ui/badge' +import { Button } from '../ui/button' +import { translate } from '@/i18n/i18n' +import { useAppStore } from '@/store' +import type { MobileRelayStatus } from '../../../../shared/mobile-relay-status' + +function relayStatusLabel(status: MobileRelayStatus): string { + if (status === 'registered') { + return translate('auto.components.settings.MobileRelayStatusSection.registered', 'Registered') + } + if (status === 'connecting') { + return translate('auto.components.settings.MobileRelayStatusSection.connecting', 'Connecting') + } + if (status === 'draining') { + return translate( + 'auto.components.settings.MobileRelayStatusSection.reconnecting', + 'Reconnecting' + ) + } + return translate('auto.components.settings.MobileRelayStatusSection.offline', 'Offline') +} + +export function MobileRelayStatusSection(): React.JSX.Element { + const authStatus = useAppStore((state) => state.orcaProfileAuthStatus) + const connecting = useAppStore((state) => state.orcaProfileConnecting) + const connect = useAppStore((state) => state.connectCurrentOrcaProfile) + const [relayStatus, setRelayStatus] = useState('offline') + const signedIn = authStatus?.state === 'connected' + const configured = authStatus?.configured !== false + + useEffect(() => { + let receivedEvent = false + let active = true + const unsubscribe = window.api.mobile.onRelayStatusChanged((status) => { + receivedEvent = true + if (active) { + setRelayStatus(status) + } + }) + void window.api.mobile + .getRelayStatus() + .then(({ status }) => { + if (active && !receivedEvent) { + setRelayStatus(status) + } + }) + .catch(() => {}) + return () => { + active = false + unsubscribe() + } + }, []) + + return ( +
+

+ {translate('auto.components.settings.MobileRelayStatusSection.title', 'Orca Relay')} +

+
+
+

+ {signedIn + ? translate( + 'auto.components.settings.MobileRelayStatusSection.automatic', + 'Connect from anywhere automatically' + ) + : translate( + 'auto.components.settings.MobileRelayStatusSection.signInPrompt', + 'Sign in on this desktop to connect from anywhere' + )} +

+

+ {signedIn + ? translate( + 'auto.components.settings.MobileRelayStatusSection.directStillAvailable', + 'LAN and Tailscale connections remain available.' + ) + : translate( + 'auto.components.settings.MobileRelayStatusSection.directNeedsNoAccount', + 'LAN and Tailscale pairing still work without an account.' + )} +

+
+ {signedIn ? ( + + {relayStatusLabel(relayStatus)} + + ) : configured ? ( + + ) : ( + + {translate( + 'auto.components.settings.MobileRelayStatusSection.unavailable', + 'Unavailable' + )} + + )} +
+
+ ) +} diff --git a/src/renderer/src/i18n/locales/en.json b/src/renderer/src/i18n/locales/en.json index c89567567d0..b3e64c39f1d 100644 --- a/src/renderer/src/i18n/locales/en.json +++ b/src/renderer/src/i18n/locales/en.json @@ -9021,6 +9021,19 @@ "TerminalInteractionSection": { "567633ff50": "Right-click pastes the clipboard into the terminal. Control-click to open the context menu.", "c64497148a": "Right-click pastes the clipboard. Control-click opens the context menu." + }, + "MobileRelayStatusSection": { + "registered": "Registered", + "connecting": "Connecting", + "reconnecting": "Reconnecting", + "offline": "Offline", + "title": "Orca Relay", + "automatic": "Connect from anywhere automatically", + "signInPrompt": "Sign in on this desktop to connect from anywhere", + "directStillAvailable": "LAN and Tailscale connections remain available.", + "directNeedsNoAccount": "LAN and Tailscale pairing still work without an account.", + "signIn": "Sign in", + "unavailable": "Unavailable" } }, "right": { diff --git a/src/renderer/src/i18n/locales/es.json b/src/renderer/src/i18n/locales/es.json index 11cf5339d6b..008bbf3111e 100644 --- a/src/renderer/src/i18n/locales/es.json +++ b/src/renderer/src/i18n/locales/es.json @@ -9021,6 +9021,19 @@ "TerminalInteractionSection": { "567633ff50": "Right-click pastes the clipboard into the terminal. Control-click to open the context menu.", "c64497148a": "Right-click pastes the clipboard. Control-click opens the context menu." + }, + "MobileRelayStatusSection": { + "registered": "Registered", + "connecting": "Connecting", + "reconnecting": "Reconnecting", + "offline": "Offline", + "title": "Orca Relay", + "automatic": "Connect from anywhere automatically", + "signInPrompt": "Sign in on this desktop to connect from anywhere", + "directStillAvailable": "LAN and Tailscale connections remain available.", + "directNeedsNoAccount": "LAN and Tailscale pairing still work without an account.", + "signIn": "Sign in", + "unavailable": "Unavailable" } }, "right": { diff --git a/src/renderer/src/i18n/locales/ja.json b/src/renderer/src/i18n/locales/ja.json index 8cc54d3ac1f..7c87837b21d 100644 --- a/src/renderer/src/i18n/locales/ja.json +++ b/src/renderer/src/i18n/locales/ja.json @@ -9021,6 +9021,19 @@ "TerminalInteractionSection": { "567633ff50": "Right-click pastes the clipboard into the terminal. Control-click to open the context menu.", "c64497148a": "Right-click pastes the clipboard. Control-click opens the context menu." + }, + "MobileRelayStatusSection": { + "registered": "Registered", + "connecting": "Connecting", + "reconnecting": "Reconnecting", + "offline": "Offline", + "title": "Orca Relay", + "automatic": "Connect from anywhere automatically", + "signInPrompt": "Sign in on this desktop to connect from anywhere", + "directStillAvailable": "LAN and Tailscale connections remain available.", + "directNeedsNoAccount": "LAN and Tailscale pairing still work without an account.", + "signIn": "Sign in", + "unavailable": "Unavailable" } }, "right": { diff --git a/src/renderer/src/i18n/locales/ko.json b/src/renderer/src/i18n/locales/ko.json index e66b07cc5e0..ced237cec20 100644 --- a/src/renderer/src/i18n/locales/ko.json +++ b/src/renderer/src/i18n/locales/ko.json @@ -9021,6 +9021,19 @@ "TerminalInteractionSection": { "567633ff50": "Right-click pastes the clipboard into the terminal. Control-click to open the context menu.", "c64497148a": "Right-click pastes the clipboard. Control-click opens the context menu." + }, + "MobileRelayStatusSection": { + "registered": "Registered", + "connecting": "Connecting", + "reconnecting": "Reconnecting", + "offline": "Offline", + "title": "Orca Relay", + "automatic": "Connect from anywhere automatically", + "signInPrompt": "Sign in on this desktop to connect from anywhere", + "directStillAvailable": "LAN and Tailscale connections remain available.", + "directNeedsNoAccount": "LAN and Tailscale pairing still work without an account.", + "signIn": "Sign in", + "unavailable": "Unavailable" } }, "right": { diff --git a/src/renderer/src/i18n/locales/zh.json b/src/renderer/src/i18n/locales/zh.json index e0d28d35be6..c491dc470be 100644 --- a/src/renderer/src/i18n/locales/zh.json +++ b/src/renderer/src/i18n/locales/zh.json @@ -9021,6 +9021,19 @@ "TerminalInteractionSection": { "567633ff50": "Right-click pastes the clipboard into the terminal. Control-click to open the context menu.", "c64497148a": "Right-click pastes the clipboard. Control-click opens the context menu." + }, + "MobileRelayStatusSection": { + "registered": "Registered", + "connecting": "Connecting", + "reconnecting": "Reconnecting", + "offline": "Offline", + "title": "Orca Relay", + "automatic": "Connect from anywhere automatically", + "signInPrompt": "Sign in on this desktop to connect from anywhere", + "directStillAvailable": "LAN and Tailscale connections remain available.", + "directNeedsNoAccount": "LAN and Tailscale pairing still work without an account.", + "signIn": "Sign in", + "unavailable": "Unavailable" } }, "right": { diff --git a/src/renderer/src/web/web-preload-api.ts b/src/renderer/src/web/web-preload-api.ts index 4e4b5502051..321176f13d8 100644 --- a/src/renderer/src/web/web-preload-api.ts +++ b/src/renderer/src/web/web-preload-api.ts @@ -750,7 +750,10 @@ function createWebPreloadApi(): Partial { revokeDevice: () => Promise.resolve({ revoked: false }), listRuntimeAccessGrants: () => Promise.resolve({ grants: [] }), revokeRuntimeAccess: () => Promise.resolve({ revoked: false }), - isWebSocketReady: () => Promise.resolve({ ready: Boolean(activeEnvironment), endpoint: null }) + isWebSocketReady: () => + Promise.resolve({ ready: Boolean(activeEnvironment), endpoint: null }), + getRelayStatus: () => Promise.resolve({ status: 'offline' as const }), + onRelayStatusChanged: () => noopUnsubscribe }, telemetryTrack: () => Promise.resolve(), telemetrySetOptIn: () => Promise.resolve(), diff --git a/src/shared/mobile-relay-status.ts b/src/shared/mobile-relay-status.ts new file mode 100644 index 00000000000..09a9a3222de --- /dev/null +++ b/src/shared/mobile-relay-status.ts @@ -0,0 +1,3 @@ +export const MOBILE_RELAY_STATUSES = ['connecting', 'registered', 'draining', 'offline'] as const + +export type MobileRelayStatus = (typeof MOBILE_RELAY_STATUSES)[number]