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 started = false;
|
||||||
let flushTimer: ReturnType<typeof setTimeout> | null = null;
|
let flushTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
let queue: DevLogEntry[] = [];
|
let queue: DevLogEntry[] = [];
|
||||||
let serverUrlRef: () => string = () => readFallbackServerUrl();
|
let serverUrlRef: () => string | Promise<string> = () => readFallbackServerUrl();
|
||||||
const pendingFetches = new Map<number, { url: string; method: string; startedAt: number }>();
|
const pendingFetches = new Map<number, { url: string; method: string; startedAt: number }>();
|
||||||
let nextFetchId = 1;
|
let nextFetchId = 1;
|
||||||
let lastHeartbeat = Date.now();
|
let lastHeartbeat = Date.now();
|
||||||
@@ -137,7 +137,7 @@ function scheduleFlush() {
|
|||||||
|
|
||||||
async function flushQueue() {
|
async function flushQueue() {
|
||||||
if (queue.length === 0) return;
|
if (queue.length === 0) return;
|
||||||
const base = serverUrlRef();
|
const base = await serverUrlRef();
|
||||||
if (!base) return;
|
if (!base) return;
|
||||||
|
|
||||||
// Skip the POST entirely when we know the sink is disabled, otherwise
|
// Skip the POST entirely when we know the sink is disabled, otherwise
|
||||||
@@ -196,7 +196,7 @@ function isEnabled(): boolean {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function startDebugLogger(opts?: { serverUrl?: () => string }) {
|
export function startDebugLogger(opts?: { serverUrl?: () => string | Promise<string> }) {
|
||||||
if (started) return;
|
if (started) return;
|
||||||
if (!isEnabled()) return;
|
if (!isEnabled()) return;
|
||||||
started = true;
|
started = true;
|
||||||
|
|||||||
@@ -27,8 +27,9 @@ let BOOT_STARTED = false;
|
|||||||
* 2) if a local workspace is selected, restart the embedded OpenWork server
|
* 2) if a local workspace is selected, restart the embedded OpenWork server
|
||||||
* 3) start the OpenCode engine pointed at the workspace
|
* 3) start the OpenCode engine pointed at the workspace
|
||||||
* 4) activate the workspace in the orchestrator
|
* 4) activate the workspace in the orchestrator
|
||||||
* 5) persist the resulting base URL + token into local OpenWork settings so the
|
* 5) notify React routes that fresh desktop runtime info is available. Electron
|
||||||
* React routes (session-route / settings-route) see a live `readOpenworkServerSettings()`
|
* 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.
|
* 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;
|
const serverInfo = boot.openworkServer;
|
||||||
if (serverInfo?.baseUrl) {
|
if (serverInfo?.baseUrl) {
|
||||||
writeOpenworkServerSettings({
|
|
||||||
urlOverride: serverInfo.baseUrl,
|
|
||||||
token:
|
|
||||||
serverInfo.ownerToken?.trim() ||
|
|
||||||
serverInfo.clientToken?.trim() ||
|
|
||||||
undefined,
|
|
||||||
portOverride: serverInfo.port ?? undefined,
|
|
||||||
remoteAccessEnabled: serverInfo.remoteAccessEnabled === true,
|
|
||||||
});
|
|
||||||
try {
|
try {
|
||||||
window.dispatchEvent(new CustomEvent("openwork-server-settings-changed"));
|
window.dispatchEvent(new CustomEvent("openwork-server-settings-changed"));
|
||||||
} catch {
|
} 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 { useEffect, type ReactNode } from "react";
|
||||||
|
|
||||||
import { isWebDeployment } from "../../app/lib/openwork-deployment";
|
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 { isDesktopRuntime } from "../../app/utils";
|
||||||
import { DenAuthProvider } from "../domains/cloud/den-auth-provider";
|
import { DenAuthProvider } from "../domains/cloud/den-auth-provider";
|
||||||
import { DesktopConfigProvider } from "../domains/cloud/desktop-config-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 { DesktopRuntimeBoot } from "./desktop-runtime-boot";
|
||||||
import { startDebugLogger, stopDebugLogger } from "./debug-logger";
|
import { startDebugLogger, stopDebugLogger } from "./debug-logger";
|
||||||
import { MigrationPrompt } from "./migration-prompt";
|
import { MigrationPrompt } from "./migration-prompt";
|
||||||
|
import { resolveOpenworkConnection } from "./openwork-connection";
|
||||||
import { ReloadCoordinatorProvider } from "./reload-coordinator";
|
import { ReloadCoordinatorProvider } from "./reload-coordinator";
|
||||||
|
|
||||||
function resolveDefaultServerUrl(): string {
|
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
|
// URL on every flush so reconnects after port changes still work. In prod
|
||||||
// builds `startDebugLogger` is a no-op.
|
// builds `startDebugLogger` is a no-op.
|
||||||
startDebugLogger({
|
startDebugLogger({
|
||||||
serverUrl: () => readOpenworkServerSettings().urlOverride?.trim() ?? "",
|
serverUrl: async () => (await resolveOpenworkConnection()).normalizedBaseUrl,
|
||||||
});
|
});
|
||||||
return () => {
|
return () => {
|
||||||
stopDebugLogger();
|
stopDebugLogger();
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import { listCommands, shellInSession } from "../../app/lib/opencode-session";
|
|||||||
import {
|
import {
|
||||||
buildOpenworkWorkspaceBaseUrl,
|
buildOpenworkWorkspaceBaseUrl,
|
||||||
createOpenworkServerClient,
|
createOpenworkServerClient,
|
||||||
normalizeOpenworkServerUrl,
|
|
||||||
readOpenworkServerSettings,
|
readOpenworkServerSettings,
|
||||||
writeOpenworkServerSettings,
|
writeOpenworkServerSettings,
|
||||||
type OpenworkServerClient,
|
type OpenworkServerClient,
|
||||||
@@ -23,7 +22,6 @@ import {
|
|||||||
import {
|
import {
|
||||||
engineInfo,
|
engineInfo,
|
||||||
revealDesktopItemInDir,
|
revealDesktopItemInDir,
|
||||||
openworkServerInfo,
|
|
||||||
openworkServerRestart,
|
openworkServerRestart,
|
||||||
pickDirectory,
|
pickDirectory,
|
||||||
resolveWorkspaceListSelectedId,
|
resolveWorkspaceListSelectedId,
|
||||||
@@ -85,6 +83,7 @@ import { useReactRenderWatchdog } from "./react-render-watchdog";
|
|||||||
import { getModelBehaviorSummary } from "../../app/lib/model-behavior";
|
import { getModelBehaviorSummary } from "../../app/lib/model-behavior";
|
||||||
import { filterProviderList, mapConfigProvidersToList } from "../../app/utils/providers";
|
import { filterProviderList, mapConfigProvidersToList } from "../../app/utils/providers";
|
||||||
import { ensureDesktopLocalOpenworkConnection } from "./desktop-local-openwork";
|
import { ensureDesktopLocalOpenworkConnection } from "./desktop-local-openwork";
|
||||||
|
import { resolveOpenworkConnection } from "./openwork-connection";
|
||||||
import { useReloadCoordinator } from "./reload-coordinator";
|
import { useReloadCoordinator } from "./reload-coordinator";
|
||||||
|
|
||||||
type RouteWorkspace = OpenworkWorkspaceInfo & {
|
type RouteWorkspace = OpenworkWorkspaceInfo & {
|
||||||
@@ -108,32 +107,6 @@ function folderNameFromPath(path: string) {
|
|||||||
return parts[parts.length - 1] ?? "workspace";
|
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) {
|
function isTransientStartupError(message: string | null | undefined) {
|
||||||
const value = (message ?? "").toLowerCase();
|
const value = (message ?? "").toLowerCase();
|
||||||
return (
|
return (
|
||||||
@@ -483,7 +456,7 @@ export function SessionRoute() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const { normalizedBaseUrl, resolvedToken, hostInfo } = await resolveRouteOpenworkConnection();
|
const { normalizedBaseUrl, resolvedToken, hostInfo } = await resolveOpenworkConnection();
|
||||||
setOpenworkServerHostInfoState(hostInfo);
|
setOpenworkServerHostInfoState(hostInfo);
|
||||||
if (!normalizedBaseUrl || !resolvedToken) {
|
if (!normalizedBaseUrl || !resolvedToken) {
|
||||||
setClient(null);
|
setClient(null);
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import { createClient } from "../../app/lib/opencode";
|
|||||||
import {
|
import {
|
||||||
buildOpenworkWorkspaceBaseUrl,
|
buildOpenworkWorkspaceBaseUrl,
|
||||||
createOpenworkServerClient,
|
createOpenworkServerClient,
|
||||||
normalizeOpenworkServerUrl,
|
|
||||||
readOpenworkServerSettings,
|
readOpenworkServerSettings,
|
||||||
type OpenworkServerCapabilities,
|
type OpenworkServerCapabilities,
|
||||||
type OpenworkServerClient,
|
type OpenworkServerClient,
|
||||||
@@ -46,7 +45,6 @@ import {
|
|||||||
useWorkspaceShellLayout,
|
useWorkspaceShellLayout,
|
||||||
} from "./workspace-shell-layout";
|
} from "./workspace-shell-layout";
|
||||||
import {
|
import {
|
||||||
openworkServerInfo,
|
|
||||||
pickDirectory,
|
pickDirectory,
|
||||||
resolveWorkspaceListSelectedId,
|
resolveWorkspaceListSelectedId,
|
||||||
workspaceBootstrap,
|
workspaceBootstrap,
|
||||||
@@ -65,6 +63,7 @@ import { ModelPickerModal } from "../domains/session/modals/model-picker-modal";
|
|||||||
import type { ModelOption, ModelRef } from "../../app/types";
|
import type { ModelOption, ModelRef } from "../../app/types";
|
||||||
import { recordInspectorEvent } from "./app-inspector";
|
import { recordInspectorEvent } from "./app-inspector";
|
||||||
import { ensureDesktopLocalOpenworkConnection } from "./desktop-local-openwork";
|
import { ensureDesktopLocalOpenworkConnection } from "./desktop-local-openwork";
|
||||||
|
import { resolveOpenworkConnection } from "./openwork-connection";
|
||||||
import { abortSessionSafe } from "../../app/lib/opencode-session";
|
import { abortSessionSafe } from "../../app/lib/opencode-session";
|
||||||
import { useReloadCoordinator } from "./reload-coordinator";
|
import { useReloadCoordinator } from "./reload-coordinator";
|
||||||
|
|
||||||
@@ -173,27 +172,13 @@ function folderNameFromPath(path: string) {
|
|||||||
return parts[parts.length - 1] ?? "workspace";
|
return parts[parts.length - 1] ?? "workspace";
|
||||||
}
|
}
|
||||||
|
|
||||||
async function resolveRouteOpenworkConnection() {
|
function isLoopbackServerUrl(raw: string) {
|
||||||
const settings = readOpenworkServerSettings();
|
try {
|
||||||
let normalizedBaseUrl = normalizeOpenworkServerUrl(settings.urlOverride ?? "") ?? "";
|
const parsed = new URL(raw);
|
||||||
let resolvedToken = settings.token?.trim() ?? "";
|
return parsed.hostname === "127.0.0.1" || parsed.hostname === "localhost" || parsed.hostname === "::1";
|
||||||
|
} catch {
|
||||||
if (isDesktopRuntime()) {
|
return false;
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return { normalizedBaseUrl, resolvedToken };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type PersistedThemeMode = "light" | "dark" | "system";
|
type PersistedThemeMode = "light" | "dark" | "system";
|
||||||
@@ -482,11 +467,13 @@ export function SettingsRoute() {
|
|||||||
() =>
|
() =>
|
||||||
createOpenworkServerStore({
|
createOpenworkServerStore({
|
||||||
startupPreference: () => {
|
startupPreference: () => {
|
||||||
// In Tauri desktop mode, prefer the embedded host server (hostInfo.baseUrl)
|
// In desktop mode, loopback URLs are ephemeral local runtime details.
|
||||||
// unless the user has explicitly pinned a remote urlOverride.
|
// Only non-loopback stored URLs indicate an explicit remote/manual
|
||||||
|
// server connection preference.
|
||||||
if (!isDesktopRuntime()) return "server";
|
if (!isDesktopRuntime()) return "server";
|
||||||
const stored = readOpenworkServerSettings();
|
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",
|
documentVisible: () => typeof document === "undefined" || document.visibilityState === "visible",
|
||||||
developerMode: () => routeStateRef.current.developerMode,
|
developerMode: () => routeStateRef.current.developerMode,
|
||||||
@@ -703,7 +690,7 @@ export function SettingsRoute() {
|
|||||||
desktopWorkspaces = workspacesRef.current;
|
desktopWorkspaces = workspacesRef.current;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const { normalizedBaseUrl, resolvedToken } = await resolveRouteOpenworkConnection();
|
const { normalizedBaseUrl, resolvedToken } = await resolveOpenworkConnection();
|
||||||
|
|
||||||
if (!normalizedBaseUrl || !resolvedToken) {
|
if (!normalizedBaseUrl || !resolvedToken) {
|
||||||
setOpenworkClient(null);
|
setOpenworkClient(null);
|
||||||
|
|||||||
Reference in New Issue
Block a user