mirror of
https://github.com/stablyai/orca.git
synced 2026-10-26 08:02:44 +00:00
feat(settings): show Orca Relay sign-in status
Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
@@ -2202,6 +2202,7 @@ app.whenReady().then(async () => {
|
||||
runtimeRpc,
|
||||
onStatus: (status) => {
|
||||
desktopRelayStatus = status
|
||||
mainWindow?.webContents.send('mobile:relayStatusChanged', status)
|
||||
}
|
||||
})
|
||||
desktopRelayService = relayService
|
||||
|
||||
@@ -293,4 +293,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' })
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
@@ -3167,6 +3168,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<SpeechModelManifest[]>
|
||||
|
||||
+12
-1
@@ -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,
|
||||
@@ -4295,7 +4296,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: {
|
||||
|
||||
@@ -12,6 +12,7 @@ import { MobilePairingQrSection } from './MobilePairingQrSection'
|
||||
import { MobilePairedDevicesSection, type PairedDevice } from './MobilePairedDevicesSection'
|
||||
import { MobileAutoRestoreFitSection } from './MobileAutoRestoreFitSection'
|
||||
import { WindowsFirewallNotice } from '../mobile/WindowsFirewallNotice'
|
||||
import { MobileRelayStatusSection } from './MobileRelayStatusSection'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
export { getMobilePaneSearchEntries } from './mobile-pane-search'
|
||||
|
||||
@@ -161,6 +162,8 @@ export function MobilePane(): React.JSX.Element {
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<MobileRelayStatusSection />
|
||||
|
||||
<MobileNetworkInterfaceSection
|
||||
networkInterfaces={networkInterfaces}
|
||||
selectedAddress={selectedAddress}
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import '@testing-library/jest-dom/vitest'
|
||||
|
||||
import { cleanup, render, screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { MobileRelayStatus } from '../../../../shared/mobile-relay-status'
|
||||
import type { OrcaProfileAuthStatus } from '../../../../shared/orca-profiles'
|
||||
import { MobileRelayStatusSection } from './MobileRelayStatusSection'
|
||||
|
||||
type MobileRelayStoreState = {
|
||||
orcaProfileAuthStatus: OrcaProfileAuthStatus | null
|
||||
orcaProfileConnecting: boolean
|
||||
connectCurrentOrcaProfile: () => Promise<null>
|
||||
}
|
||||
|
||||
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(<MobileRelayStatusSection />)
|
||||
|
||||
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(<MobileRelayStatusSection />)
|
||||
|
||||
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()
|
||||
})
|
||||
})
|
||||
@@ -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<MobileRelayStatus>('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 (
|
||||
<section className="space-y-2">
|
||||
<h3 className="text-sm font-medium">
|
||||
{translate('auto.components.settings.MobileRelayStatusSection.title', 'Orca Relay')}
|
||||
</h3>
|
||||
<div className="flex items-center justify-between gap-4 rounded-lg border border-border/60 px-3 py-2.5">
|
||||
<div className="min-w-0 space-y-0.5">
|
||||
<p className="text-sm font-medium">
|
||||
{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'
|
||||
)}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{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.'
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
{signedIn ? (
|
||||
<Badge variant="outline" className="shrink-0">
|
||||
{relayStatusLabel(relayStatus)}
|
||||
</Badge>
|
||||
) : configured ? (
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
className="w-24 shrink-0"
|
||||
disabled={connecting}
|
||||
onClick={() => void connect()}
|
||||
>
|
||||
{connecting ? <Loader2 className="animate-spin" /> : null}
|
||||
{translate('auto.components.settings.MobileRelayStatusSection.signIn', 'Sign in')}
|
||||
</Button>
|
||||
) : (
|
||||
<Badge variant="outline" className="shrink-0">
|
||||
{translate(
|
||||
'auto.components.settings.MobileRelayStatusSection.unavailable',
|
||||
'Unavailable'
|
||||
)}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -9032,6 +9032,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": {
|
||||
|
||||
@@ -9032,6 +9032,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": {
|
||||
|
||||
@@ -9032,6 +9032,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": {
|
||||
|
||||
@@ -9032,6 +9032,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": {
|
||||
|
||||
@@ -9032,6 +9032,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": {
|
||||
|
||||
@@ -758,7 +758,10 @@ function createWebPreloadApi(): Partial<PreloadApi> {
|
||||
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(),
|
||||
|
||||
Reference in New Issue
Block a user