Skip to content

Commit 80c30ed

Browse files
simplify desktop server connection resolution
1 parent 2b9b382 commit 80c30ed

6 files changed

Lines changed: 80 additions & 71 deletions

File tree

apps/app/src/react-app/shell/debug-logger.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ type DevLogEntry = {
3131
let started = false;
3232
let flushTimer: ReturnType<typeof setTimeout> | null = null;
3333
let queue: DevLogEntry[] = [];
34-
let serverUrlRef: () => string = () => readFallbackServerUrl();
34+
let serverUrlRef: () => string | Promise<string> = () => readFallbackServerUrl();
3535
const pendingFetches = new Map<number, { url: string; method: string; startedAt: number }>();
3636
let nextFetchId = 1;
3737
let lastHeartbeat = Date.now();
@@ -137,7 +137,7 @@ function scheduleFlush() {
137137

138138
async function flushQueue() {
139139
if (queue.length === 0) return;
140-
const base = serverUrlRef();
140+
const base = await serverUrlRef();
141141
if (!base) return;
142142

143143
// Skip the POST entirely when we know the sink is disabled, otherwise
@@ -196,7 +196,7 @@ function isEnabled(): boolean {
196196
return true;
197197
}
198198

199-
export function startDebugLogger(opts?: { serverUrl?: () => string }) {
199+
export function startDebugLogger(opts?: { serverUrl?: () => string | Promise<string> }) {
200200
if (started) return;
201201
if (!isEnabled()) return;
202202
started = true;

apps/app/src/react-app/shell/desktop-runtime-boot.ts

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,9 @@ let BOOT_STARTED = false;
2727
* 2) if a local workspace is selected, restart the embedded OpenWork server
2828
* 3) start the OpenCode engine pointed at the workspace
2929
* 4) activate the workspace in the orchestrator
30-
* 5) persist the resulting base URL + token into local OpenWork settings so the
31-
* React routes (session-route / settings-route) see a live `readOpenworkServerSettings()`
30+
* 5) notify React routes that fresh desktop runtime info is available. Electron
31+
* routes read live runtime info directly instead of persisting ephemeral
32+
* localhost ports/tokens into OpenWork settings.
3233
*
3334
* Safe to call multiple times — gated by a `didBoot` ref so it runs once per mount.
3435
*/
@@ -112,15 +113,6 @@ export function useDesktopRuntimeBoot() {
112113
}
113114
const serverInfo = boot.openworkServer;
114115
if (serverInfo?.baseUrl) {
115-
writeOpenworkServerSettings({
116-
urlOverride: serverInfo.baseUrl,
117-
token:
118-
serverInfo.ownerToken?.trim() ||
119-
serverInfo.clientToken?.trim() ||
120-
undefined,
121-
portOverride: serverInfo.port ?? undefined,
122-
remoteAccessEnabled: serverInfo.remoteAccessEnabled === true,
123-
});
124116
try {
125117
window.dispatchEvent(new CustomEvent("openwork-server-settings-changed"));
126118
} catch {
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import {
2+
normalizeOpenworkServerUrl,
3+
readOpenworkServerSettings,
4+
} from "../../app/lib/openwork-server";
5+
import { openworkServerInfo, type OpenworkServerInfo } from "../../app/lib/desktop";
6+
import { isDesktopRuntime } from "../../app/utils";
7+
8+
export type OpenworkConnectionSource = "desktop-runtime" | "stored-settings" | "empty";
9+
10+
export type ResolvedOpenworkConnection = {
11+
normalizedBaseUrl: string;
12+
resolvedToken: string;
13+
hostInfo: OpenworkServerInfo | null;
14+
source: OpenworkConnectionSource;
15+
};
16+
17+
/**
18+
* Resolve the OpenWork server connection for routes that consume the server API.
19+
*
20+
* Local desktop-hosted servers expose ephemeral loopback ports and freshly
21+
* minted tokens on every boot, so live runtime info is the source of truth
22+
* there. Stored settings remain the fallback for remote/manual server
23+
* connections and for desktop cases where the runtime bridge is unavailable.
24+
*/
25+
export async function resolveOpenworkConnection(): Promise<ResolvedOpenworkConnection> {
26+
if (isDesktopRuntime()) {
27+
try {
28+
const info = await openworkServerInfo();
29+
const normalizedBaseUrl =
30+
normalizeOpenworkServerUrl(info.connectUrl ?? info.baseUrl ?? info.lanUrl ?? info.mdnsUrl ?? "") ??
31+
"";
32+
const resolvedToken = info.ownerToken?.trim() || info.clientToken?.trim() || "";
33+
if (normalizedBaseUrl || resolvedToken) {
34+
return {
35+
normalizedBaseUrl,
36+
resolvedToken,
37+
hostInfo: info,
38+
source: "desktop-runtime",
39+
};
40+
}
41+
} catch {
42+
// Fall through to stored settings for remote/manual connections.
43+
}
44+
}
45+
46+
const settings = readOpenworkServerSettings();
47+
const normalizedBaseUrl = normalizeOpenworkServerUrl(settings.urlOverride ?? "") ?? "";
48+
const resolvedToken = settings.token?.trim() ?? "";
49+
50+
return {
51+
normalizedBaseUrl,
52+
resolvedToken,
53+
hostInfo: null,
54+
source: normalizedBaseUrl || resolvedToken ? "stored-settings" : "empty",
55+
};
56+
}

apps/app/src/react-app/shell/providers.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import { useEffect, type ReactNode } from "react";
33

44
import { isWebDeployment } from "../../app/lib/openwork-deployment";
5-
import { hydrateOpenworkServerSettingsFromEnv, readOpenworkServerSettings } from "../../app/lib/openwork-server";
5+
import { hydrateOpenworkServerSettingsFromEnv } from "../../app/lib/openwork-server";
66
import { isDesktopRuntime } from "../../app/utils";
77
import { DenAuthProvider } from "../domains/cloud/den-auth-provider";
88
import { DesktopConfigProvider } from "../domains/cloud/desktop-config-provider";
@@ -13,6 +13,7 @@ import { BootStateProvider } from "./boot-state";
1313
import { DesktopRuntimeBoot } from "./desktop-runtime-boot";
1414
import { startDebugLogger, stopDebugLogger } from "./debug-logger";
1515
import { MigrationPrompt } from "./migration-prompt";
16+
import { resolveOpenworkConnection } from "./openwork-connection";
1617
import { ReloadCoordinatorProvider } from "./reload-coordinator";
1718

1819
function resolveDefaultServerUrl(): string {
@@ -49,7 +50,7 @@ export function AppProviders({ children }: AppProvidersProps) {
4950
// URL on every flush so reconnects after port changes still work. In prod
5051
// builds `startDebugLogger` is a no-op.
5152
startDebugLogger({
52-
serverUrl: () => readOpenworkServerSettings().urlOverride?.trim() ?? "",
53+
serverUrl: async () => (await resolveOpenworkConnection()).normalizedBaseUrl,
5354
});
5455
return () => {
5556
stopDebugLogger();

apps/app/src/react-app/shell/session-route.tsx

Lines changed: 2 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import { listCommands, shellInSession } from "../../app/lib/opencode-session";
1414
import {
1515
buildOpenworkWorkspaceBaseUrl,
1616
createOpenworkServerClient,
17-
normalizeOpenworkServerUrl,
1817
readOpenworkServerSettings,
1918
writeOpenworkServerSettings,
2019
type OpenworkServerClient,
@@ -23,7 +22,6 @@ import {
2322
import {
2423
engineInfo,
2524
revealDesktopItemInDir,
26-
openworkServerInfo,
2725
openworkServerRestart,
2826
pickDirectory,
2927
resolveWorkspaceListSelectedId,
@@ -85,6 +83,7 @@ import { useReactRenderWatchdog } from "./react-render-watchdog";
8583
import { getModelBehaviorSummary } from "../../app/lib/model-behavior";
8684
import { filterProviderList, mapConfigProvidersToList } from "../../app/utils/providers";
8785
import { ensureDesktopLocalOpenworkConnection } from "./desktop-local-openwork";
86+
import { resolveOpenworkConnection } from "./openwork-connection";
8887
import { useReloadCoordinator } from "./reload-coordinator";
8988

9089
type RouteWorkspace = OpenworkWorkspaceInfo & {
@@ -108,32 +107,6 @@ function folderNameFromPath(path: string) {
108107
return parts[parts.length - 1] ?? "workspace";
109108
}
110109

111-
async function resolveRouteOpenworkConnection() {
112-
const settings = readOpenworkServerSettings();
113-
let normalizedBaseUrl = normalizeOpenworkServerUrl(settings.urlOverride ?? "") ?? "";
114-
let resolvedToken = settings.token?.trim() ?? "";
115-
let hostInfo: OpenworkServerInfo | null = null;
116-
117-
if (isDesktopRuntime()) {
118-
try {
119-
const info = await openworkServerInfo();
120-
hostInfo = info;
121-
// Desktop-hosted servers use a fresh loopback port and freshly minted
122-
// owner token per boot. Prefer the live runtime info over localStorage;
123-
// the stored URL/token can point at the previous process and produce
124-
// ERR_CONNECTION_REFUSED or 401 before the boot event refreshes settings.
125-
normalizedBaseUrl =
126-
normalizeOpenworkServerUrl(info.connectUrl ?? info.baseUrl ?? info.lanUrl ?? info.mdnsUrl ?? "") ??
127-
normalizedBaseUrl;
128-
resolvedToken = info.ownerToken?.trim() || info.clientToken?.trim() || resolvedToken;
129-
} catch {
130-
// ignore and fall back to stored settings only
131-
}
132-
}
133-
134-
return { normalizedBaseUrl, resolvedToken, hostInfo };
135-
}
136-
137110
function isTransientStartupError(message: string | null | undefined) {
138111
const value = (message ?? "").toLowerCase();
139112
return (
@@ -483,7 +456,7 @@ export function SessionRoute() {
483456
}
484457
}
485458

486-
const { normalizedBaseUrl, resolvedToken, hostInfo } = await resolveRouteOpenworkConnection();
459+
const { normalizedBaseUrl, resolvedToken, hostInfo } = await resolveOpenworkConnection();
487460
setOpenworkServerHostInfoState(hostInfo);
488461
if (!normalizedBaseUrl || !resolvedToken) {
489462
setClient(null);

apps/app/src/react-app/shell/settings-route.tsx

Lines changed: 13 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import { createClient } from "../../app/lib/opencode";
77
import {
88
buildOpenworkWorkspaceBaseUrl,
99
createOpenworkServerClient,
10-
normalizeOpenworkServerUrl,
1110
readOpenworkServerSettings,
1211
type OpenworkServerCapabilities,
1312
type OpenworkServerClient,
@@ -46,7 +45,6 @@ import {
4645
useWorkspaceShellLayout,
4746
} from "./workspace-shell-layout";
4847
import {
49-
openworkServerInfo,
5048
pickDirectory,
5149
resolveWorkspaceListSelectedId,
5250
workspaceBootstrap,
@@ -65,6 +63,7 @@ import { ModelPickerModal } from "../domains/session/modals/model-picker-modal";
6563
import type { ModelOption, ModelRef } from "../../app/types";
6664
import { recordInspectorEvent } from "./app-inspector";
6765
import { ensureDesktopLocalOpenworkConnection } from "./desktop-local-openwork";
66+
import { resolveOpenworkConnection } from "./openwork-connection";
6867
import { abortSessionSafe } from "../../app/lib/opencode-session";
6968
import { useReloadCoordinator } from "./reload-coordinator";
7069

@@ -173,27 +172,13 @@ function folderNameFromPath(path: string) {
173172
return parts[parts.length - 1] ?? "workspace";
174173
}
175174

176-
async function resolveRouteOpenworkConnection() {
177-
const settings = readOpenworkServerSettings();
178-
let normalizedBaseUrl = normalizeOpenworkServerUrl(settings.urlOverride ?? "") ?? "";
179-
let resolvedToken = settings.token?.trim() ?? "";
180-
181-
if (isDesktopRuntime()) {
182-
try {
183-
const info = await openworkServerInfo();
184-
// Desktop-hosted servers use a fresh loopback port and freshly minted
185-
// owner token per boot. Prefer the live runtime info over localStorage;
186-
// stored settings may belong to a previous server process.
187-
normalizedBaseUrl =
188-
normalizeOpenworkServerUrl(info.connectUrl ?? info.baseUrl ?? info.lanUrl ?? info.mdnsUrl ?? "") ??
189-
normalizedBaseUrl;
190-
resolvedToken = info.ownerToken?.trim() || info.clientToken?.trim() || resolvedToken;
191-
} catch {
192-
// ignore and fall back to stored settings only
193-
}
175+
function isLoopbackServerUrl(raw: string) {
176+
try {
177+
const parsed = new URL(raw);
178+
return parsed.hostname === "127.0.0.1" || parsed.hostname === "localhost" || parsed.hostname === "::1";
179+
} catch {
180+
return false;
194181
}
195-
196-
return { normalizedBaseUrl, resolvedToken };
197182
}
198183

199184
type PersistedThemeMode = "light" | "dark" | "system";
@@ -482,11 +467,13 @@ export function SettingsRoute() {
482467
() =>
483468
createOpenworkServerStore({
484469
startupPreference: () => {
485-
// In Tauri desktop mode, prefer the embedded host server (hostInfo.baseUrl)
486-
// unless the user has explicitly pinned a remote urlOverride.
470+
// In desktop mode, loopback URLs are ephemeral local runtime details.
471+
// Only non-loopback stored URLs indicate an explicit remote/manual
472+
// server connection preference.
487473
if (!isDesktopRuntime()) return "server";
488474
const stored = readOpenworkServerSettings();
489-
return stored.urlOverride?.trim() ? "server" : "local";
475+
const urlOverride = stored.urlOverride?.trim() ?? "";
476+
return urlOverride && !isLoopbackServerUrl(urlOverride) ? "server" : "local";
490477
},
491478
documentVisible: () => typeof document === "undefined" || document.visibilityState === "visible",
492479
developerMode: () => routeStateRef.current.developerMode,
@@ -703,7 +690,7 @@ export function SettingsRoute() {
703690
desktopWorkspaces = workspacesRef.current;
704691
}
705692
}
706-
const { normalizedBaseUrl, resolvedToken } = await resolveRouteOpenworkConnection();
693+
const { normalizedBaseUrl, resolvedToken } = await resolveOpenworkConnection();
707694

708695
if (!normalizedBaseUrl || !resolvedToken) {
709696
setOpenworkClient(null);

0 commit comments

Comments
 (0)