Files
openchamber/packages/ui/src/components/onboarding/RecoveryScreen.tsx
T
Bohdan Triapitsyn a0caec0984 feat(desktop): support remote-only startup
Allow Desktop to skip its in-process OpenChamber server with OPENCHAMBER_SKIP_LOCAL_SERVER=1 while continuing to load the packaged UI shell.

Carry local runtime availability through the boot contract so unavailable or unconfigured remotes enter a remote-only chooser instead of offering broken local recovery actions. The chooser can select saved instances, add a server by URL, or redeem an OpenChamber pairing link over direct or E2EE relay transports.

Keep additional windows, Mini Chat, background startup, and unreachable-host recovery functional without a local origin. Render boot and recovery surfaces with the active theme background rather than exposing the native vibrancy backing.

Document the environment variable and cover serverless boot routing plus malformed pairing imports with focused tests.
2026-07-21 21:11:13 +03:00

134 lines
4.3 KiB
TypeScript

import React from 'react';
import { isDesktopShell, restartDesktopApp } from '@/lib/desktop';
import { DesktopConnectionRecovery, type RecoveryVariant } from './DesktopConnectionRecovery';
import { RemoteConnectionForm } from './RemoteConnectionForm';
import { resolveRecoveryNextStep } from './desktopRecoveryRouting';
import { desktopHostsGet, desktopHostsSet } from '@/lib/desktopHosts';
import { runtimeFetch } from '@/lib/runtime-fetch';
type RecoveryScreenProps = {
/** Recovery variant */
variant: RecoveryVariant;
/** Host URL for recovery context */
hostUrl?: string;
/** Host label for recovery context */
hostLabel?: string;
/** Callback when user wants to retry */
onRetry?: () => void;
/** Callback when user chooses remote */
onChooseRemote?: () => void;
/** Whether to show the remote connection form */
showRemoteForm?: boolean;
/** Callback when closing remote form */
onCloseRemoteForm?: () => void;
/** Callback when switching to local from remote form */
onSwitchToLocalFromRemote?: () => void;
/** Callback when entering local setup */
onEnterLocalSetup?: () => void;
/** Whether retry action is in progress */
isRetrying?: boolean;
localAvailable?: boolean;
};
export function RecoveryScreen({
variant,
hostUrl,
hostLabel,
onRetry,
onChooseRemote,
showRemoteForm = false,
onCloseRemoteForm,
onSwitchToLocalFromRemote,
onEnterLocalSetup,
isRetrying = false,
localAvailable = true,
}: RecoveryScreenProps) {
// Persist the user's first choice (local or remote)
const persistFirstChoice = React.useCallback(async (choice: 'local' | 'remote') => {
if (!isDesktopShell()) return;
const config = await desktopHostsGet();
await desktopHostsSet({
...config,
// Only change defaultHostId when switching to local; remote keeps
// whatever was there (or null) until a successful connect.
...(choice === 'local' ? { defaultHostId: 'local' } : {}),
initialHostChoiceCompleted: true,
});
}, []);
const handleRecoveryRetry = React.useCallback(async () => {
// In desktop boot flow, restart the app so the native host can
// re-evaluate the boot outcome.
if (isDesktopShell()) {
await restartDesktopApp();
return;
}
await runtimeFetch('/api/config/reload', { method: 'POST' });
onRetry?.();
}, [onRetry]);
const handleRecoveryUseLocal = React.useCallback(async () => {
const step = resolveRecoveryNextStep(variant, 'use-local');
if (step.kind === 'local-setup') {
// local-unavailable + local → enter local-setup subflow without reload
onEnterLocalSetup?.();
return;
}
// switch-default-to-local → persist local choice and restart
await persistFirstChoice('local');
if (isDesktopShell()) {
await restartDesktopApp();
return;
}
window.location.reload();
}, [variant, persistFirstChoice, onEnterLocalSetup]);
const handleRecoveryUseRemote = React.useCallback(() => {
const step = resolveRecoveryNextStep(variant, 'use-remote');
if (step.kind === 'remote-form') {
onChooseRemote?.();
}
}, [variant, onChooseRemote]);
// Recovery mode — show recovery component first; only switch to remote form on explicit user action
if (showRemoteForm) {
// For remote-wrong-service, do NOT auto-populate the known bad URL
const prefillUrl = variant === 'remote-wrong-service' ? '' : (hostUrl || '');
const prefillLabel = variant === 'remote-wrong-service' ? '' : (hostLabel || '');
return (
<RemoteConnectionForm
onBack={onCloseRemoteForm || (() => onChooseRemote?.())}
initialUrl={prefillUrl}
initialLabel={prefillLabel}
isRecoveryMode={true}
showInstancePicker={!localAvailable}
onSwitchToLocal={localAvailable ? (onSwitchToLocalFromRemote || (() => {
persistFirstChoice('local').then(() => {
if (isDesktopShell()) {
restartDesktopApp();
} else {
onEnterLocalSetup?.();
}
});
})) : undefined}
/>
);
}
return (
<DesktopConnectionRecovery
variant={variant}
hostLabel={hostLabel}
hostUrl={hostUrl}
onRetry={handleRecoveryRetry}
onUseLocal={localAvailable ? handleRecoveryUseLocal : undefined}
onUseRemote={handleRecoveryUseRemote}
isRetrying={isRetrying}
/>
);
}