mirror of
https://github.com/different-ai/openwork
synced 2026-04-25 17:15:34 +02:00
simplify desktop server connection resolution
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
56
apps/app/src/react-app/shell/openwork-connection.ts
Normal file
56
apps/app/src/react-app/shell/openwork-connection.ts
Normal 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",
|
||||
};
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user