mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 08:02:28 +00:00
Split runtime environments pane layers (#17198)
* Split speech session lifecycle * Split terminal output scheduler pipeline * Split mobile browser pane modules * Prune resolved max-lines suppressions * Split pane tree equalization logic * Extract mobile troubleshoot screen styles * Split external automation manager * Split main window service attachments * Split hosted review creation checks * Split automation dispatch event handling * Split settings navigation metadata * Split daemon initialization lifecycle * Split GitLab item dialog * Split relay dispatcher layers * Split mobile host screen * Retarget mobile view settings source test * Split runtime file client layers * Split ports panel layers * Split runtime environments pane layers * Fix F3-speech for #17123 * Fix F1-cycle for #17131 * Fix F4-navtest for #17157 * Fix F2-allowlist for #17161
This commit is contained in:
@@ -44,7 +44,6 @@ inline src/renderer/src/components/editor/MarkdownPreview.tsx
|
||||
inline src/renderer/src/components/floating-terminal/FloatingTerminalPanel.tsx
|
||||
inline src/renderer/src/components/new-workspace/SmartWorkspaceNameField.tsx
|
||||
inline src/renderer/src/components/settings/AccountsPane.tsx
|
||||
inline src/renderer/src/components/settings/RuntimeEnvironmentsPane.tsx
|
||||
inline src/renderer/src/components/settings/Settings.tsx
|
||||
inline src/renderer/src/components/status-bar/ResourceUsageStatusSegment.tsx
|
||||
inline src/renderer/src/components/status-bar/StatusBar.tsx
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,211 @@
|
||||
import { ChevronDown, Loader2, RefreshCw } from 'lucide-react'
|
||||
import type { PublicKnownRuntimeEnvironment } from '../../../../shared/runtime-environments'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { Button } from '../ui/button'
|
||||
import { Label } from '../ui/label'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select'
|
||||
import {
|
||||
getActiveServerModeDescription,
|
||||
getHostModelCapabilitySummary,
|
||||
getRuntimeCapabilitiesSummary,
|
||||
type RuntimeHostDetails
|
||||
} from './runtime-environment-host-details'
|
||||
|
||||
type RuntimeActiveServerSectionProps = {
|
||||
visible: boolean
|
||||
advancedOpen: boolean
|
||||
allowLocalRuntime: boolean
|
||||
localRuntimeValue: string
|
||||
noRuntimeValue: string
|
||||
activeValue: string
|
||||
environments: PublicKnownRuntimeEnvironment[]
|
||||
detailsByEnvironmentId: Record<string, RuntimeHostDetails>
|
||||
isBusy: boolean
|
||||
isLoading: boolean
|
||||
onToggleAdvanced: () => void
|
||||
onValueChange: (value: string) => void
|
||||
onRefresh: () => void
|
||||
}
|
||||
|
||||
export function RuntimeActiveServerSection({
|
||||
visible,
|
||||
advancedOpen,
|
||||
allowLocalRuntime,
|
||||
localRuntimeValue,
|
||||
noRuntimeValue,
|
||||
activeValue,
|
||||
environments,
|
||||
detailsByEnvironmentId,
|
||||
isBusy,
|
||||
isLoading,
|
||||
onToggleAdvanced,
|
||||
onValueChange,
|
||||
onRefresh
|
||||
}: RuntimeActiveServerSectionProps): React.JSX.Element {
|
||||
return (
|
||||
<div data-settings-section="default-runtime" className={!visible ? 'hidden' : undefined}>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={onToggleAdvanced}
|
||||
className="-ml-2 text-xs"
|
||||
aria-expanded={advancedOpen}
|
||||
aria-controls="runtime-server-advanced-content"
|
||||
>
|
||||
{translate('auto.components.settings.RuntimeEnvironmentsPane.advanced', 'Advanced')}
|
||||
<ChevronDown className={cn('size-4 transition-transform', advancedOpen && 'rotate-180')} />
|
||||
</Button>
|
||||
|
||||
<div
|
||||
id="runtime-server-advanced-content"
|
||||
className={cn(
|
||||
'grid overflow-hidden transition-[grid-template-rows] duration-200 ease-out',
|
||||
advancedOpen ? 'grid-rows-[1fr]' : 'grid-rows-[0fr]'
|
||||
)}
|
||||
aria-hidden={!advancedOpen}
|
||||
inert={!advancedOpen}
|
||||
>
|
||||
<div className="min-h-0">
|
||||
<div
|
||||
className={cn(
|
||||
'space-y-2 px-1 pt-3 pb-1 transition-[opacity,transform] duration-150 ease-out',
|
||||
advancedOpen
|
||||
? 'translate-y-0 opacity-100 delay-200'
|
||||
: '-translate-y-1 opacity-0 delay-0'
|
||||
)}
|
||||
>
|
||||
<div className="space-y-1">
|
||||
<Label id="runtime-active-server-label">
|
||||
{translate(
|
||||
'auto.components.settings.RuntimeEnvironmentsPane.64b6bea541',
|
||||
'Active Server'
|
||||
)}
|
||||
</Label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{getActiveServerModeDescription(allowLocalRuntime)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Select
|
||||
value={activeValue}
|
||||
onValueChange={(value) => {
|
||||
if (value !== activeValue) {
|
||||
onValueChange(value)
|
||||
}
|
||||
}}
|
||||
disabled={isBusy}
|
||||
>
|
||||
<SelectTrigger
|
||||
size="sm"
|
||||
className="min-w-[260px]"
|
||||
aria-labelledby="runtime-active-server-label"
|
||||
>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{allowLocalRuntime ? (
|
||||
<SelectItem value={localRuntimeValue}>
|
||||
{translate(
|
||||
'auto.components.settings.RuntimeEnvironmentsPane.78692becbd',
|
||||
'Local desktop'
|
||||
)}
|
||||
</SelectItem>
|
||||
) : environments.length === 0 ? (
|
||||
<SelectItem value={noRuntimeValue} disabled>
|
||||
{translate(
|
||||
'auto.components.settings.RuntimeEnvironmentsPane.b07070ed3c',
|
||||
'No server connected'
|
||||
)}
|
||||
</SelectItem>
|
||||
) : null}
|
||||
{environments.map((environment) => (
|
||||
<SelectItem key={environment.id} value={environment.id}>
|
||||
{environment.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="icon-sm"
|
||||
aria-label={translate(
|
||||
'auto.components.settings.RuntimeEnvironmentsPane.6ce4664003',
|
||||
'Refresh servers'
|
||||
)}
|
||||
title={translate(
|
||||
'auto.components.settings.RuntimeEnvironmentsPane.6ce4664003',
|
||||
'Refresh servers'
|
||||
)}
|
||||
onClick={onRefresh}
|
||||
disabled={isLoading || isBusy}
|
||||
>
|
||||
{isLoading ? <Loader2 className="animate-spin" /> : <RefreshCw />}
|
||||
</Button>
|
||||
</div>
|
||||
{environments.length > 0 ? (
|
||||
<div className="space-y-2 pt-2">
|
||||
<div className="text-xs font-medium">
|
||||
{translate(
|
||||
'auto.components.settings.RuntimeEnvironmentsPane.serverDetails',
|
||||
'Server details'
|
||||
)}
|
||||
</div>
|
||||
<div className="space-y-1 rounded-lg border border-border/50 bg-card/30 p-2">
|
||||
{environments.map((environment) => {
|
||||
const details = detailsByEnvironmentId[environment.id]
|
||||
return (
|
||||
<div
|
||||
key={environment.id}
|
||||
className="grid gap-1 rounded-md px-2 py-1.5 text-[11px] text-muted-foreground sm:grid-cols-[minmax(0,9rem)_minmax(0,1fr)]"
|
||||
>
|
||||
<div className="truncate font-medium text-foreground">
|
||||
{environment.name}
|
||||
</div>
|
||||
<div className="min-w-0 space-y-0.5">
|
||||
<div className="truncate font-mono">
|
||||
{environment.endpoints[0]?.endpoint ??
|
||||
translate(
|
||||
'auto.components.settings.RuntimeEnvironmentsPane.6ef71985da',
|
||||
'No endpoint'
|
||||
)}
|
||||
</div>
|
||||
{details?.runtimeStatus ? (
|
||||
<div className="truncate">
|
||||
{translate(
|
||||
'auto.components.settings.RuntimeEnvironmentsPane.0ef838094a',
|
||||
'Protocol {{value0}}',
|
||||
{
|
||||
value0:
|
||||
details.runtimeStatus?.runtimeProtocolVersion ??
|
||||
details.runtimeStatus?.protocolVersion ??
|
||||
0
|
||||
}
|
||||
)}
|
||||
{details.runtimeStatus.hostPlatform
|
||||
? ` · ${details.runtimeStatus.hostPlatform}`
|
||||
: ''}
|
||||
{' · '}
|
||||
{getRuntimeCapabilitiesSummary(details.runtimeStatus)}
|
||||
</div>
|
||||
) : null}
|
||||
{getHostModelCapabilitySummary(details?.runtimeStatus) ? (
|
||||
<div className="truncate">
|
||||
{getHostModelCapabilitySummary(details?.runtimeStatus)}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
import { Loader2, Trash2 } from 'lucide-react'
|
||||
import type { PublicKnownRuntimeEnvironment } from '../../../../shared/runtime-environments'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { Button } from '../ui/button'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle
|
||||
} from '../ui/dialog'
|
||||
|
||||
export function RuntimeEnvironmentSwitchDialog({
|
||||
pendingSwitchValue,
|
||||
switchingValue,
|
||||
switchError,
|
||||
getEnvironmentLabel,
|
||||
onOpenChange,
|
||||
onCancel,
|
||||
onConfirm
|
||||
}: {
|
||||
pendingSwitchValue: string | null
|
||||
switchingValue: string | null
|
||||
switchError: string | null
|
||||
getEnvironmentLabel: (value: string) => string
|
||||
onOpenChange: (open: boolean) => void
|
||||
onCancel: () => void
|
||||
onConfirm: () => void
|
||||
}): React.JSX.Element {
|
||||
return (
|
||||
<Dialog open={pendingSwitchValue !== null} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-sm sm:max-w-sm" showCloseButton={false}>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-sm">
|
||||
{translate(
|
||||
'auto.components.settings.RuntimeEnvironmentsPane.d570c35a99',
|
||||
'Switch Server'
|
||||
)}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
{translate(
|
||||
'auto.components.settings.RuntimeEnvironmentsPane.b2290ed203',
|
||||
'Orca will focus this host and load its projects. Existing terminals and browser tabs on other hosts stay alive.'
|
||||
)}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
{pendingSwitchValue ? (
|
||||
<div className="rounded-md border border-border/70 bg-muted/35 px-3 py-2 text-xs">
|
||||
<div className="text-muted-foreground">
|
||||
{translate(
|
||||
'auto.components.settings.RuntimeEnvironmentsPane.05e0fc3ebf',
|
||||
'Switch to'
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-0.5 truncate font-medium">
|
||||
{getEnvironmentLabel(pendingSwitchValue)}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
{switchError ? <p className="text-sm text-destructive">{switchError}</p> : null}
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={onCancel} disabled={switchingValue !== null}>
|
||||
{translate('auto.components.settings.RuntimeEnvironmentsPane.af53761f31', 'Cancel')}
|
||||
</Button>
|
||||
<Button onClick={onConfirm} disabled={switchingValue !== null}>
|
||||
{switchingValue !== null ? <Loader2 className="animate-spin" /> : null}
|
||||
{translate('auto.components.settings.RuntimeEnvironmentsPane.d2e00809e4', 'Switch')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
export function RuntimeEnvironmentRemoveDialog({
|
||||
pendingRemove,
|
||||
removingId,
|
||||
removeError,
|
||||
removingActiveServer,
|
||||
onOpenChange,
|
||||
onCancel,
|
||||
onConfirm
|
||||
}: {
|
||||
pendingRemove: PublicKnownRuntimeEnvironment | null
|
||||
removingId: string | null
|
||||
removeError: string | null
|
||||
removingActiveServer: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
onCancel: () => void
|
||||
onConfirm: () => void
|
||||
}): React.JSX.Element {
|
||||
return (
|
||||
<Dialog open={pendingRemove !== null} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-sm sm:max-w-sm" showCloseButton={false}>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-sm">
|
||||
{translate(
|
||||
'auto.components.settings.RuntimeEnvironmentsPane.bb90dd6487',
|
||||
'Remove Server'
|
||||
)}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
{removingActiveServer
|
||||
? translate(
|
||||
'auto.components.settings.RuntimeEnvironmentsPane.removeActiveServerDescription',
|
||||
'Choose another Active Server in Advanced before removing this server. Existing host sessions are left alone.'
|
||||
)
|
||||
: translate(
|
||||
'auto.components.settings.RuntimeEnvironmentsPane.ed3e3f069d',
|
||||
'This removes the saved server from Orca. It does not change the active server.'
|
||||
)}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
{pendingRemove ? (
|
||||
<div className="rounded-md border border-border/70 bg-muted/35 px-3 py-2 text-xs">
|
||||
<div className="truncate font-medium">{pendingRemove.name}</div>
|
||||
<div className="mt-0.5 truncate font-mono text-muted-foreground">
|
||||
{pendingRemove.endpoints[0]?.endpoint ??
|
||||
translate(
|
||||
'auto.components.settings.RuntimeEnvironmentsPane.6ef71985da',
|
||||
'No endpoint'
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
{removeError ? <p className="text-sm text-destructive">{removeError}</p> : null}
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={onCancel} disabled={removingId !== null}>
|
||||
{translate('auto.components.settings.RuntimeEnvironmentsPane.af53761f31', 'Cancel')}
|
||||
</Button>
|
||||
<Button variant="destructive" onClick={onConfirm} disabled={removingId !== null}>
|
||||
{removingId !== null ? <Loader2 className="animate-spin" /> : <Trash2 />}
|
||||
{translate('auto.components.settings.RuntimeEnvironmentsPane.d25f0688b1', 'Remove')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import {
|
||||
describeRuntimeCompatBlock,
|
||||
evaluateRuntimeCompat,
|
||||
type RuntimeCompatVerdict
|
||||
} from '../../../../shared/protocol-compat'
|
||||
import {
|
||||
MIN_COMPATIBLE_RUNTIME_SERVER_VERSION,
|
||||
PROJECT_HOST_SETUP_RUNTIME_CAPABILITY,
|
||||
RUNTIME_PROTOCOL_VERSION,
|
||||
TASK_SOURCE_CONTEXT_RUNTIME_CAPABILITY,
|
||||
WORKSPACE_RUN_CONTEXT_RUNTIME_CAPABILITY
|
||||
} from '../../../../shared/protocol-version'
|
||||
import type { RuntimeStatus } from '../../../../shared/runtime-types'
|
||||
|
||||
export type RuntimeHostDetails = {
|
||||
status: 'loading' | 'ready' | 'error'
|
||||
runtimeStatus: RuntimeStatus | null
|
||||
compatibility: RuntimeCompatVerdict | null
|
||||
error: string | null
|
||||
}
|
||||
|
||||
export function evaluateHostDetails(status: RuntimeStatus): RuntimeCompatVerdict {
|
||||
return evaluateRuntimeCompat({
|
||||
clientProtocolVersion: RUNTIME_PROTOCOL_VERSION,
|
||||
minCompatibleServerProtocolVersion: MIN_COMPATIBLE_RUNTIME_SERVER_VERSION,
|
||||
serverProtocolVersion: status.runtimeProtocolVersion ?? status.protocolVersion,
|
||||
serverMinCompatibleClientProtocolVersion:
|
||||
status.minCompatibleRuntimeClientVersion ?? status.minCompatibleMobileVersion
|
||||
})
|
||||
}
|
||||
|
||||
export function getHostDetailsSummary(details: RuntimeHostDetails | undefined): string {
|
||||
if (!details || details.status === 'loading') {
|
||||
return translate('auto.components.settings.RuntimeEnvironmentsPane.5120beaac6', 'Checking…')
|
||||
}
|
||||
if (details.status === 'error') {
|
||||
return translate(
|
||||
'auto.components.settings.RuntimeEnvironmentsPane.c8791efc45',
|
||||
'Status unavailable'
|
||||
)
|
||||
}
|
||||
if (details.compatibility?.kind === 'blocked') {
|
||||
return details.compatibility.reason === 'client-too-old'
|
||||
? translate('auto.components.settings.RuntimeEnvironmentsPane.62ac182a27', 'Update client')
|
||||
: translate('auto.components.settings.RuntimeEnvironmentsPane.86ed75bec8', 'Update server')
|
||||
}
|
||||
return translate('auto.components.settings.RuntimeEnvironmentsPane.9a91c4a0eb', 'Compatible')
|
||||
}
|
||||
|
||||
export function getHostDetailsDescription(details: RuntimeHostDetails | undefined): string | null {
|
||||
if (!details || details.status === 'loading') {
|
||||
return null
|
||||
}
|
||||
if (details.status === 'error') {
|
||||
return details.error
|
||||
}
|
||||
if (details.compatibility?.kind === 'blocked') {
|
||||
return describeRuntimeCompatBlock(details.compatibility)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export function getRuntimeCapabilitiesSummary(status: RuntimeStatus | null | undefined): string {
|
||||
const capabilities = status?.capabilities ?? []
|
||||
if (capabilities.length === 0) {
|
||||
return translate(
|
||||
'auto.components.settings.RuntimeEnvironmentsPane.4b5c6d7e8f',
|
||||
'No capabilities reported'
|
||||
)
|
||||
}
|
||||
const visibleCapabilities = capabilities.slice(0, 3).join(', ')
|
||||
const hiddenCount = capabilities.length - 3
|
||||
return hiddenCount > 0 ? `${visibleCapabilities} +${hiddenCount}` : visibleCapabilities
|
||||
}
|
||||
|
||||
export function getHostModelCapabilitySummary(
|
||||
status: RuntimeStatus | null | undefined
|
||||
): string | null {
|
||||
if (!status) {
|
||||
return null
|
||||
}
|
||||
const capabilities = status.capabilities
|
||||
if (!capabilities) {
|
||||
return translate(
|
||||
'auto.components.settings.RuntimeEnvironmentsPane.hostModelCapabilityUnknown',
|
||||
'Host model support: checking server capabilities'
|
||||
)
|
||||
}
|
||||
const missing = [
|
||||
PROJECT_HOST_SETUP_RUNTIME_CAPABILITY,
|
||||
TASK_SOURCE_CONTEXT_RUNTIME_CAPABILITY,
|
||||
WORKSPACE_RUN_CONTEXT_RUNTIME_CAPABILITY
|
||||
].filter((capability) => !capabilities.includes(capability))
|
||||
if (missing.length === 0) {
|
||||
return translate(
|
||||
'auto.components.settings.RuntimeEnvironmentsPane.hostModelCapabilitySupported',
|
||||
'Host model support: ready'
|
||||
)
|
||||
}
|
||||
const missingLabels = missing.map(getHostModelCapabilityLabel)
|
||||
return translate(
|
||||
'auto.components.settings.RuntimeEnvironmentsPane.hostModelCapabilityMissing',
|
||||
'Host model support: update server for {{value0}}',
|
||||
{ value0: missingLabels.join(', ') }
|
||||
)
|
||||
}
|
||||
|
||||
function getHostModelCapabilityLabel(capability: string): string {
|
||||
switch (capability) {
|
||||
case PROJECT_HOST_SETUP_RUNTIME_CAPABILITY:
|
||||
return translate(
|
||||
'auto.components.settings.RuntimeEnvironmentsPane.hostModelCapabilityProjectSetup',
|
||||
'project setup'
|
||||
)
|
||||
case TASK_SOURCE_CONTEXT_RUNTIME_CAPABILITY:
|
||||
return translate(
|
||||
'auto.components.settings.RuntimeEnvironmentsPane.hostModelCapabilityTaskSourceContext',
|
||||
'task source context'
|
||||
)
|
||||
case WORKSPACE_RUN_CONTEXT_RUNTIME_CAPABILITY:
|
||||
return translate(
|
||||
'auto.components.settings.RuntimeEnvironmentsPane.hostModelCapabilityWorkspaceRunContext',
|
||||
'workspace run context'
|
||||
)
|
||||
default:
|
||||
return capability
|
||||
}
|
||||
}
|
||||
|
||||
export function getActiveServerModeDescription(allowLocalRuntime: boolean): string {
|
||||
return allowLocalRuntime
|
||||
? translate(
|
||||
'auto.components.settings.RuntimeEnvironmentsPane.3f67e8078a',
|
||||
'Use this computer by default. Choose a saved server only when you want supported projects, files, terminals, provider checks, and browser/mobile handoff to run through that server.'
|
||||
)
|
||||
: translate(
|
||||
'auto.components.settings.RuntimeEnvironmentsPane.2c85efb3e8',
|
||||
'Selecting a saved server makes this browser use that paired Orca runtime as its default Host.'
|
||||
)
|
||||
}
|
||||
|
||||
export function isRuntimeEnvironmentRemovalBlocked(
|
||||
activeRuntimeEnvironmentId: string | null | undefined,
|
||||
environmentId: string
|
||||
): boolean {
|
||||
return activeRuntimeEnvironmentId === environmentId
|
||||
}
|
||||
|
||||
export type RuntimeServerConnectionState = 'connected' | 'checking' | 'disconnected'
|
||||
|
||||
export function getRuntimeServerConnectionState(
|
||||
details: RuntimeHostDetails | undefined
|
||||
): RuntimeServerConnectionState {
|
||||
if (!details || details.status === 'loading') {
|
||||
return 'checking'
|
||||
}
|
||||
if (details.status !== 'ready' || details.compatibility?.kind === 'blocked') {
|
||||
return 'disconnected'
|
||||
}
|
||||
// Why: an attached, reachable, compatible host is "Connected" (and exposes
|
||||
// Disconnect). Whether it is the default *active* server is a separate concept,
|
||||
// surfaced by the Advanced > Active Server selector and the row's help text —
|
||||
// it must not change this connection label, or the dot/label/button disagree.
|
||||
return 'connected'
|
||||
}
|
||||
|
||||
export function getRuntimeServerConnectionLabel(state: RuntimeServerConnectionState): string {
|
||||
switch (state) {
|
||||
case 'connected':
|
||||
return translate(
|
||||
'auto.components.settings.RuntimeEnvironmentsPane.serverConnected',
|
||||
'Connected'
|
||||
)
|
||||
case 'checking':
|
||||
return translate(
|
||||
'auto.components.settings.RuntimeEnvironmentsPane.serverChecking',
|
||||
'Checking…'
|
||||
)
|
||||
case 'disconnected':
|
||||
return translate(
|
||||
'auto.components.settings.RuntimeEnvironmentsPane.serverDisconnected',
|
||||
'Disconnected'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export function getRuntimeServerDotClass(state: RuntimeServerConnectionState): string {
|
||||
switch (state) {
|
||||
case 'connected':
|
||||
return 'bg-emerald-500'
|
||||
case 'checking':
|
||||
return 'bg-yellow-500'
|
||||
case 'disconnected':
|
||||
return 'bg-muted-foreground/40'
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export const LOCAL_RUNTIME_VALUE = '__local__'
|
||||
export const NO_RUNTIME_VALUE = '__none__'
|
||||
@@ -0,0 +1,194 @@
|
||||
import { AlertTriangle, Loader2, Server, ServerOff, Trash2 } from 'lucide-react'
|
||||
import type { PublicKnownRuntimeEnvironment } from '../../../../shared/runtime-environments'
|
||||
import type { RemoteServerUpdateEntry } from '@/runtime/remote-server-update-coordinator'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { Button } from '../ui/button'
|
||||
import {
|
||||
getHostDetailsDescription,
|
||||
getHostDetailsSummary,
|
||||
getRuntimeServerConnectionLabel,
|
||||
getRuntimeServerConnectionState,
|
||||
getRuntimeServerDotClass,
|
||||
type RuntimeHostDetails
|
||||
} from './runtime-environment-host-details'
|
||||
import {
|
||||
getRemoteServerManualUpdateHelp,
|
||||
RemoteServerUpdateStatus
|
||||
} from './RemoteServerUpdateStatus'
|
||||
|
||||
type RuntimeServerRowProps = {
|
||||
environment: PublicKnownRuntimeEnvironment
|
||||
details: RuntimeHostDetails | undefined
|
||||
isActive: boolean
|
||||
remoteUpdate: RemoteServerUpdateEntry | undefined
|
||||
remoteServerUpdatesRunning: boolean
|
||||
connecting: boolean
|
||||
switching: boolean
|
||||
disconnecting: boolean
|
||||
removing: boolean
|
||||
isBusy: boolean
|
||||
onOpenUpdate: () => void
|
||||
onDisconnect: (environment: PublicKnownRuntimeEnvironment) => void
|
||||
onConnect: (environment: PublicKnownRuntimeEnvironment) => void
|
||||
onRemove: (environment: PublicKnownRuntimeEnvironment) => void
|
||||
}
|
||||
|
||||
export function RuntimeServerRow({
|
||||
environment,
|
||||
details,
|
||||
isActive,
|
||||
remoteUpdate,
|
||||
remoteServerUpdatesRunning,
|
||||
connecting,
|
||||
switching,
|
||||
disconnecting,
|
||||
removing,
|
||||
isBusy,
|
||||
onOpenUpdate,
|
||||
onDisconnect,
|
||||
onConnect,
|
||||
onRemove
|
||||
}: RuntimeServerRowProps): React.JSX.Element {
|
||||
const detailsDescription = getHostDetailsDescription(details)
|
||||
const connectionState = getRuntimeServerConnectionState(details)
|
||||
// A connected host exposes Disconnect; otherwise Connect.
|
||||
const isReachable = connectionState === 'connected'
|
||||
const actionBusy = connecting || switching || disconnecting || removing
|
||||
|
||||
return (
|
||||
<div data-settings-section={environment.id} className="flex items-center gap-3 px-4 py-3">
|
||||
<Server className="size-4 shrink-0 text-muted-foreground" />
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
<div className="truncate text-sm font-medium">{environment.name}</div>
|
||||
<span
|
||||
className={cn(
|
||||
'size-2 shrink-0 rounded-full',
|
||||
getRuntimeServerDotClass(connectionState)
|
||||
)}
|
||||
/>
|
||||
<span className="text-[11px] text-muted-foreground">
|
||||
{getRuntimeServerConnectionLabel(connectionState)}
|
||||
</span>
|
||||
{details?.compatibility?.kind === 'blocked' ? (
|
||||
<AlertTriangle className="size-3.5 shrink-0 text-destructive" />
|
||||
) : details?.status === 'loading' ? (
|
||||
<Loader2 className="size-3.5 shrink-0 animate-spin text-muted-foreground" />
|
||||
) : null}
|
||||
</div>
|
||||
<p className="truncate text-xs text-muted-foreground">
|
||||
{environment.connectionDependency === 'ssh-tunnel'
|
||||
? translate(
|
||||
'auto.components.settings.RuntimeEnvironmentsPane.sshTunnelRequired',
|
||||
'SSH tunnel required'
|
||||
)
|
||||
: isActive
|
||||
? translate(
|
||||
'auto.components.settings.RuntimeEnvironmentsPane.activeServerRowHelp',
|
||||
'Active server for server-routed projects, terminals, and provider checks.'
|
||||
)
|
||||
: getHostDetailsSummary(details)}
|
||||
</p>
|
||||
{detailsDescription ? (
|
||||
<p
|
||||
className={cn(
|
||||
'mt-0.5 truncate text-xs',
|
||||
details?.compatibility?.kind === 'blocked'
|
||||
? 'text-destructive'
|
||||
: 'text-muted-foreground'
|
||||
)}
|
||||
>
|
||||
{detailsDescription}
|
||||
</p>
|
||||
) : null}
|
||||
{remoteUpdate ? (
|
||||
<div className="mt-1 flex flex-wrap items-center gap-2">
|
||||
<span className="text-[11px] text-muted-foreground">
|
||||
{remoteUpdate.currentVersion
|
||||
? translate(
|
||||
'auto.components.settings.RuntimeEnvironmentsPane.orcaVersion',
|
||||
'Orca v{{value0}}',
|
||||
{ value0: remoteUpdate.currentVersion }
|
||||
)
|
||||
: translate(
|
||||
'auto.components.settings.RuntimeEnvironmentsPane.versionUnavailable',
|
||||
'Orca version unavailable'
|
||||
)}
|
||||
</span>
|
||||
<RemoteServerUpdateStatus entry={remoteUpdate} compact />
|
||||
</div>
|
||||
) : null}
|
||||
{remoteUpdate?.phase === 'manual' ? (
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
{getRemoteServerManualUpdateHelp(remoteUpdate)}
|
||||
</p>
|
||||
) : null}
|
||||
{remoteUpdate?.phase === 'failed' && remoteUpdate.error ? (
|
||||
<p className="mt-1 text-xs text-destructive">{remoteUpdate.error}</p>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="flex shrink-0 items-center gap-1">
|
||||
{remoteUpdate?.phase === 'available' || remoteUpdate?.phase === 'failed' ? (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
onClick={onOpenUpdate}
|
||||
disabled={remoteServerUpdatesRunning}
|
||||
>
|
||||
{translate('auto.components.settings.RuntimeEnvironmentsPane.updateServer', 'Update')}
|
||||
</Button>
|
||||
) : null}
|
||||
{isReachable ? (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
className="gap-1.5"
|
||||
onClick={() => onDisconnect(environment)}
|
||||
disabled={actionBusy}
|
||||
>
|
||||
{disconnecting ? (
|
||||
<Loader2 className="size-3 animate-spin" />
|
||||
) : (
|
||||
<ServerOff className="size-3" />
|
||||
)}
|
||||
{translate('auto.components.settings.RuntimeEnvironmentsPane.disconnect', 'Disconnect')}
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
className="gap-1.5"
|
||||
onClick={() => onConnect(environment)}
|
||||
disabled={actionBusy || connectionState === 'checking'}
|
||||
>
|
||||
{connecting ? (
|
||||
<Loader2 className="size-3 animate-spin" />
|
||||
) : (
|
||||
<Server className="size-3" />
|
||||
)}
|
||||
{translate('auto.components.settings.RuntimeEnvironmentsPane.connect', 'Connect')}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => onRemove(environment)}
|
||||
className="size-7 text-muted-foreground hover:text-red-400"
|
||||
disabled={isBusy}
|
||||
aria-label={translate(
|
||||
'auto.components.settings.RuntimeEnvironmentsPane.aeb26635d2',
|
||||
'Remove {{value0}}',
|
||||
{ value0: environment.name }
|
||||
)}
|
||||
>
|
||||
{removing ? <Loader2 className="size-3 animate-spin" /> : <Trash2 className="size-3" />}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
import { ChevronDown, Share2 } from 'lucide-react'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { Button } from '../ui/button'
|
||||
import { RuntimePairingUrlGenerator } from './RuntimePairingUrlGenerator'
|
||||
|
||||
export type RemoteServerWorkflow = 'connect' | 'cloud-vm' | 'share'
|
||||
|
||||
export function RuntimeServerWorkflowPicker({
|
||||
canGeneratePairingUrl,
|
||||
visibleWorkflow,
|
||||
onCloseAddServerForm,
|
||||
onWorkflowChange
|
||||
}: {
|
||||
canGeneratePairingUrl: boolean
|
||||
visibleWorkflow: RemoteServerWorkflow
|
||||
onCloseAddServerForm: () => void
|
||||
onWorkflowChange: (workflow: RemoteServerWorkflow) => void
|
||||
}): React.JSX.Element {
|
||||
return (
|
||||
<div
|
||||
role="group"
|
||||
aria-label={translate(
|
||||
'auto.components.settings.RuntimeEnvironmentsPane.workflow',
|
||||
'Remote server workflow'
|
||||
)}
|
||||
className={cn('grid gap-2 sm:grid-cols-2', canGeneratePairingUrl && 'sm:grid-cols-3')}
|
||||
>
|
||||
{(
|
||||
[
|
||||
[
|
||||
'connect',
|
||||
translate(
|
||||
'auto.components.settings.RuntimeEnvironmentsPane.connectWorkflow',
|
||||
'Connect to a host'
|
||||
),
|
||||
translate(
|
||||
'auto.components.settings.RuntimeEnvironmentsPane.connectWorkflowHelp',
|
||||
'This app joins another machine'
|
||||
)
|
||||
],
|
||||
[
|
||||
'share',
|
||||
translate(
|
||||
'auto.components.settings.RuntimeEnvironmentsPane.shareWorkflow',
|
||||
'Share this host'
|
||||
),
|
||||
translate(
|
||||
'auto.components.settings.RuntimeEnvironmentsPane.shareWorkflowHelp',
|
||||
'Other devices join this machine'
|
||||
)
|
||||
],
|
||||
[
|
||||
'cloud-vm',
|
||||
translate(
|
||||
'auto.components.settings.RuntimeEnvironmentsPane.cloudVmWorkflow',
|
||||
'Cloud VM'
|
||||
),
|
||||
translate(
|
||||
'auto.components.settings.RuntimeEnvironmentsPane.cloudVmWorkflowHelp',
|
||||
'Manage recipe-created cloud machines'
|
||||
)
|
||||
]
|
||||
] as const
|
||||
)
|
||||
.filter(([value]) => value !== 'share' || canGeneratePairingUrl)
|
||||
.map(([value, label, description]) => (
|
||||
<button
|
||||
key={value}
|
||||
type="button"
|
||||
aria-pressed={visibleWorkflow === value}
|
||||
onClick={() => {
|
||||
if (value !== 'connect') {
|
||||
onCloseAddServerForm()
|
||||
}
|
||||
onWorkflowChange(value)
|
||||
}}
|
||||
className={cn(
|
||||
'rounded-lg border p-3 text-left transition-colors',
|
||||
visibleWorkflow === value
|
||||
? 'border-ring bg-accent text-accent-foreground'
|
||||
: 'border-border hover:bg-accent'
|
||||
)}
|
||||
>
|
||||
<span className="block text-sm font-medium">{label}</span>
|
||||
<span
|
||||
className={cn(
|
||||
'mt-1 block text-xs',
|
||||
visibleWorkflow === value ? 'text-accent-foreground' : 'text-muted-foreground'
|
||||
)}
|
||||
>
|
||||
{description}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function RuntimeServerShareSection({
|
||||
shareServerFormOpen,
|
||||
onToggleShareServerForm
|
||||
}: {
|
||||
shareServerFormOpen: boolean
|
||||
onToggleShareServerForm: () => void
|
||||
}): React.JSX.Element {
|
||||
return (
|
||||
<div className="space-y-3 pt-2">
|
||||
<div className="space-y-0.5">
|
||||
<div className="text-sm font-medium">
|
||||
{translate(
|
||||
'auto.components.settings.RuntimeEnvironmentsPane.advertiseThisApp',
|
||||
'Advertise this app as a server'
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{translate(
|
||||
'auto.components.settings.RuntimeEnvironmentsPane.advertiseThisAppHelp',
|
||||
'Create access links for browsers, mobile clients, or another Orca client to connect back to this running app.'
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="overflow-hidden rounded-lg border border-border/50 bg-card/30">
|
||||
<div className="flex flex-wrap items-center justify-between gap-3 px-3 py-2.5">
|
||||
<div className="min-w-0 space-y-0.5">
|
||||
<div className="text-sm font-medium">
|
||||
{translate(
|
||||
'auto.components.settings.RuntimeEnvironmentsPane.6e1280ca55',
|
||||
'Share this Orca server'
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{translate(
|
||||
'auto.components.settings.RuntimeEnvironmentsPane.84b9b2be05',
|
||||
'Create a revocable access grant so a browser or another Orca client can connect.'
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="gap-1.5"
|
||||
onClick={onToggleShareServerForm}
|
||||
>
|
||||
<Share2 />
|
||||
{shareServerFormOpen
|
||||
? translate(
|
||||
'auto.components.settings.RuntimeEnvironmentsPane.54dee18f5c',
|
||||
'Hide Form'
|
||||
)
|
||||
: translate(
|
||||
'auto.components.settings.RuntimeEnvironmentsPane.3595fd1948',
|
||||
'New Link'
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="border-t border-border/40 px-3 py-3">
|
||||
<RuntimePairingUrlGenerator
|
||||
framed={false}
|
||||
showHeader={false}
|
||||
showGeneratorForm={shareServerFormOpen}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function RuntimeServerTroubleshooting(): React.JSX.Element {
|
||||
return (
|
||||
<details className="group rounded-lg border border-border/60">
|
||||
<summary className="flex cursor-pointer list-none items-center gap-2 p-4 text-sm font-medium">
|
||||
{translate(
|
||||
'auto.components.settings.RuntimeEnvironmentsPane.troubleshootWorkflow',
|
||||
'Connection troubleshooting'
|
||||
)}
|
||||
<ChevronDown className="ml-auto size-4 transition-transform group-open:rotate-180" />
|
||||
</summary>
|
||||
<div className="space-y-4 border-t border-border/50 p-4">
|
||||
<div className="space-y-1">
|
||||
<div className="text-sm font-medium">
|
||||
{translate(
|
||||
'auto.components.settings.RuntimeEnvironmentsPane.troubleshootTitle',
|
||||
'Create a new link on the other host'
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{translate(
|
||||
'auto.components.settings.RuntimeEnvironmentsPane.troubleshootDescription',
|
||||
'A link that uses 127.0.0.1 points back to the device opening it, not the computer that created it.'
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<ol className="ml-4 list-decimal space-y-1 text-xs text-muted-foreground">
|
||||
<li>
|
||||
{translate(
|
||||
'auto.components.settings.RuntimeEnvironmentsPane.troubleshootStepShare',
|
||||
'On the other computer, open Share this host.'
|
||||
)}
|
||||
</li>
|
||||
<li>
|
||||
{translate(
|
||||
'auto.components.settings.RuntimeEnvironmentsPane.troubleshootStepAddress',
|
||||
'Choose Another device and select its Tailscale or LAN address.'
|
||||
)}
|
||||
</li>
|
||||
<li>
|
||||
{translate(
|
||||
'auto.components.settings.RuntimeEnvironmentsPane.troubleshootStepRegenerate',
|
||||
'Generate a new access link and use only the newest link here.'
|
||||
)}
|
||||
</li>
|
||||
</ol>
|
||||
<div className="rounded-md border border-border/60 bg-muted/30 p-3 text-xs">
|
||||
{translate(
|
||||
'auto.components.settings.RuntimeEnvironmentsPane.troubleshootTunnel',
|
||||
'Using an SSH local forward? Return to Connect to a host, paste the loopback link, then enable “I am using an SSH tunnel” under Advanced.'
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
import { Loader2, Plus, RefreshCw } from 'lucide-react'
|
||||
import type { PublicKnownRuntimeEnvironment } from '../../../../shared/runtime-environments'
|
||||
import type { UpdateCheckOptions } from '../../../../shared/update-status-types'
|
||||
import type { RemoteServerUpdateEntry } from '@/runtime/remote-server-update-coordinator'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { getUpdateCheckClickOptions, getUpdateCheckHint } from '@/lib/update-check-click-options'
|
||||
import { Button } from '../ui/button'
|
||||
import type { RuntimeHostDetails } from './runtime-environment-host-details'
|
||||
import { RuntimeHostAccessForm, type RuntimeHostAccessFailure } from './RuntimeHostAccessForm'
|
||||
import { RuntimeServerRow } from './runtime-server-row'
|
||||
|
||||
type RuntimeServersConnectSectionProps = {
|
||||
visible: boolean
|
||||
environments: PublicKnownRuntimeEnvironment[]
|
||||
detailsByEnvironmentId: Record<string, RuntimeHostDetails>
|
||||
activeRuntimeEnvironmentId: string | null | undefined
|
||||
addServerFormOpen: boolean
|
||||
name: string
|
||||
pairingCode: string
|
||||
addServerFailure: RuntimeHostAccessFailure | null
|
||||
isBusy: boolean
|
||||
remoteServerUpdates: Map<string, RemoteServerUpdateEntry>
|
||||
remoteServerUpdatesChecking: boolean
|
||||
remoteServerUpdatesRunning: boolean
|
||||
connectingId: string | null
|
||||
switchingValue: string | null
|
||||
disconnectingId: string | null
|
||||
removingId: string | null
|
||||
onOpenAddServerForm: () => void
|
||||
onCloseAddServerForm: () => void
|
||||
onNameChange: (value: string) => void
|
||||
onPairingCodeChange: (value: string) => void
|
||||
onAddEnvironment: (allowLoopback: boolean) => void
|
||||
onOpenUpdateDialog: () => void
|
||||
refreshRemoteServerUpdates: (options?: UpdateCheckOptions) => Promise<void>
|
||||
onConnect: (environment: PublicKnownRuntimeEnvironment) => void
|
||||
onDisconnect: (environment: PublicKnownRuntimeEnvironment) => void
|
||||
onRemove: (environment: PublicKnownRuntimeEnvironment) => void
|
||||
}
|
||||
|
||||
export function RuntimeServersConnectSection({
|
||||
visible,
|
||||
environments,
|
||||
detailsByEnvironmentId,
|
||||
activeRuntimeEnvironmentId,
|
||||
addServerFormOpen,
|
||||
name,
|
||||
pairingCode,
|
||||
addServerFailure,
|
||||
isBusy,
|
||||
remoteServerUpdates,
|
||||
remoteServerUpdatesChecking,
|
||||
remoteServerUpdatesRunning,
|
||||
connectingId,
|
||||
switchingValue,
|
||||
disconnectingId,
|
||||
removingId,
|
||||
onOpenAddServerForm,
|
||||
onCloseAddServerForm,
|
||||
onNameChange,
|
||||
onPairingCodeChange,
|
||||
onAddEnvironment,
|
||||
onOpenUpdateDialog,
|
||||
refreshRemoteServerUpdates,
|
||||
onConnect,
|
||||
onDisconnect,
|
||||
onRemove
|
||||
}: RuntimeServersConnectSectionProps): React.JSX.Element {
|
||||
const updateCheckHint = getUpdateCheckHint()
|
||||
return (
|
||||
<div className={cn('space-y-3', !visible && 'hidden')}>
|
||||
<div
|
||||
data-settings-section="remote-server-updates"
|
||||
className="flex items-center justify-between gap-3"
|
||||
>
|
||||
<div className="min-w-0 space-y-0.5">
|
||||
<div className="text-sm font-medium">
|
||||
{translate(
|
||||
'auto.components.settings.RuntimeEnvironmentsPane.connectToRemoteServers',
|
||||
'Connect to remote servers'
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{translate(
|
||||
'auto.components.settings.RuntimeEnvironmentsPane.connectToRemoteServersHelp',
|
||||
'Pair another Orca runtime, then connect or disconnect it here.'
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex shrink-0 items-center gap-2">
|
||||
{environments.length > 0 ? (
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="gap-1.5"
|
||||
title={updateCheckHint}
|
||||
onClick={(event) => {
|
||||
onOpenUpdateDialog()
|
||||
void refreshRemoteServerUpdates(getUpdateCheckClickOptions(event))
|
||||
}}
|
||||
disabled={remoteServerUpdatesChecking && remoteServerUpdates.size === 0}
|
||||
>
|
||||
{remoteServerUpdatesChecking || remoteServerUpdatesRunning ? (
|
||||
<Loader2 className="animate-spin" />
|
||||
) : (
|
||||
<RefreshCw />
|
||||
)}
|
||||
{remoteServerUpdatesRunning
|
||||
? translate(
|
||||
'auto.components.settings.RuntimeEnvironmentsPane.updatingServers',
|
||||
'Updating servers…'
|
||||
)
|
||||
: translate(
|
||||
'auto.components.settings.RuntimeEnvironmentsPane.reviewServerUpdates',
|
||||
'Check for Server Updates'
|
||||
)}
|
||||
</Button>
|
||||
) : null}
|
||||
{addServerFormOpen ? null : (
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="gap-1.5"
|
||||
onClick={onOpenAddServerForm}
|
||||
disabled={isBusy}
|
||||
>
|
||||
<Plus />
|
||||
{translate(
|
||||
'auto.components.settings.RuntimeEnvironmentsPane.9bee6bbeeb',
|
||||
'Add Server'
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{addServerFormOpen ? (
|
||||
<RuntimeHostAccessForm
|
||||
name={name}
|
||||
accessLink={pairingCode}
|
||||
busy={isBusy}
|
||||
failure={addServerFailure}
|
||||
onNameChange={onNameChange}
|
||||
onAccessLinkChange={onPairingCodeChange}
|
||||
onCancel={onCloseAddServerForm}
|
||||
onSubmit={onAddEnvironment}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<div className="rounded-lg border border-border/50 bg-card/30">
|
||||
{environments.length === 0 ? (
|
||||
<div className="px-3 py-4 text-sm text-muted-foreground">
|
||||
{translate(
|
||||
'auto.components.settings.RuntimeEnvironmentsPane.9a3758d983',
|
||||
'No saved servers.'
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="divide-y divide-border/50">
|
||||
{environments.map((environment) => (
|
||||
<RuntimeServerRow
|
||||
key={environment.id}
|
||||
environment={environment}
|
||||
details={detailsByEnvironmentId[environment.id]}
|
||||
isActive={activeRuntimeEnvironmentId === environment.id}
|
||||
remoteUpdate={remoteServerUpdates.get(environment.id)}
|
||||
remoteServerUpdatesRunning={remoteServerUpdatesRunning}
|
||||
connecting={connectingId === environment.id}
|
||||
switching={switchingValue === environment.id}
|
||||
disconnecting={disconnectingId === environment.id}
|
||||
removing={removingId === environment.id}
|
||||
isBusy={isBusy}
|
||||
onOpenUpdate={onOpenUpdateDialog}
|
||||
onConnect={onConnect}
|
||||
onDisconnect={onDisconnect}
|
||||
onRemove={onRemove}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
import {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useState,
|
||||
type Dispatch,
|
||||
type MutableRefObject,
|
||||
type SetStateAction
|
||||
} from 'react'
|
||||
import { toast } from 'sonner'
|
||||
import { useMountedRef } from '@/hooks/useMountedRef'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { unwrapRuntimeRpcResult } from '@/runtime/runtime-rpc-client'
|
||||
import { useAppStore } from '@/store'
|
||||
import {
|
||||
isUserManagedRuntimeEnvironment,
|
||||
type PublicKnownRuntimeEnvironment
|
||||
} from '../../../../shared/runtime-environments'
|
||||
import type { RuntimeStatus } from '../../../../shared/runtime-types'
|
||||
import { evaluateHostDetails, type RuntimeHostDetails } from './runtime-environment-host-details'
|
||||
|
||||
type RuntimeEnvironmentCatalog = {
|
||||
environments: PublicKnownRuntimeEnvironment[]
|
||||
isLoading: boolean
|
||||
detailsByEnvironmentId: Record<string, RuntimeHostDetails>
|
||||
setDetailsByEnvironmentId: Dispatch<SetStateAction<Record<string, RuntimeHostDetails>>>
|
||||
mountedRef: MutableRefObject<boolean>
|
||||
loadEnvironments: (verified?: {
|
||||
environmentId: string
|
||||
runtimeStatus: RuntimeStatus
|
||||
}) => Promise<void>
|
||||
}
|
||||
|
||||
export function useRuntimeEnvironmentCatalog(): RuntimeEnvironmentCatalog {
|
||||
const [environments, setEnvironments] = useState<PublicKnownRuntimeEnvironment[]>([])
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [detailsByEnvironmentId, setDetailsByEnvironmentId] = useState<
|
||||
Record<string, RuntimeHostDetails>
|
||||
>({})
|
||||
const mountedRef = useMountedRef()
|
||||
|
||||
const loadEnvironments = useCallback(
|
||||
async (verified?: { environmentId: string; runtimeStatus: RuntimeStatus }): Promise<void> => {
|
||||
if (mountedRef.current) {
|
||||
setIsLoading(true)
|
||||
}
|
||||
try {
|
||||
const nextEnvironments = await window.api.runtimeEnvironments.list()
|
||||
const visibleEnvironments = nextEnvironments.filter(isUserManagedRuntimeEnvironment)
|
||||
// Why: drop store status for servers no longer saved so stale hosts don't
|
||||
// linger in the sidebar registry.
|
||||
useAppStore.getState().setRuntimeEnvironments(nextEnvironments)
|
||||
if (verified) {
|
||||
useAppStore.getState().setRuntimeEnvironmentStatus(verified.environmentId, {
|
||||
status: verified.runtimeStatus,
|
||||
checkedAt: Date.now()
|
||||
})
|
||||
}
|
||||
if (mountedRef.current) {
|
||||
setEnvironments(visibleEnvironments)
|
||||
setDetailsByEnvironmentId((current) => {
|
||||
const next: Record<string, RuntimeHostDetails> = {}
|
||||
for (const environment of visibleEnvironments) {
|
||||
next[environment.id] =
|
||||
verified?.environmentId === environment.id
|
||||
? {
|
||||
status: 'ready',
|
||||
runtimeStatus: verified.runtimeStatus,
|
||||
compatibility: evaluateHostDetails(verified.runtimeStatus),
|
||||
error: null
|
||||
}
|
||||
: (current[environment.id] ?? {
|
||||
status: 'loading',
|
||||
runtimeStatus: null,
|
||||
compatibility: null,
|
||||
error: null
|
||||
})
|
||||
}
|
||||
return next
|
||||
})
|
||||
}
|
||||
await Promise.allSettled(
|
||||
visibleEnvironments
|
||||
.filter((environment) => environment.id !== verified?.environmentId)
|
||||
.map(async (environment) => {
|
||||
try {
|
||||
const response = await window.api.runtimeEnvironments.getStatus({
|
||||
selector: environment.id,
|
||||
timeoutMs: 10_000
|
||||
})
|
||||
const runtimeStatus = unwrapRuntimeRpcResult<RuntimeStatus>(response)
|
||||
// Why: feed the live status into the store so sidebar host pickers
|
||||
// reflect manual refreshes, not just the settings pane.
|
||||
useAppStore.getState().setRuntimeEnvironmentStatus(environment.id, {
|
||||
status: runtimeStatus,
|
||||
checkedAt: Date.now()
|
||||
})
|
||||
if (!mountedRef.current) {
|
||||
return
|
||||
}
|
||||
setDetailsByEnvironmentId((current) => ({
|
||||
...current,
|
||||
[environment.id]: {
|
||||
status: 'ready',
|
||||
runtimeStatus,
|
||||
compatibility: evaluateHostDetails(runtimeStatus),
|
||||
error: null
|
||||
}
|
||||
}))
|
||||
} catch (error) {
|
||||
// Why: record the failed probe (null status) so the sidebar can
|
||||
// distinguish unreachable from never-checked.
|
||||
useAppStore.getState().setRuntimeEnvironmentStatus(environment.id, {
|
||||
status: null,
|
||||
checkedAt: Date.now()
|
||||
})
|
||||
if (!mountedRef.current) {
|
||||
return
|
||||
}
|
||||
setDetailsByEnvironmentId((current) => ({
|
||||
...current,
|
||||
[environment.id]: {
|
||||
status: 'error',
|
||||
runtimeStatus: null,
|
||||
compatibility: null,
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
}
|
||||
}))
|
||||
}
|
||||
})
|
||||
)
|
||||
} catch (error) {
|
||||
if (mountedRef.current) {
|
||||
toast.error(
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: translate(
|
||||
'auto.components.settings.RuntimeEnvironmentsPane.e6410d72c3',
|
||||
'Failed to load runtime environments.'
|
||||
)
|
||||
)
|
||||
}
|
||||
} finally {
|
||||
if (mountedRef.current) {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
},
|
||||
[mountedRef]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
void loadEnvironments()
|
||||
}, [loadEnvironments])
|
||||
|
||||
return {
|
||||
environments,
|
||||
isLoading,
|
||||
detailsByEnvironmentId,
|
||||
setDetailsByEnvironmentId,
|
||||
mountedRef,
|
||||
loadEnvironments
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
import { useState, type Dispatch, type MutableRefObject, type SetStateAction } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { unwrapRuntimeRpcResult } from '@/runtime/runtime-rpc-client'
|
||||
import { useAppStore } from '@/store'
|
||||
import { describeRuntimeCompatBlock } from '../../../../shared/protocol-compat'
|
||||
import type { PublicKnownRuntimeEnvironment } from '../../../../shared/runtime-environments'
|
||||
import type { RuntimeStatus } from '../../../../shared/runtime-types'
|
||||
import { evaluateHostDetails, type RuntimeHostDetails } from './runtime-environment-host-details'
|
||||
import { LOCAL_RUNTIME_VALUE, NO_RUNTIME_VALUE } from './runtime-environment-selection'
|
||||
|
||||
type RuntimeEnvironmentConnectionActionParams = {
|
||||
allowLocalRuntime: boolean
|
||||
mountedRef: MutableRefObject<boolean>
|
||||
setDetailsByEnvironmentId: Dispatch<SetStateAction<Record<string, RuntimeHostDetails>>>
|
||||
setActiveRuntimeEnvironmentPreference: (environmentId: string | null) => Promise<boolean>
|
||||
getEnvironmentLabel: (value: string) => string
|
||||
}
|
||||
|
||||
export function useRuntimeEnvironmentConnectionActions({
|
||||
allowLocalRuntime,
|
||||
mountedRef,
|
||||
setDetailsByEnvironmentId,
|
||||
setActiveRuntimeEnvironmentPreference,
|
||||
getEnvironmentLabel
|
||||
}: RuntimeEnvironmentConnectionActionParams) {
|
||||
const [connectingId, setConnectingId] = useState<string | null>(null)
|
||||
const [switchingValue, setSwitchingValue] = useState<string | null>(null)
|
||||
const [disconnectingId, setDisconnectingId] = useState<string | null>(null)
|
||||
const [switchError, setSwitchError] = useState<string | null>(null)
|
||||
|
||||
const disconnectEnvironment = async (
|
||||
environment: PublicKnownRuntimeEnvironment
|
||||
): Promise<boolean> => {
|
||||
setDisconnectingId(environment.id)
|
||||
setSwitchError(null)
|
||||
try {
|
||||
await window.api.runtimeEnvironments.disconnect({ selector: environment.id })
|
||||
// Why: disconnect is non-destructive; keep the saved server but show the
|
||||
// user that this live client is no longer attached to it.
|
||||
useAppStore.getState().setRuntimeEnvironmentStatus(
|
||||
environment.id,
|
||||
{
|
||||
status: null,
|
||||
checkedAt: Date.now()
|
||||
},
|
||||
{ suppressDisconnectToast: true }
|
||||
)
|
||||
if (mountedRef.current) {
|
||||
setDetailsByEnvironmentId((current) => ({
|
||||
...current,
|
||||
[environment.id]: {
|
||||
status: 'error',
|
||||
runtimeStatus: null,
|
||||
compatibility: null,
|
||||
error: null
|
||||
}
|
||||
}))
|
||||
toast.success(
|
||||
translate(
|
||||
'auto.components.settings.RuntimeEnvironmentsPane.disconnectedServer',
|
||||
'Disconnected from {{value0}}.',
|
||||
{ value0: environment.name }
|
||||
)
|
||||
)
|
||||
}
|
||||
return true
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Failed to disconnect server.'
|
||||
if (mountedRef.current) {
|
||||
setSwitchError(message)
|
||||
toast.error(message)
|
||||
}
|
||||
return false
|
||||
} finally {
|
||||
if (mountedRef.current) {
|
||||
setDisconnectingId(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const connectEnvironment = async (
|
||||
environment: PublicKnownRuntimeEnvironment
|
||||
): Promise<boolean> => {
|
||||
setConnectingId(environment.id)
|
||||
setSwitchError(null)
|
||||
try {
|
||||
const response = await window.api.runtimeEnvironments.connect({
|
||||
selector: environment.id,
|
||||
timeoutMs: 15_000
|
||||
})
|
||||
const runtimeStatus = unwrapRuntimeRpcResult<RuntimeStatus>(response)
|
||||
const compatibility = evaluateHostDetails(runtimeStatus)
|
||||
// Why: row Connect is reachability only. The Advanced selector is the
|
||||
// explicit default-host control and should be the only active-server path.
|
||||
useAppStore.getState().setRuntimeEnvironmentStatus(environment.id, {
|
||||
status: runtimeStatus,
|
||||
checkedAt: Date.now()
|
||||
})
|
||||
if (mountedRef.current) {
|
||||
setDetailsByEnvironmentId((current) => ({
|
||||
...current,
|
||||
[environment.id]: {
|
||||
status: 'ready',
|
||||
runtimeStatus,
|
||||
compatibility,
|
||||
error: null
|
||||
}
|
||||
}))
|
||||
}
|
||||
if (compatibility.kind === 'blocked') {
|
||||
const message = describeRuntimeCompatBlock(compatibility)
|
||||
if (mountedRef.current) {
|
||||
setSwitchError(message)
|
||||
toast.error(message)
|
||||
}
|
||||
return false
|
||||
}
|
||||
const store = useAppStore.getState()
|
||||
// Why: Connect is not the Active Server selector anymore, but connected
|
||||
// hosts should still contribute their projects/workspaces to the sidebar.
|
||||
const repos = await store.fetchRuntimeEnvironmentRepos(environment.id)
|
||||
await Promise.all(repos.map((repo) => useAppStore.getState().fetchWorktrees(repo.id)))
|
||||
await useAppStore.getState().fetchWorktreeLineage()
|
||||
if (mountedRef.current) {
|
||||
toast.success(
|
||||
translate(
|
||||
'auto.components.settings.RuntimeEnvironmentsPane.runtimeReachable',
|
||||
'{{value0}} is reachable.',
|
||||
{ value0: environment.name }
|
||||
)
|
||||
)
|
||||
}
|
||||
return true
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Failed to connect server.'
|
||||
useAppStore.getState().setRuntimeEnvironmentStatus(environment.id, {
|
||||
status: null,
|
||||
checkedAt: Date.now()
|
||||
})
|
||||
if (mountedRef.current) {
|
||||
setDetailsByEnvironmentId((current) => ({
|
||||
...current,
|
||||
[environment.id]: {
|
||||
status: 'error',
|
||||
runtimeStatus: null,
|
||||
compatibility: null,
|
||||
error: message
|
||||
}
|
||||
}))
|
||||
setSwitchError(message)
|
||||
toast.error(message)
|
||||
}
|
||||
return false
|
||||
} finally {
|
||||
if (mountedRef.current) {
|
||||
setConnectingId(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const switchToValue = async (value: string): Promise<boolean> => {
|
||||
if (value === NO_RUNTIME_VALUE) {
|
||||
return false
|
||||
}
|
||||
setSwitchingValue(value)
|
||||
setSwitchError(null)
|
||||
try {
|
||||
const switched = await setActiveRuntimeEnvironmentPreference(
|
||||
allowLocalRuntime && value === LOCAL_RUNTIME_VALUE ? null : value
|
||||
)
|
||||
if (switched) {
|
||||
if (mountedRef.current) {
|
||||
toast.success(
|
||||
translate(
|
||||
'auto.components.settings.RuntimeEnvironmentsPane.99ac81fb43',
|
||||
'Switched to {{value0}}.',
|
||||
{ value0: getEnvironmentLabel(value) }
|
||||
)
|
||||
)
|
||||
}
|
||||
return true
|
||||
}
|
||||
if (mountedRef.current) {
|
||||
setSwitchError('Could not switch servers. Fix the issue and try again.')
|
||||
}
|
||||
return false
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Failed to switch servers.'
|
||||
if (mountedRef.current) {
|
||||
setSwitchError(message)
|
||||
toast.error(message)
|
||||
}
|
||||
return false
|
||||
} finally {
|
||||
if (mountedRef.current) {
|
||||
setSwitchingValue(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
connectingId,
|
||||
switchingValue,
|
||||
disconnectingId,
|
||||
switchError,
|
||||
setSwitchError,
|
||||
connectEnvironment,
|
||||
disconnectEnvironment,
|
||||
switchToValue
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
import { useState, type Dispatch, type MutableRefObject, type SetStateAction } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import type { GlobalSettings } from '../../../../shared/global-settings-types'
|
||||
import type { PublicKnownRuntimeEnvironment } from '../../../../shared/runtime-environments'
|
||||
import type { RuntimeStatus } from '../../../../shared/runtime-types'
|
||||
import { isRuntimeEnvironmentRemovalBlocked } from './runtime-environment-host-details'
|
||||
import type { RuntimeHostAccessFailure } from './RuntimeHostAccessForm'
|
||||
|
||||
type RuntimeEnvironmentMutationActionParams = {
|
||||
environments: PublicKnownRuntimeEnvironment[]
|
||||
settings: GlobalSettings
|
||||
allowLocalRuntime: boolean
|
||||
mountedRef: MutableRefObject<boolean>
|
||||
setAddServerFormOpen: Dispatch<SetStateAction<boolean>>
|
||||
loadEnvironments: (verified?: {
|
||||
environmentId: string
|
||||
runtimeStatus: RuntimeStatus
|
||||
}) => Promise<void>
|
||||
connectEnvironment: (environment: PublicKnownRuntimeEnvironment) => Promise<boolean>
|
||||
}
|
||||
|
||||
export function useRuntimeEnvironmentMutationActions({
|
||||
environments,
|
||||
settings,
|
||||
allowLocalRuntime,
|
||||
mountedRef,
|
||||
setAddServerFormOpen,
|
||||
loadEnvironments,
|
||||
connectEnvironment
|
||||
}: RuntimeEnvironmentMutationActionParams) {
|
||||
const [isSaving, setIsSaving] = useState(false)
|
||||
const [removingId, setRemovingId] = useState<string | null>(null)
|
||||
const [removeError, setRemoveError] = useState<string | null>(null)
|
||||
const [name, setName] = useState('')
|
||||
const [pairingCode, setPairingCode] = useState('')
|
||||
const [addServerFailure, setAddServerFailure] = useState<RuntimeHostAccessFailure | null>(null)
|
||||
|
||||
const closeAddServerForm = (): void => {
|
||||
if (isSaving) {
|
||||
return
|
||||
}
|
||||
setAddServerFormOpen(false)
|
||||
setName('')
|
||||
setPairingCode('')
|
||||
setAddServerFailure(null)
|
||||
}
|
||||
|
||||
const addEnvironment = async (allowLoopback: boolean): Promise<void> => {
|
||||
const trimmedName = name.trim()
|
||||
const trimmedPairingCode = pairingCode.trim()
|
||||
if (!trimmedName || !trimmedPairingCode) {
|
||||
toast.error(
|
||||
translate(
|
||||
'auto.components.settings.RuntimeEnvironmentsPane.0c55a47480',
|
||||
'Name and pairing code are required.'
|
||||
)
|
||||
)
|
||||
return
|
||||
}
|
||||
const duplicate = environments.find(
|
||||
(environment) => environment.name.trim().toLowerCase() === trimmedName.toLowerCase()
|
||||
)
|
||||
if (duplicate) {
|
||||
toast.error(
|
||||
translate(
|
||||
'auto.components.settings.RuntimeEnvironmentsPane.5ef712f407',
|
||||
'A server named "{{value0}}" already exists.',
|
||||
{ value0: duplicate.name }
|
||||
)
|
||||
)
|
||||
return
|
||||
}
|
||||
setAddServerFailure(null)
|
||||
setIsSaving(true)
|
||||
try {
|
||||
const result = await window.api.runtimeEnvironments.verifyAndAddFromPairingCode({
|
||||
name: trimmedName,
|
||||
pairingCode: trimmedPairingCode,
|
||||
allowLoopback
|
||||
})
|
||||
if (!result.ok) {
|
||||
if (mountedRef.current) {
|
||||
setAddServerFailure({ kind: result.kind, message: result.message })
|
||||
}
|
||||
return
|
||||
}
|
||||
if (mountedRef.current) {
|
||||
setName('')
|
||||
setPairingCode('')
|
||||
}
|
||||
await loadEnvironments({
|
||||
environmentId: result.environment.id,
|
||||
runtimeStatus: result.runtimeStatus
|
||||
})
|
||||
if (!allowLocalRuntime) {
|
||||
const connected = await connectEnvironment(result.environment)
|
||||
if (!connected) {
|
||||
await window.api.runtimeEnvironments.remove({ selector: result.environment.id })
|
||||
await loadEnvironments()
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if (mountedRef.current) {
|
||||
toast.success(
|
||||
translate(
|
||||
'auto.components.settings.RuntimeEnvironmentsPane.7b5986c8df',
|
||||
'Connected to {{value0}}. Use Advanced > Active Server to make it the default.',
|
||||
{ value0: result.environment.name }
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
if (mountedRef.current) {
|
||||
setAddServerFormOpen(false)
|
||||
}
|
||||
} catch (error) {
|
||||
if (mountedRef.current) {
|
||||
toast.error(
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: translate(
|
||||
'auto.components.settings.RuntimeEnvironmentsPane.6cb6eae14f',
|
||||
'Failed to save runtime environment.'
|
||||
)
|
||||
)
|
||||
}
|
||||
} finally {
|
||||
if (mountedRef.current) {
|
||||
setIsSaving(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const removeEnvironment = async (
|
||||
environment: PublicKnownRuntimeEnvironment
|
||||
): Promise<boolean> => {
|
||||
setRemovingId(environment.id)
|
||||
setRemoveError(null)
|
||||
try {
|
||||
if (isRuntimeEnvironmentRemovalBlocked(settings.activeRuntimeEnvironmentId, environment.id)) {
|
||||
if (mountedRef.current) {
|
||||
setRemoveError(
|
||||
translate(
|
||||
'auto.components.settings.RuntimeEnvironmentsPane.removeActiveServerBlocked',
|
||||
'Choose another Active Server in Advanced before removing this server.'
|
||||
)
|
||||
)
|
||||
}
|
||||
return false
|
||||
}
|
||||
await window.api.runtimeEnvironments.remove({ selector: environment.id })
|
||||
await loadEnvironments()
|
||||
if (mountedRef.current) {
|
||||
toast.success(
|
||||
translate(
|
||||
'auto.components.settings.RuntimeEnvironmentsPane.b5b5114cb0',
|
||||
'Removed {{value0}}.',
|
||||
{ value0: environment.name }
|
||||
)
|
||||
)
|
||||
}
|
||||
return true
|
||||
} catch (error) {
|
||||
const message =
|
||||
error instanceof Error ? error.message : 'Failed to remove runtime environment.'
|
||||
if (mountedRef.current) {
|
||||
setRemoveError(message)
|
||||
toast.error(message)
|
||||
}
|
||||
return false
|
||||
} finally {
|
||||
if (mountedRef.current) {
|
||||
setRemovingId(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
isSaving,
|
||||
removingId,
|
||||
removeError,
|
||||
setRemoveError,
|
||||
name,
|
||||
setName,
|
||||
pairingCode,
|
||||
setPairingCode,
|
||||
addServerFailure,
|
||||
setAddServerFailure,
|
||||
closeAddServerForm,
|
||||
addEnvironment,
|
||||
removeEnvironment
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user