refactor(webui): centralize native runtime access (#4769)
This commit is contained in:
+3
-4
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user