simplify desktop server connection resolution

This commit is contained in:
Benjamin Shafii
2026-04-24 18:10:42 -07:00
parent 2b9b382a4d
commit 80c30ed96e
6 changed files with 80 additions and 71 deletions

View File

@@ -31,7 +31,7 @@ type DevLogEntry = {
let started = false;
let flushTimer: ReturnType<typeof setTimeout> | null = null;
let queue: DevLogEntry[] = [];
let serverUrlRef: () => string = () => readFallbackServerUrl();
let serverUrlRef: () => string | Promise<string> = () => readFallbackServerUrl();
const pendingFetches = new Map<number, { url: string; method: string; startedAt: number }>();
let nextFetchId = 1;
let lastHeartbeat = Date.now();
@@ -137,7 +137,7 @@ function scheduleFlush() {
async function flushQueue() {
if (queue.length === 0) return;
const base = serverUrlRef();
const base = await serverUrlRef();
if (!base) return;
// Skip the POST entirely when we know the sink is disabled, otherwise
@@ -196,7 +196,7 @@ function isEnabled(): boolean {
return true;
}
export function startDebugLogger(opts?: { serverUrl?: () => string }) {
export function startDebugLogger(opts?: { serverUrl?: () => string | Promise<string> }) {
if (started) return;
if (!isEnabled()) return;
started = true;

View File

@@ -27,8 +27,9 @@ let BOOT_STARTED = false;
* 2) if a local workspace is selected, restart the embedded OpenWork server
* 3) start the OpenCode engine pointed at the workspace
* 4) activate the workspace in the orchestrator
* 5) persist the resulting base URL + token into local OpenWork settings so the
* React routes (session-route / settings-route) see a live `readOpenworkServerSettings()`
* 5) notify React routes that fresh desktop runtime info is available. Electron
* routes read live runtime info directly instead of persisting ephemeral
* localhost ports/tokens into OpenWork settings.
*
* Safe to call multiple times — gated by a `didBoot` ref so it runs once per mount.
*/
@@ -112,15 +113,6 @@ export function useDesktopRuntimeBoot() {
}
const serverInfo = boot.openworkServer;
if (serverInfo?.baseUrl) {
writeOpenworkServerSettings({
urlOverride: serverInfo.baseUrl,
token:
serverInfo.ownerToken?.trim() ||
serverInfo.clientToken?.trim() ||
undefined,
portOverride: serverInfo.port ?? undefined,
remoteAccessEnabled: serverInfo.remoteAccessEnabled === true,
});
try {
window.dispatchEvent(new CustomEvent("openwork-server-settings-changed"));
} catch {

View File

@@ -0,0 +1,56 @@
import {
normalizeOpenworkServerUrl,
readOpenworkServerSettings,
} from "../../app/lib/openwork-server";
import { openworkServerInfo, type OpenworkServerInfo } from "../../app/lib/desktop";
import { isDesktopRuntime } from "../../app/utils";
export type OpenworkConnectionSource = "desktop-runtime" | "stored-settings" | "empty";
export type ResolvedOpenworkConnection = {
normalizedBaseUrl: string;
resolvedToken: string;
hostInfo: OpenworkServerInfo | null;
source: OpenworkConnectionSource;
};
/**
* Resolve the OpenWork server connection for routes that consume the server API.
*
* Local desktop-hosted servers expose ephemeral loopback ports and freshly
* minted tokens on every boot, so live runtime info is the source of truth
* there. Stored settings remain the fallback for remote/manual server
* connections and for desktop cases where the runtime bridge is unavailable.
*/
export async function resolveOpenworkConnection(): Promise<ResolvedOpenworkConnection> {
if (isDesktopRuntime()) {
try {
const info = await openworkServerInfo();
const normalizedBaseUrl =
normalizeOpenworkServerUrl(info.connectUrl ?? info.baseUrl ?? info.lanUrl ?? info.mdnsUrl ?? "") ??
"";
const resolvedToken = info.ownerToken?.trim() || info.clientToken?.trim() || "";
if (normalizedBaseUrl || resolvedToken) {
return {
normalizedBaseUrl,
resolvedToken,
hostInfo: info,
source: "desktop-runtime",
};
}
} catch {
// Fall through to stored settings for remote/manual connections.
}
}
const settings = readOpenworkServerSettings();
const normalizedBaseUrl = normalizeOpenworkServerUrl(settings.urlOverride ?? "") ?? "";
const resolvedToken = settings.token?.trim() ?? "";
return {
normalizedBaseUrl,
resolvedToken,
hostInfo: null,
source: normalizedBaseUrl || resolvedToken ? "stored-settings" : "empty",
};
}

View File

@@ -2,7 +2,7 @@
import { useEffect, type ReactNode } from "react";
import { isWebDeployment } from "../../app/lib/openwork-deployment";
import { hydrateOpenworkServerSettingsFromEnv, readOpenworkServerSettings } from "../../app/lib/openwork-server";
import { hydrateOpenworkServerSettingsFromEnv } from "../../app/lib/openwork-server";
import { isDesktopRuntime } from "../../app/utils";
import { DenAuthProvider } from "../domains/cloud/den-auth-provider";
import { DesktopConfigProvider } from "../domains/cloud/desktop-config-provider";
@@ -13,6 +13,7 @@ import { BootStateProvider } from "./boot-state";
import { DesktopRuntimeBoot } from "./desktop-runtime-boot";
import { startDebugLogger, stopDebugLogger } from "./debug-logger";
import { MigrationPrompt } from "./migration-prompt";
import { resolveOpenworkConnection } from "./openwork-connection";
import { ReloadCoordinatorProvider } from "./reload-coordinator";
function resolveDefaultServerUrl(): string {
@@ -49,7 +50,7 @@ export function AppProviders({ children }: AppProvidersProps) {
// URL on every flush so reconnects after port changes still work. In prod
// builds `startDebugLogger` is a no-op.
startDebugLogger({
serverUrl: () => readOpenworkServerSettings().urlOverride?.trim() ?? "",
serverUrl: async () => (await resolveOpenworkConnection()).normalizedBaseUrl,
});
return () => {
stopDebugLogger();

View File

@@ -14,7 +14,6 @@ import { listCommands, shellInSession } from "../../app/lib/opencode-session";
import {
buildOpenworkWorkspaceBaseUrl,
createOpenworkServerClient,
normalizeOpenworkServerUrl,
readOpenworkServerSettings,
writeOpenworkServerSettings,
type OpenworkServerClient,
@@ -23,7 +22,6 @@ import {
import {
engineInfo,
revealDesktopItemInDir,
openworkServerInfo,
openworkServerRestart,
pickDirectory,
resolveWorkspaceListSelectedId,
@@ -85,6 +83,7 @@ import { useReactRenderWatchdog } from "./react-render-watchdog";
import { getModelBehaviorSummary } from "../../app/lib/model-behavior";
import { filterProviderList, mapConfigProvidersToList } from "../../app/utils/providers";
import { ensureDesktopLocalOpenworkConnection } from "./desktop-local-openwork";
import { resolveOpenworkConnection } from "./openwork-connection";
import { useReloadCoordinator } from "./reload-coordinator";
type RouteWorkspace = OpenworkWorkspaceInfo & {
@@ -108,32 +107,6 @@ function folderNameFromPath(path: string) {
return parts[parts.length - 1] ?? "workspace";
}
async function resolveRouteOpenworkConnection() {
const settings = readOpenworkServerSettings();
let normalizedBaseUrl = normalizeOpenworkServerUrl(settings.urlOverride ?? "") ?? "";
let resolvedToken = settings.token?.trim() ?? "";
let hostInfo: OpenworkServerInfo | null = null;
if (isDesktopRuntime()) {
try {
const info = await openworkServerInfo();
hostInfo = info;
// Desktop-hosted servers use a fresh loopback port and freshly minted
// owner token per boot. Prefer the live runtime info over localStorage;
// the stored URL/token can point at the previous process and produce
// ERR_CONNECTION_REFUSED or 401 before the boot event refreshes settings.
normalizedBaseUrl =
normalizeOpenworkServerUrl(info.connectUrl ?? info.baseUrl ?? info.lanUrl ?? info.mdnsUrl ?? "") ??
normalizedBaseUrl;
resolvedToken = info.ownerToken?.trim() || info.clientToken?.trim() || resolvedToken;
} catch {
// ignore and fall back to stored settings only
}
}
return { normalizedBaseUrl, resolvedToken, hostInfo };
}
function isTransientStartupError(message: string | null | undefined) {
const value = (message ?? "").toLowerCase();
return (
@@ -483,7 +456,7 @@ export function SessionRoute() {
}
}
const { normalizedBaseUrl, resolvedToken, hostInfo } = await resolveRouteOpenworkConnection();
const { normalizedBaseUrl, resolvedToken, hostInfo } = await resolveOpenworkConnection();
setOpenworkServerHostInfoState(hostInfo);
if (!normalizedBaseUrl || !resolvedToken) {
setClient(null);

View File

@@ -7,7 +7,6 @@ import { createClient } from "../../app/lib/opencode";
import {
buildOpenworkWorkspaceBaseUrl,
createOpenworkServerClient,
normalizeOpenworkServerUrl,
readOpenworkServerSettings,
type OpenworkServerCapabilities,
type OpenworkServerClient,
@@ -46,7 +45,6 @@ import {
useWorkspaceShellLayout,
} from "./workspace-shell-layout";
import {
openworkServerInfo,
pickDirectory,
resolveWorkspaceListSelectedId,
workspaceBootstrap,
@@ -65,6 +63,7 @@ import { ModelPickerModal } from "../domains/session/modals/model-picker-modal";
import type { ModelOption, ModelRef } from "../../app/types";
import { recordInspectorEvent } from "./app-inspector";
import { ensureDesktopLocalOpenworkConnection } from "./desktop-local-openwork";
import { resolveOpenworkConnection } from "./openwork-connection";
import { abortSessionSafe } from "../../app/lib/opencode-session";
import { useReloadCoordinator } from "./reload-coordinator";
@@ -173,27 +172,13 @@ function folderNameFromPath(path: string) {
return parts[parts.length - 1] ?? "workspace";
}
async function resolveRouteOpenworkConnection() {
const settings = readOpenworkServerSettings();
let normalizedBaseUrl = normalizeOpenworkServerUrl(settings.urlOverride ?? "") ?? "";
let resolvedToken = settings.token?.trim() ?? "";
if (isDesktopRuntime()) {
try {
const info = await openworkServerInfo();
// Desktop-hosted servers use a fresh loopback port and freshly minted
// owner token per boot. Prefer the live runtime info over localStorage;
// stored settings may belong to a previous server process.
normalizedBaseUrl =
normalizeOpenworkServerUrl(info.connectUrl ?? info.baseUrl ?? info.lanUrl ?? info.mdnsUrl ?? "") ??
normalizedBaseUrl;
resolvedToken = info.ownerToken?.trim() || info.clientToken?.trim() || resolvedToken;
} catch {
// ignore and fall back to stored settings only
}
function isLoopbackServerUrl(raw: string) {
try {
const parsed = new URL(raw);
return parsed.hostname === "127.0.0.1" || parsed.hostname === "localhost" || parsed.hostname === "::1";
} catch {
return false;
}
return { normalizedBaseUrl, resolvedToken };
}
type PersistedThemeMode = "light" | "dark" | "system";
@@ -482,11 +467,13 @@ export function SettingsRoute() {
() =>
createOpenworkServerStore({
startupPreference: () => {
// In Tauri desktop mode, prefer the embedded host server (hostInfo.baseUrl)
// unless the user has explicitly pinned a remote urlOverride.
// In desktop mode, loopback URLs are ephemeral local runtime details.
// Only non-loopback stored URLs indicate an explicit remote/manual
// server connection preference.
if (!isDesktopRuntime()) return "server";
const stored = readOpenworkServerSettings();
return stored.urlOverride?.trim() ? "server" : "local";
const urlOverride = stored.urlOverride?.trim() ?? "";
return urlOverride && !isLoopbackServerUrl(urlOverride) ? "server" : "local";
},
documentVisible: () => typeof document === "undefined" || document.visibilityState === "visible",
developerMode: () => routeStateRef.current.developerMode,
@@ -703,7 +690,7 @@ export function SettingsRoute() {
desktopWorkspaces = workspacesRef.current;
}
}
const { normalizedBaseUrl, resolvedToken } = await resolveRouteOpenworkConnection();
const { normalizedBaseUrl, resolvedToken } = await resolveOpenworkConnection();
if (!normalizedBaseUrl || !resolvedToken) {
setOpenworkClient(null);