refactor(webui): centralize native runtime access (#4769)

This commit is contained in:
chengyongru
2026-07-14 15:00:30 +08:00
committed by GitHub
parent b7048cf76a
commit 6c9e3a2cc3
5 changed files with 95 additions and 33 deletions
+3 -4
View File
@@ -57,7 +57,6 @@ import {
} from "@/lib/api";
import {
createRuntimeHost,
getHostApi,
toRuntimeSurface,
} from "@/lib/runtime";
import { projectNameFromPath } from "@/lib/workspace";
@@ -939,8 +938,8 @@ export default function App() {
};
const handleNativeEngineRestart = async (): Promise<string> => {
const hostApi = getHostApi();
if (!hostApi?.restartEngine) {
const runtimeHost = createRuntimeHost(state.runtimeSurface);
if (!runtimeHost.restartEngine) {
throw new Error("native engine restart is unavailable");
}
rememberRestartRoute();
@@ -950,7 +949,7 @@ export default function App() {
// ignore storage errors
}
try {
await hostApi.restartEngine();
await runtimeHost.restartEngine();
const refreshed = await refreshReadyClient(state.client, state.runtimeSurface);
return refreshed.token;
} finally {
+13 -10
View File
@@ -136,7 +136,7 @@ import {
type LocalDensity,
type LocalPreferences,
} from "@/lib/local-preferences";
import { getHostApi } from "@/lib/runtime";
import { getRuntimeHost, isNativeRuntime } from "@/lib/runtime";
import { notifyMcpPresetsChanged } from "@/lib/mcp-preset-events";
import { fmtDateTime, relativeTime } from "@/lib/format";
import { useLogoFallback } from "@/hooks/useLogoFallback";
@@ -6695,7 +6695,11 @@ function RuntimeSettings({
}) {
const { t } = useTranslation();
const tx = (key: string, fallback: string) => t(key, { defaultValue: fallback });
const isNativeHost = getHostApi() !== null || (settings.surface ?? settings.runtime_surface) === "native";
const runtimeSurface = settings.surface ?? settings.runtime_surface;
const runtimeHost = getRuntimeHost(runtimeSurface, settings.runtime_capabilities);
const openLogs = runtimeHost.openLogs;
const exportDiagnostics = runtimeHost.exportDiagnostics;
const isNativeHost = isNativeRuntime(runtimeSurface);
const restartActionLabel = isNativeHost
? tx("app.system.restartEngine", "Restart engine")
: t("app.system.restart");
@@ -6709,7 +6713,6 @@ function RuntimeSettings({
} | null>(null);
const [hostActionBusy, setHostActionBusy] =
useState<"logs" | "diagnostics" | null>(null);
const hostApi = getHostApi();
const apiDefaults = apiService ?? {
installed: false,
running: false,
@@ -6741,11 +6744,11 @@ function RuntimeSettings({
: tx("settings.values.ready", "Ready");
const runHostAction = async (
target: "logs" | "diagnostics",
action: () => Promise<string | void>,
action: (() => Promise<string | void>) | undefined,
successMessage: (result: string | void) => string,
failureMessage: string,
) => {
if (!hostApi) {
if (!action) {
setHostActionMessage({
target,
message: tx(
@@ -6832,7 +6835,7 @@ function RuntimeSettings({
onClick={() =>
void runHostAction(
"logs",
() => hostApi!.openLogs(),
openLogs,
() => tx("settings.status.logsOpened", "Opened logs folder."),
tx("settings.status.logsOpenFailed", "Could not open logs folder."),
)
@@ -6863,11 +6866,11 @@ function RuntimeSettings({
onClick={() =>
void runHostAction(
"diagnostics",
async () => {
const path = await hostApi!.exportDiagnostics();
exportDiagnostics ? async () => {
const path = await exportDiagnostics();
setDiagnosticsPath(path);
return path;
},
} : undefined,
(path) =>
t("settings.status.diagnosticsExported", {
path: String(path ?? ""),
@@ -8308,7 +8311,7 @@ function RestartSettingsFooter({
}) {
const { t } = useTranslation();
const tx = (key: string, fallback: string) => t(key, { defaultValue: fallback });
const isNativeHost = getHostApi() !== null;
const isNativeHost = isNativeRuntime();
const restartLabel = isNativeHost
? tx("app.system.restartEngine", "Restart engine")
: t("app.system.restart");
@@ -15,7 +15,7 @@ import type {
WorkspaceScopePayload,
WorkspacesPayload,
} from "@/lib/types";
import { getHostApi } from "@/lib/runtime";
import { getRuntimeHost } from "@/lib/runtime";
import { cn } from "@/lib/utils";
import {
isAbsoluteWorkspacePath,
@@ -55,8 +55,8 @@ export function WorkspaceProjectPicker({
&& !!defaultScope
&& !!onChange
&& controls?.can_change_project !== false;
const hostApi = getHostApi();
const nativeProjectPicker = !!hostApi;
const pickFolder = getRuntimeHost().pickFolder;
const nativeProjectPicker = !!pickFolder;
useEffect(() => {
if (!open) return;
@@ -90,17 +90,17 @@ export function WorkspaceProjectPicker({
);
const pickNativeFolder = useCallback(async () => {
if (!hostApi || disabled) return;
if (!pickFolder || disabled) return;
setPickingFolder(true);
try {
const picked = await hostApi.pickFolder();
const picked = await pickFolder();
if (picked) applyProjectPath(picked);
} catch (err) {
setPathError((err as Error).message);
} finally {
setPickingFolder(false);
}
}, [applyProjectPath, disabled, hostApi]);
}, [applyProjectPath, disabled, pickFolder]);
if (!visible || !defaultScope || !onChange) return null;
+28 -13
View File
@@ -62,7 +62,7 @@ declare global {
}
}
export function getHostApi(): NanobotHostApi | null {
function getHostApi(): NanobotHostApi | null {
if (typeof window === "undefined") return null;
return window.nanobotHost ?? null;
}
@@ -88,13 +88,27 @@ export function createRuntimeHost(
surface,
capabilities: mergedCapabilities,
socketFactory: bridge ? createHostWebSocket : undefined,
pickFolder: api?.pickFolder,
restartEngine: api?.restartEngine,
openLogs: api?.openLogs,
exportDiagnostics: api?.exportDiagnostics,
pickFolder: api ? () => api.pickFolder() : undefined,
restartEngine: api ? () => api.restartEngine() : undefined,
openLogs: api ? () => api.openLogs() : undefined,
exportDiagnostics: api ? () => api.exportDiagnostics() : undefined,
};
}
export function getRuntimeHost(
surface?: string | null,
capabilities?: Partial<RuntimeCapabilities> | null,
): RuntimeHost {
const api = getHostApi();
const runtimeSurface =
surface == null ? (api ? "native" : "browser") : toRuntimeSurface(surface);
return createRuntimeHost(runtimeSurface, capabilities);
}
export function isNativeRuntime(surface?: string | null): boolean {
return getHostApi() !== null || toRuntimeSurface(surface) === "native";
}
export function createHostWebSocket(url: string): WebSocket {
const api = getHostSocketBridge();
if (!api) {
@@ -105,19 +119,20 @@ export function createHostWebSocket(url: string): WebSocket {
function getHostSocketBridge(): HostSocketBridge | null {
const api = getHostApi();
const { closeSocket, onSocketEvent, openSocket, sendSocket } = api ?? {};
if (
!api?.openSocket
|| !api.sendSocket
|| !api.closeSocket
|| !api.onSocketEvent
!openSocket
|| !sendSocket
|| !closeSocket
|| !onSocketEvent
) {
return null;
}
return {
closeSocket: api.closeSocket,
onSocketEvent: api.onSocketEvent,
openSocket: api.openSocket,
sendSocket: api.sendSocket,
closeSocket: (id) => closeSocket.call(api, id),
onSocketEvent: (listener) => onSocketEvent.call(api, listener),
openSocket: (url) => openSocket.call(api, url),
sendSocket: (id, data) => sendSocket.call(api, id, data),
};
}
+45
View File
@@ -0,0 +1,45 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { getRuntimeHost, isNativeRuntime } from "@/lib/runtime";
afterEach(() => {
Reflect.deleteProperty(window, "nanobotHost");
});
describe("runtime host facade", () => {
it("defaults to browser runtime without host actions", () => {
const host = getRuntimeHost();
expect(host.surface).toBe("browser");
expect(host.pickFolder).toBeUndefined();
expect(isNativeRuntime()).toBe(false);
});
it("wraps native host actions behind the runtime facade", async () => {
const pickFolder = vi.fn(async () => "/tmp/project");
const restartEngine = vi.fn(async () => undefined);
Object.defineProperty(window, "nanobotHost", {
configurable: true,
value: {
getRuntimeInfo: vi.fn(),
restartEngine,
pickFolder,
openLogs: vi.fn(async () => undefined),
exportDiagnostics: vi.fn(async () => "/tmp/diagnostics.txt"),
},
});
const host = getRuntimeHost();
expect(host.surface).toBe("native");
expect(isNativeRuntime()).toBe(true);
await expect(host.pickFolder?.()).resolves.toBe("/tmp/project");
await host.restartEngine?.();
expect(pickFolder).toHaveBeenCalledTimes(1);
expect(restartEngine).toHaveBeenCalledTimes(1);
});
it("treats server-reported native surface as native for UI labels", () => {
expect(isNativeRuntime("native")).toBe(true);
});
});