Move mobile settings to beta section (#1741)

* Move mobile settings to beta section

Co-authored-by: Orca <help@stably.ai>

* Add mobile setup guidance

Co-authored-by: Orca <help@stably.ai>

* Use global App Store URL for mobile

Co-authored-by: Orca <help@stably.ai>

---------

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
Jinwoo Hong 2026-05-13 03:29:52 -04:00 committed by GitHub
parent 954fb9f316
commit fa769edb8d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 194 additions and 82 deletions

View File

@ -9,7 +9,6 @@ import { useAppStore } from '../../store'
import { SearchableSetting } from './SearchableSetting'
import { matchesSettingsSearch } from './settings-search'
import { EXPERIMENTAL_PANE_SEARCH_ENTRIES, EXPERIMENTAL_SEARCH_ENTRY } from './experimental-search'
import { MobilePane } from './MobilePane'
import { HiddenExperimentalGroup } from './HiddenExperimentalGroup'
export { EXPERIMENTAL_PANE_SEARCH_ENTRIES }
@ -31,7 +30,6 @@ export function ExperimentalPane({
hiddenExperimentalUnlocked = false
}: ExperimentalPaneProps): React.JSX.Element {
const searchQuery = useAppStore((s) => s.settingsSearchQuery)
const showMobile = matchesSettingsSearch(searchQuery, [EXPERIMENTAL_SEARCH_ENTRY.mobile])
const showPet = matchesSettingsSearch(searchQuery, [EXPERIMENTAL_SEARCH_ENTRY.pet])
const showOrchestration = matchesSettingsSearch(searchQuery, [
EXPERIMENTAL_SEARCH_ENTRY.orchestration
@ -70,66 +68,6 @@ export function ExperimentalPane({
return (
<div className="space-y-4">
{showMobile ? (
<SearchableSetting
title="Mobile Pairing"
description="Pair a mobile device to control Orca remotely."
keywords={EXPERIMENTAL_SEARCH_ENTRY.mobile.keywords}
className="space-y-3 px-1 py-2"
>
<div className="flex items-start justify-between gap-4">
<div className="min-w-0 shrink space-y-1.5">
<Label>Mobile Pairing</Label>
<p className="text-xs text-muted-foreground">
Control Orca from your phone by scanning a QR code. Beta / early preview &mdash;
expect bugs and breaking changes. Get started from the{' '}
<button
type="button"
// Why: points at the current mobile release tag rather than
// the generic /releases page, which is dominated by desktop
// releases and forces the user to scroll. Update this URL
// when cutting a new mobile-v* tag.
onClick={() =>
void window.api.shell.openUrl(
'https://github.com/stablyai/orca/releases/tag/mobile-v0.0.2'
)
}
className="cursor-pointer underline underline-offset-2 hover:text-foreground"
>
GitHub Releases page
</button>
.
</p>
</div>
<button
type="button"
role="switch"
aria-checked={settings.experimentalMobile}
onClick={() =>
updateSettings({
experimentalMobile: !settings.experimentalMobile
})
}
className={`relative inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border border-transparent transition-colors ${
settings.experimentalMobile ? 'bg-foreground' : 'bg-muted-foreground/30'
}`}
>
<span
className={`inline-block h-3.5 w-3.5 transform rounded-full bg-background shadow-sm transition-transform ${
settings.experimentalMobile ? 'translate-x-4' : 'translate-x-0.5'
}`}
/>
</button>
</div>
{settings.experimentalMobile ? (
<div className="rounded-xl border border-border/60 bg-card/50 p-4">
<MobilePane />
</div>
) : null}
</SearchableSetting>
) : null}
{showPet ? (
<SearchableSetting
title="Pet"

View File

@ -1,6 +1,16 @@
import { useCallback, useEffect, useState } from 'react'
import { toast } from 'sonner'
import { Check, Copy, Maximize2, RefreshCw, Smartphone, Trash2, Wifi } from 'lucide-react'
import {
Check,
Copy,
ExternalLink,
Maximize2,
RefreshCw,
Smartphone,
Trash2,
Wifi
} from 'lucide-react'
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '../ui/accordion'
import { Button } from '../ui/button'
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '../ui/dialog'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select'
@ -18,6 +28,8 @@ const AUTO_RESTORE_FIT_OPTIONS: { value: string; label: string; ms: number | nul
{ value: '30m', label: 'After 30 minutes', ms: 30 * 60_000 }
]
const TAILSCALE_DOWNLOAD_URL = 'https://tailscale.com/download'
function autoRestoreValueFromMs(ms: number | null | undefined): string {
if (ms == null) {
return 'indefinite'
@ -40,7 +52,19 @@ export const MOBILE_PANE_SEARCH_ENTRIES: SettingsSearchEntry[] = [
{
title: 'Network Interface',
description: 'Choose which network address to use for mobile pairing.',
keywords: ['network', 'interface', 'tailscale', 'vpn', 'overlay', 'ip', 'address', 'wifi']
keywords: [
'network',
'interface',
'tailscale',
'tailnet',
'vpn',
'overlay',
'ip',
'address',
'wifi',
'lan',
'remote'
]
},
{
title: 'When you leave the mobile app',
@ -227,6 +251,40 @@ export function MobilePane(): React.JSX.Element {
{qrDataUrl ? 'Regenerate' : 'Generate QR Code'}
</Button>
</div>
<Accordion type="single" collapsible className="mt-4 border-t border-border/60 pt-2">
<AccordionItem value="remote-pairing-guide">
<AccordionTrigger className="py-2 text-xs">
Connect outside your Wi-Fi with a tailnet
</AccordionTrigger>
<AccordionContent className="space-y-3 text-xs text-muted-foreground">
<p>
Orca Mobile connects directly to this computer. To use it away from the same local
network, put your computer and phone on the same private overlay network, then
generate the QR code with that network address selected.
</p>
<ol className="list-decimal space-y-1 pl-4">
<li>
Install{' '}
<button
type="button"
onClick={() => void window.api.shell.openUrl(TAILSCALE_DOWNLOAD_URL)}
className="inline-flex items-center gap-1 font-medium text-foreground underline-offset-2 hover:underline"
>
Tailscale
<ExternalLink className="size-3" />
</button>{' '}
on your computer and phone.
</li>
<li>Sign in to the same tailnet on both devices.</li>
<li>
In this Network Interface menu, choose the Tailscale address, usually a 100.x.y.z
IP.
</li>
<li>Regenerate the QR code and scan it from the Orca mobile app.</li>
</ol>
</AccordionContent>
</AccordionItem>
</Accordion>
</div>
{/* QR code display */}

View File

@ -0,0 +1,114 @@
import type { GlobalSettings } from '../../../../shared/types'
import { Label } from '../ui/label'
import { SearchableSetting } from './SearchableSetting'
import { matchesSettingsSearch, type SettingsSearchEntry } from './settings-search'
import { useAppStore } from '../../store'
import { MobilePane, MOBILE_PANE_SEARCH_ENTRIES } from './MobilePane'
const ORCA_IOS_APP_STORE_URL = 'https://apps.apple.com/app/orca-ide/id6766130217'
const ORCA_ANDROID_RELEASE_URL = 'https://github.com/stablyai/orca/releases/tag/mobile-v0.0.2'
const MOBILE_ENABLE_SEARCH_ENTRY: SettingsSearchEntry = {
title: 'Mobile',
description: 'Pair a mobile device to control Orca remotely.',
keywords: [
'mobile',
'phone',
'pair',
'qr',
'code',
'scan',
'remote',
'android',
'apk',
'beta',
'experimental'
]
}
export const MOBILE_SETTINGS_PANE_SEARCH_ENTRIES: SettingsSearchEntry[] = [
MOBILE_ENABLE_SEARCH_ENTRY,
...MOBILE_PANE_SEARCH_ENTRIES
]
type MobileSettingsPaneProps = {
settings: GlobalSettings
updateSettings: (updates: Partial<GlobalSettings>) => void
}
export function MobileSettingsPane({
settings,
updateSettings
}: MobileSettingsPaneProps): React.JSX.Element {
const searchQuery = useAppStore((s) => s.settingsSearchQuery)
const showEnableSetting =
!settings.experimentalMobile || matchesSettingsSearch(searchQuery, [MOBILE_ENABLE_SEARCH_ENTRY])
return (
<div className="space-y-4">
{showEnableSetting ? (
<SearchableSetting
title="Mobile"
description="Pair a mobile device to control Orca remotely."
keywords={MOBILE_ENABLE_SEARCH_ENTRY.keywords}
className="space-y-3 px-1 py-2"
>
<div className="flex items-start justify-between gap-4">
<div className="min-w-0 shrink space-y-1.5">
<Label>Mobile</Label>
<p className="text-xs text-muted-foreground">
Control Orca from your phone by scanning a QR code. Beta / early preview &mdash;
expect bugs and breaking changes. Get the iOS app from the{' '}
<button
type="button"
onClick={() => void window.api.shell.openUrl(ORCA_IOS_APP_STORE_URL)}
className="cursor-pointer underline underline-offset-2 hover:text-foreground"
>
App Store
</button>{' '}
or the Android APK from{' '}
<button
type="button"
// Why: Android still ships from the current mobile release tag rather than
// the generic /releases page, which is dominated by desktop
// releases and forces the user to scroll. Update this URL
// when cutting a new mobile-v* tag.
onClick={() => void window.api.shell.openUrl(ORCA_ANDROID_RELEASE_URL)}
className="cursor-pointer underline underline-offset-2 hover:text-foreground"
>
GitHub Releases page
</button>
.
</p>
</div>
<button
type="button"
role="switch"
aria-checked={settings.experimentalMobile}
onClick={() =>
updateSettings({
experimentalMobile: !settings.experimentalMobile
})
}
className={`relative inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border border-transparent transition-colors ${
settings.experimentalMobile ? 'bg-foreground' : 'bg-muted-foreground/30'
}`}
>
<span
className={`inline-block h-3.5 w-3.5 transform rounded-full bg-background shadow-sm transition-transform ${
settings.experimentalMobile ? 'translate-x-4' : 'translate-x-0.5'
}`}
/>
</button>
</div>
</SearchableSetting>
) : null}
{settings.experimentalMobile ? (
<div className="rounded-xl border border-border/60 bg-card/50 p-4">
<MobilePane />
</div>
) : null}
</div>
)
}

View File

@ -15,6 +15,7 @@ import {
Palette,
Server,
SlidersHorizontal,
Smartphone,
Blocks,
SquareTerminal,
UserCog
@ -51,6 +52,7 @@ import {
DEVELOPER_PERMISSIONS_PANE_SEARCH_ENTRIES
} from './DeveloperPermissionsPane'
import { ComputerUsePane, COMPUTER_USE_PANE_SEARCH_ENTRIES } from './ComputerUsePane'
import { MobileSettingsPane, MOBILE_SETTINGS_PANE_SEARCH_ENTRIES } from './MobileSettingsPane'
import { PrivacyPane } from './PrivacyPane'
import { PRIVACY_PANE_SEARCH_ENTRIES } from './privacy-search'
import { SettingsSidebar } from './SettingsSidebar'
@ -431,6 +433,14 @@ function Settings(): React.JSX.Element {
icon: Bell,
searchEntries: NOTIFICATIONS_PANE_SEARCH_ENTRIES
},
{
id: 'mobile',
title: 'Mobile',
description: 'Pair and manage mobile devices.',
icon: Smartphone,
searchEntries: MOBILE_SETTINGS_PANE_SEARCH_ENTRIES,
badge: 'Beta'
},
{
id: 'computer-use',
title: 'Computer Use',
@ -781,6 +791,16 @@ function Settings(): React.JSX.Element {
<NotificationsPane settings={settings} updateSettings={updateSettings} />
</SettingsSection>
<SettingsSection
id="mobile"
title="Mobile"
badge="Beta"
description="Pair and manage mobile devices."
searchEntries={MOBILE_SETTINGS_PANE_SEARCH_ENTRIES}
>
<MobileSettingsPane settings={settings} updateSettings={updateSettings} />
</SettingsSection>
<SettingsSection
id="computer-use"
title="Computer Use"

View File

@ -1,23 +1,6 @@
import type { SettingsSearchEntry } from './settings-search'
export const EXPERIMENTAL_PANE_SEARCH_ENTRIES: SettingsSearchEntry[] = [
{
title: 'Mobile Pairing',
description:
'Pair a mobile device to control Orca remotely. Experimental — requires the Orca mobile APK from GitHub Releases.',
keywords: [
'experimental',
'mobile',
'phone',
'pair',
'qr',
'code',
'scan',
'remote',
'android',
'apk'
]
},
{
title: 'Pet',
description: 'Floating animated pet in the bottom-right corner.',
@ -95,7 +78,6 @@ function findEntry(title: string): SettingsSearchEntry {
}
export const EXPERIMENTAL_SEARCH_ENTRY = {
mobile: findEntry('Mobile Pairing'),
pet: findEntry('Pet'),
orchestration: findEntry('Agent Orchestration'),
activity: findEntry('Activity Page'),