From f5a2a417710a0d7fdaf6545d3de4d8706b425ac2 Mon Sep 17 00:00:00 2001 From: Xubin Ren <52506698+Re-bin@users.noreply.github.com> Date: Thu, 3 Sep 2026 08:59:49 +0800 Subject: [PATCH] fix(webui): honor native host chrome --- webui/src/App.tsx | 17 ++++++++++++----- webui/src/tests/app-layout.test.tsx | 28 ++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 5 deletions(-) diff --git a/webui/src/App.tsx b/webui/src/App.tsx index cb4d556ce..790651833 100644 --- a/webui/src/App.tsx +++ b/webui/src/App.tsx @@ -79,6 +79,7 @@ import { } from "@/lib/api"; import { createRuntimeHost, + isNativeRuntime, toRuntimeSurface, } from "@/lib/runtime"; import { projectNameFromPath, scopeWithAccessMode } from "@/lib/workspace"; @@ -778,6 +779,14 @@ function formatPairingExpiry(seconds: number | null | undefined): string { return `${Math.ceil(seconds / 60)} min`; } +function resolveRuntimeSurface( + surface: RuntimeSurface | null | undefined, + fallback: RuntimeSurface, +): RuntimeSurface { + if (isNativeRuntime(surface)) return "native"; + return surface ? toRuntimeSurface(surface) : fallback; +} + export default function App() { const { t } = useTranslation(); const [state, setState] = useState({ status: "loading" }); @@ -787,9 +796,7 @@ export default function App() { async (client: NanobotClient, fallbackSurface: RuntimeSurface) => { const boot = await fetchBootstrap("", bootstrapSecretRef.current); const url = deriveWsUrl(boot.ws_path, boot.token, boot.ws_url); - const runtimeSurface = boot.runtime_surface - ? toRuntimeSurface(boot.runtime_surface) - : fallbackSurface; + const runtimeSurface = resolveRuntimeSurface(boot.runtime_surface, fallbackSurface); const runtimeHost = createRuntimeHost(runtimeSurface, boot.runtime_capabilities); const tokenExpiresAt = boot.expires_in ? bootstrapTokenExpiresAt(boot.expires_in) @@ -827,7 +834,7 @@ export default function App() { if (cancelled) return; if (secret) saveSecret(secret); const url = deriveWsUrl(boot.ws_path, boot.token, boot.ws_url); - const runtimeSurface = toRuntimeSurface(boot.runtime_surface); + const runtimeSurface = resolveRuntimeSurface(boot.runtime_surface, "browser"); const runtimeHost = createRuntimeHost(runtimeSurface, boot.runtime_capabilities); const client = new NanobotClient({ url, @@ -1086,7 +1093,7 @@ function Shell({ const temporarySessionsRef = useRef>({}); const effectiveRuntimeSurface = settingsSnapshot?.surface ?? settingsSnapshot?.runtime_surface ?? runtimeSurface; - const showHostChrome = effectiveRuntimeSurface === "native"; + const showHostChrome = isNativeRuntime(effectiveRuntimeSurface); const showMainSidebar = view !== "settings"; const activeTemporarySession = activeKey ? temporarySessions[activeKey] ?? null : null; const temporaryChatId = activeTemporarySession?.chatId ?? null; diff --git a/webui/src/tests/app-layout.test.tsx b/webui/src/tests/app-layout.test.tsx index 1f550ad65..299d51d2a 100644 --- a/webui/src/tests/app-layout.test.tsx +++ b/webui/src/tests/app-layout.test.tsx @@ -310,6 +310,7 @@ describe("App layout", () => { sessionUpdateHandlers.clear(); sidebarStateUpdateHandlers.clear(); window.history.replaceState(null, "", "/"); + Reflect.deleteProperty(window, "nanobotHost"); setNavigatorPlatform("Linux x86_64"); localStorage.removeItem("nanobot-webui.sidebar"); localStorage.removeItem("nanobot-webui.sidebar.completed-runs.v1"); @@ -335,6 +336,7 @@ describe("App layout", () => { afterEach(() => { cleanup(); + Reflect.deleteProperty(window, "nanobotHost"); vi.useRealTimers(); vi.unstubAllGlobals(); }); @@ -1655,6 +1657,32 @@ describe("App layout", () => { await waitFor(() => expect(flowSidebar).toHaveStyle({ width: "272px" })); }); + it("uses native chrome when the host bridge overrides browser gateway metadata", async () => { + Reflect.set(window, "nanobotHost", { pickFolder: vi.fn() }); + vi.mocked(fetchBootstrap).mockResolvedValue({ + token: "tok", + api_token: "api-tok", + ws_path: "/", + expires_in: 300, + runtime_surface: "browser", + }); + mockFetchRoutes({ + "/api/settings": { + ...baseSettingsPayload(), + surface: "browser", + runtime_surface: "browser", + }, + }); + + render(); + + await waitFor(() => expect(connectSpy).toHaveBeenCalled()); + await waitFor(() => { + expect(screen.getByTestId("sidebar-brand-mark")).toHaveClass("mt-5"); + }); + expect(document.documentElement).toHaveClass("native-host"); + }); + it("switches to the next session when deleting the active chat", async () => { mockSessions = [ {