Decouple bundled UI from runtime API and add remote instance tooling (#1228)

Add a packaged-client runtime boundary so the shared UI can talk to local,
desktop, remote, and VS Code runtimes through the right transport instead of
assuming one same-origin web server.

Centralize OpenChamber-owned API access behind RuntimeAPIs, runtimeFetch, and
runtime URL helpers, while keeping official OpenCode traffic on the SDK path.
Support runtime switching, remote host selection, desktop client credentials,
and headless connection links for pairing packaged clients with remote
OpenChamber servers.

Harden the new auth model by moving long-lived client tokens out of browser
URLs, introducing short-lived scoped URL tokens for browser-owned transports,
restricting URL-token access to explicit readable/realtime routes, and making
client-token management session-scoped or self-scoped as appropriate.

Update browser-owned assets and preview proxy flows to work with the split
runtime model, including authenticated project icons, preview token propagation,
CSP-safe preview bridge injection, and preview proxy auth that survives
short-lived URL-token expiry.

Tighten Electron security boundaries for packaged clients by gating privileged
preload state to trusted origins and requiring explicit confirmation before
connect deep-links import or switch remote runtimes.

Also refresh agent guidance and project skills so future runtime/API, auth,
preview, UI, CLI, settings, locale, and drag-to-reorder work follows the new
architecture.
This commit is contained in:
Bohdan Triapitsyn
2026-06-02 00:43:05 +03:00
committed by GitHub
parent a4314c189b
commit 2031e3b4a8
282 changed files with 16524 additions and 4259 deletions
@@ -10,6 +10,7 @@ import { cn } from '@/lib/utils';
import { RemoteConnectionForm } from './RemoteConnectionForm';
import { desktopHostsGet, desktopHostsSet } from '@/lib/desktopHosts';
import { useI18n } from '@/lib/i18n';
import { runtimeFetch } from '@/lib/runtime-fetch';
const INSTALL_COMMAND = 'curl -fsSL https://opencode.ai/install | bash';
const DOCS_URL = 'https://opencode.ai/docs';
@@ -78,7 +79,7 @@ export function ChooserScreen({ onCliAvailable }: ChooserScreenProps) {
let cancelled = false;
void (async () => {
try {
const response = await fetch('/api/config/settings', { method: 'GET', headers: { Accept: 'application/json' } });
const response = await runtimeFetch('/api/config/settings', { method: 'GET', headers: { Accept: 'application/json' } });
if (!response.ok) return;
const data = (await response.json().catch(() => null)) as null | { opencodeBinary?: unknown };
if (!data || cancelled) return;
@@ -105,7 +106,7 @@ export function ChooserScreen({ onCliAvailable }: ChooserScreenProps) {
const checkCliAvailability = React.useCallback(async (): Promise<boolean> => {
try {
const response = await fetch('/health');
const response = await runtimeFetch('/health');
if (!response.ok) return false;
const data = await response.json();
return data.openCodeRunning === true || data.isOpenCodeReady === true;
@@ -206,7 +207,7 @@ export function ChooserScreen({ onCliAvailable }: ChooserScreenProps) {
await restartDesktopApp();
return;
}
await fetch('/api/config/reload', { method: 'POST' });
await runtimeFetch('/api/config/reload', { method: 'POST' });
} finally {
setTimeout(() => setIsApplyingPath(false), 1000);
}
@@ -50,7 +50,7 @@ export function DesktopConnectionRecovery({
if (variant === 'remote-unreachable') {
return { host: t('onboarding.desktopRecovery.placeholders.remoteServer') };
}
if (variant === 'remote-wrong-service') {
if (variant === 'remote-wrong-service' || variant === 'remote-incompatible') {
return { host: t('onboarding.desktopRecovery.placeholders.unknownServer') };
}
return undefined;
@@ -84,7 +84,7 @@ export function DesktopConnectionRecovery({
</div>
{/* Host info if available */}
{hostUrl && (variant === 'remote-unreachable' || variant === 'remote-wrong-service') && (
{hostUrl && (variant === 'remote-unreachable' || variant === 'remote-wrong-service' || variant === 'remote-incompatible') && (
<div className="rounded-lg border border-border bg-background/50 p-3">
<div className="text-xs text-muted-foreground mb-1">{t('onboarding.remoteConnection.field.serverAddress')}</div>
<div className="font-mono text-sm text-foreground truncate">{redactSensitiveUrl(hostUrl)}</div>
@@ -7,6 +7,7 @@ import { updateDesktopSettings } from '@/lib/persistence';
import { copyTextToClipboard } from '@/lib/clipboard';
import { restartDesktopApp } from '@/lib/desktop';
import { useI18n } from '@/lib/i18n';
import { runtimeFetch } from '@/lib/runtime-fetch';
const INSTALL_COMMAND = 'curl -fsSL https://opencode.ai/install | bash';
const DOCS_URL = 'https://opencode.ai/docs';
@@ -99,7 +100,7 @@ export function LocalSetupScreen({
let cancelled = false;
void (async () => {
try {
const response = await fetch('/api/config/settings', { method: 'GET', headers: { Accept: 'application/json' } });
const response = await runtimeFetch('/api/config/settings', { method: 'GET', headers: { Accept: 'application/json' } });
if (!response.ok) return;
const data = (await response.json().catch(() => null)) as null | { opencodeBinary?: unknown };
if (!data || cancelled) return;
@@ -134,7 +135,7 @@ export function LocalSetupScreen({
const checkCliAvailability = React.useCallback(async (): Promise<boolean> => {
try {
const response = await fetch('/health');
const response = await runtimeFetch('/health');
if (!response.ok) return false;
const data = await response.json();
return data.openCodeRunning === true || data.isOpenCodeReady === true;
@@ -182,7 +183,7 @@ export function LocalSetupScreen({
return;
}
await fetch('/api/config/reload', { method: 'POST' });
await runtimeFetch('/api/config/reload', { method: 'POST' });
} finally {
setTimeout(() => setIsRetrying(false), 1000);
}
@@ -4,6 +4,7 @@ import { DesktopConnectionRecovery, type RecoveryVariant } from './DesktopConnec
import { RemoteConnectionForm } from './RemoteConnectionForm';
import { resolveRecoveryNextStep } from './desktopRecoveryRouting';
import { desktopHostsGet, desktopHostsSet } from '@/lib/desktopHosts';
import { runtimeFetch } from '@/lib/runtime-fetch';
type RecoveryScreenProps = {
/** Recovery variant */
@@ -62,7 +63,7 @@ export function RecoveryScreen({
return;
}
await fetch('/api/config/reload', { method: 'POST' });
await runtimeFetch('/api/config/reload', { method: 'POST' });
onRetry?.();
}, [onRetry]);
@@ -3,7 +3,7 @@ import {
desktopHostsGet,
desktopHostsSet,
desktopHostProbe,
normalizeHostUrl,
resolveDesktopHostUrl,
type HostProbeResult,
} from '@/lib/desktopHosts';
import { Button } from '@/components/ui/button';
@@ -37,6 +37,10 @@ function getProbeStatusMessageKey(status: ProbeStatus): string | null {
return null; // Success is shown separately
case 'auth':
return 'onboarding.remoteConnection.probe.authMessage';
case 'update-recommended':
return 'onboarding.remoteConnection.probe.updateRecommendedMessage';
case 'incompatible':
return 'onboarding.remoteConnection.probe.incompatibleMessage';
case 'wrong-service':
return 'onboarding.remoteConnection.probe.wrongServiceMessage';
case 'unreachable':
@@ -47,7 +51,7 @@ function getProbeStatusMessageKey(status: ProbeStatus): string | null {
}
function isBlockingStatus(status: ProbeStatus): boolean {
return status === 'wrong-service' || status === 'unreachable';
return status === 'wrong-service' || status === 'unreachable' || status === 'incompatible';
}
export function RemoteConnectionForm({
@@ -66,7 +70,8 @@ export function RemoteConnectionForm({
const [probeResult, setProbeResult] = useState<HostProbeResult | null>(null);
const [error, setError] = useState('');
const normalizedUrl = normalizeHostUrl(url);
const resolvedUrl = resolveDesktopHostUrl(url);
const normalizedUrl = resolvedUrl?.persistedUrl ?? null;
const handleUrlChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
setUrl(e.target.value);
@@ -89,7 +94,7 @@ export function RemoteConnectionForm({
try {
const result = await desktopHostProbe(normalizedUrl);
setProbeResult(result);
setState(result.status === 'ok' ? 'success' : 'error');
setState(result.status === 'ok' || result.status === 'update-recommended' ? 'success' : 'error');
} catch (err) {
setError(err instanceof Error ? err.message : t('onboarding.remoteConnection.errors.connectionTestFailed'));
setState('error');
@@ -97,14 +102,15 @@ export function RemoteConnectionForm({
}, [normalizedUrl, t]);
const handleConnect = useCallback(async () => {
if (!normalizedUrl) return;
if (!resolvedUrl) return;
const targetUrl = resolvedUrl.persistedUrl;
setState('testing');
setProbeResult(null);
setError('');
try {
const probe = await desktopHostProbe(normalizedUrl);
const probe = await desktopHostProbe(targetUrl);
setProbeResult(probe);
// Block connection on wrong-service or unreachable
@@ -114,10 +120,10 @@ export function RemoteConnectionForm({
}
const config = await desktopHostsGet();
const hostLabel = label.trim() || normalizedUrl;
const hostLabel = label.trim() || targetUrl;
const existingHost = config.hosts.find(
(h) => h.url === normalizedUrl
(h) => h.url === targetUrl
);
const hostId = existingHost ? existingHost.id : `host-${Date.now().toString(16)}`;
@@ -125,7 +131,8 @@ export function RemoteConnectionForm({
const newHost = {
id: hostId,
label: hostLabel,
url: normalizedUrl,
url: targetUrl,
apiUrl: targetUrl,
};
const updatedHosts = existingHost
@@ -141,6 +148,11 @@ export function RemoteConnectionForm({
onConnect?.();
if (resolvedUrl.redeemUrl) {
window.location.assign(resolvedUrl.redeemUrl);
return;
}
if (isTauriShell()) {
const tauri = (window as unknown as { __TAURI__?: { core?: { invoke?: (cmd: string, args?: Record<string, unknown>) => Promise<unknown> } } }).__TAURI__;
await tauri?.core?.invoke?.('desktop_restart');
@@ -149,7 +161,7 @@ export function RemoteConnectionForm({
setError(err instanceof Error ? err.message : t('onboarding.remoteConnection.errors.failedToSaveConnection'));
setState('error');
}
}, [normalizedUrl, label, onConnect, t]);
}, [resolvedUrl, label, onConnect, t]);
const isTesting = state === 'testing';
const canTest = normalizedUrl !== null && !isTesting;
@@ -157,6 +169,7 @@ export function RemoteConnectionForm({
const probeMessageKey = getProbeStatusMessageKey(probeResult?.status ?? null);
const isSuccess = probeResult?.status === 'ok';
const isUpdateRecommended = probeResult?.status === 'update-recommended';
const isAuth = probeResult?.status === 'auth';
const isBlocking = isBlockingStatus(probeResult?.status ?? null);
@@ -238,6 +251,18 @@ export function RemoteConnectionForm({
</div>
)}
{probeResult && isUpdateRecommended && (
<div
className="rounded-lg border p-3 text-sm"
style={{
borderColor: 'var(--status-warning)',
color: 'var(--status-warning)',
}}
>
{probeMessageKey ? t(probeMessageKey as Parameters<typeof t>[0]) : null}
</div>
)}
{/* Blocking errors */}
{probeResult && isBlocking && (
<div
@@ -60,6 +60,15 @@ describe('getDesktopRecoveryConfig', () => {
expect(config.useRemoteLabel).toBe('Use Remote');
});
test('remote-incompatible exposes retry and both actions', () => {
const config = getDesktopRecoveryConfig('remote-incompatible', 'Old Server', 'https://old.example');
expect(config.showRetry).toBe(true);
expect(config.showUseLocal).toBe(true);
expect(config.showUseRemote).toBe(true);
expect(config.titleKey).toBe('onboarding.desktopRecovery.remoteIncompatible.title');
});
// ---------------------------------------------------------------------------
// 4. missing-default-host: chooser-with-context (both actions, no retry)
// ---------------------------------------------------------------------------
@@ -3,6 +3,7 @@ import { redactSensitiveUrl } from '@/lib/desktopHosts';
export type RecoveryVariant =
| 'local-unavailable'
| 'remote-unreachable'
| 'remote-incompatible'
| 'remote-wrong-service'
| 'remote-missing'
| 'missing-default-host';
@@ -113,6 +114,27 @@ export function getDesktopRecoveryConfig(
};
}
case 'remote-incompatible': {
const host = formatHostDisplay(hostLabel, hostUrl);
return {
title: 'Server Update Required',
description: `The OpenChamber server at "${host || 'unknown'}" is not compatible with this app version. Update OpenChamber on the server, then try again.`,
titleKey: 'onboarding.desktopRecovery.remoteIncompatible.title',
descriptionKey: 'onboarding.desktopRecovery.remoteIncompatible.description',
descriptionParams: host ? { host } : undefined,
iconKey: 'remote',
showRetry: true,
retryLabel: 'Retry Connection',
retryLabelKey: 'onboarding.desktopRecovery.remoteUnreachable.retry',
showUseLocal: true,
showUseRemote: true,
useLocalLabel: 'Use Local',
useLocalLabelKey: 'onboarding.desktopRecovery.common.useLocal',
useRemoteLabel: 'Use Remote',
useRemoteLabelKey: 'onboarding.desktopRecovery.common.useRemote',
};
}
case 'missing-default-host':
return {
title: 'No Default Connection',
@@ -17,6 +17,10 @@ const EXPECTED_ROUTING: Record<RecoveryVariant, Record<RecoveryPrimaryAction, Re
'use-local': 'switch-default-to-local',
'use-remote': 'remote-form',
},
'remote-incompatible': {
'use-local': 'switch-default-to-local',
'use-remote': 'remote-form',
},
'remote-wrong-service': {
'use-local': 'switch-default-to-local',
'use-remote': 'remote-form',
@@ -20,6 +20,7 @@ export function resolveRecoveryNextStep(
case 'local-unavailable':
return { kind: 'local-setup' };
case 'remote-unreachable':
case 'remote-incompatible':
case 'remote-wrong-service':
case 'remote-missing':
case 'missing-default-host':