Skip to content

Commit ad3ea4e

Browse files
committed
fix(desktop): stabilize packaged webview startup
1 parent 09c00ad commit ad3ea4e

8 files changed

Lines changed: 150 additions & 18 deletions

File tree

apps/desktop/preload/index.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
updaterEvents,
1515
} from "../shared/host";
1616
import { getDesktopRuntimeConfig } from "../shared/runtime-config";
17+
import { resolveWebviewPreloadUrl } from "./webview-preload-url";
1718

1819
const validChannels = new Set<string>(hostInvokeChannels);
1920

@@ -66,10 +67,7 @@ const hostBridge: HostBridge = {
6667
posthogHost: runtimeConfig.posthogHost,
6768
isPackaged: !process.defaultApp,
6869
needsSetupAnimation: process.env.NEXU_NEEDS_SETUP_ANIMATION === "1",
69-
webviewPreloadUrl: new URL(
70-
"./webview-preload.js",
71-
import.meta.url,
72-
).toString(),
70+
webviewPreloadUrl: resolveWebviewPreloadUrl(import.meta.dirname),
7371
},
7472

7573
invoke<TChannel extends HostInvokeChannel>(
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { join } from "node:path";
2+
import { pathToFileURL } from "node:url";
3+
4+
export function resolveWebviewPreloadUrl(preloadDir: string): string {
5+
return pathToFileURL(join(preloadDir, "webview-preload.js")).toString();
6+
}

apps/desktop/preload/webview-preload.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
hostInvokeChannels,
1111
} from "../shared/host";
1212
import { getDesktopRuntimeConfig } from "../shared/runtime-config";
13+
import { resolveWebviewPreloadUrl } from "./webview-preload-url";
1314

1415
const validChannels = new Set<string>(hostInvokeChannels);
1516

@@ -34,10 +35,7 @@ const hostBridge: HostBridge = {
3435
posthogHost: runtimeConfig.posthogHost,
3536
isPackaged: !process.defaultApp,
3637
needsSetupAnimation: false,
37-
webviewPreloadUrl: new URL(
38-
"./webview-preload.js",
39-
import.meta.url,
40-
).toString(),
38+
webviewPreloadUrl: resolveWebviewPreloadUrl(import.meta.dirname),
4139
},
4240

4341
invoke<TChannel extends HostInvokeChannel>(

apps/desktop/src/components/desktop-shell.tsx

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,7 @@ import { SurfaceFrame } from "./surface-frame";
1616
import { UpdateBanner } from "./update-banner";
1717

1818
function getWebviewPreloadUrl(): string {
19-
return new URL(
20-
"../dist-electron/preload/webview-preload.js",
21-
document.location.href,
22-
).href;
19+
return window.nexuHost.bootstrap.webviewPreloadUrl;
2320
}
2421

2522
export function DesktopShell() {
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import type { DesktopRuntimeConfig, RuntimeState } from "../../shared/host";
2+
3+
export function isOpenClawSurfaceReady(
4+
runtimeState: RuntimeState | null,
5+
): boolean {
6+
return (
7+
runtimeState?.units.some(
8+
(unit) => unit.id === "openclaw" && unit.phase === "running",
9+
) ?? false
10+
);
11+
}
12+
13+
export function getDesktopOpenClawUrl(input: {
14+
runtimeConfig: Pick<DesktopRuntimeConfig, "urls" | "tokens"> | null;
15+
runtimeState: RuntimeState | null;
16+
}): string | null {
17+
if (!input.runtimeConfig || !isOpenClawSurfaceReady(input.runtimeState)) {
18+
return null;
19+
}
20+
21+
return new URL(
22+
`/#token=${input.runtimeConfig.tokens.gateway}`,
23+
input.runtimeConfig.urls.openclawBase,
24+
).toString();
25+
}

apps/desktop/src/main.tsx

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ import {
5050
triggerMainProcessCrash,
5151
triggerRendererProcessCrash,
5252
} from "./lib/host-api";
53+
import { getDesktopOpenClawUrl } from "./lib/openclaw-surface";
5354
import { CloudProfilePage } from "./pages/cloud-profile-page";
5455
import "./runtime-page.css";
5556

@@ -1093,6 +1094,7 @@ function DesktopShell() {
10931094
const webSurfaceVersion = 0;
10941095
const [runtimeConfig, setRuntimeConfig] =
10951096
useState<DesktopRuntimeConfig | null>(null);
1097+
const [runtimeState, setRuntimeState] = useState<RuntimeState | null>(null);
10961098
const update = useAutoUpdate();
10971099

10981100
// Setup animation phases:
@@ -1124,6 +1126,24 @@ function DesktopShell() {
11241126
setSetupPhase((prev) => (prev === "looping" ? "fading" : prev));
11251127
})
11261128
.catch(() => null);
1129+
1130+
void getRuntimeState()
1131+
.then(setRuntimeState)
1132+
.catch(() => null);
1133+
}, []);
1134+
1135+
useEffect(() => {
1136+
const unsubscribe = onRuntimeEvent((event) => {
1137+
setRuntimeState((current) => {
1138+
if (!current) {
1139+
return current;
1140+
}
1141+
1142+
return applyRuntimeEvent(current, event);
1143+
});
1144+
});
1145+
1146+
return unsubscribe;
11271147
}, []);
11281148

11291149
useEffect(() => {
@@ -1210,12 +1230,10 @@ function DesktopShell() {
12101230
runtimeConfig && controllerReady
12111231
? new URL("/workspace", runtimeConfig.urls.web).toString()
12121232
: null;
1213-
const desktopOpenClawUrl = runtimeConfig
1214-
? new URL(
1215-
`/#token=${runtimeConfig.tokens.gateway}`,
1216-
runtimeConfig.urls.openclawBase,
1217-
).toString()
1218-
: null;
1233+
const desktopOpenClawUrl = getDesktopOpenClawUrl({
1234+
runtimeConfig,
1235+
runtimeState,
1236+
});
12191237
return (
12201238
<div
12211239
className={
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { describe, expect, it } from "vitest";
2+
import type {
3+
RuntimeState,
4+
RuntimeUnitState,
5+
} from "../../apps/desktop/shared/host";
6+
import {
7+
getDesktopOpenClawUrl,
8+
isOpenClawSurfaceReady,
9+
} from "../../apps/desktop/src/lib/openclaw-surface";
10+
11+
function createUnit(overrides: Partial<RuntimeUnitState>): RuntimeUnitState {
12+
return {
13+
id: "openclaw",
14+
label: "OpenClaw",
15+
kind: "service",
16+
launchStrategy: "launchd",
17+
phase: "starting",
18+
autoStart: true,
19+
pid: null,
20+
port: 18789,
21+
startedAt: null,
22+
exitedAt: null,
23+
exitCode: null,
24+
lastError: null,
25+
lastReasonCode: null,
26+
lastProbeAt: null,
27+
restartCount: 0,
28+
commandSummary: null,
29+
binaryPath: null,
30+
logFilePath: null,
31+
logTail: [],
32+
...overrides,
33+
};
34+
}
35+
36+
function createRuntimeState(units: RuntimeUnitState[]): RuntimeState {
37+
return {
38+
startedAt: new Date(0).toISOString(),
39+
units,
40+
};
41+
}
42+
43+
describe("openclaw surface gating", () => {
44+
it("keeps the surface unavailable until the openclaw runtime unit is running", () => {
45+
const runtimeState = createRuntimeState([
46+
createUnit({ phase: "starting" }),
47+
]);
48+
49+
expect(isOpenClawSurfaceReady(runtimeState)).toBe(false);
50+
});
51+
52+
it("builds the gateway URL only after the openclaw runtime unit is running", () => {
53+
const startingState = createRuntimeState([
54+
createUnit({ phase: "starting" }),
55+
]);
56+
const runningState = createRuntimeState([createUnit({ phase: "running" })]);
57+
58+
expect(
59+
getDesktopOpenClawUrl({
60+
runtimeConfig: {
61+
urls: { openclawBase: "http://127.0.0.1:18789" },
62+
tokens: { gateway: "gw-secret-token" },
63+
},
64+
runtimeState: startingState,
65+
}),
66+
).toBeNull();
67+
68+
expect(
69+
getDesktopOpenClawUrl({
70+
runtimeConfig: {
71+
urls: { openclawBase: "http://127.0.0.1:18789" },
72+
tokens: { gateway: "gw-secret-token" },
73+
},
74+
runtimeState: runningState,
75+
}),
76+
).toBe("http://127.0.0.1:18789/#token=gw-secret-token");
77+
});
78+
});
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { describe, expect, it } from "vitest";
2+
import { resolveWebviewPreloadUrl } from "../../apps/desktop/preload/webview-preload-url";
3+
4+
describe("resolveWebviewPreloadUrl", () => {
5+
it("returns a file URL for the packaged webview preload script", () => {
6+
const url = resolveWebviewPreloadUrl("/tmp/nexu/dist-electron/preload");
7+
8+
expect(url).toBe(
9+
"file:///tmp/nexu/dist-electron/preload/webview-preload.js",
10+
);
11+
});
12+
});

0 commit comments

Comments
 (0)