fix(webui): honor native host chrome

This commit is contained in:
Xubin Ren
2026-09-03 13:38:21 +08:00
parent fd67f5594c
commit ea57b80fb5
2 changed files with 40 additions and 5 deletions
+12 -5
View File
@@ -79,6 +79,7 @@ import {
} from "@/lib/api"; } from "@/lib/api";
import { import {
createRuntimeHost, createRuntimeHost,
isNativeRuntime,
toRuntimeSurface, toRuntimeSurface,
} from "@/lib/runtime"; } from "@/lib/runtime";
import { projectNameFromPath, scopeWithAccessMode } from "@/lib/workspace"; import { projectNameFromPath, scopeWithAccessMode } from "@/lib/workspace";
@@ -778,6 +779,14 @@ function formatPairingExpiry(seconds: number | null | undefined): string {
return `${Math.ceil(seconds / 60)} min`; 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() { export default function App() {
const { t } = useTranslation(); const { t } = useTranslation();
const [state, setState] = useState<BootState>({ status: "loading" }); const [state, setState] = useState<BootState>({ status: "loading" });
@@ -787,9 +796,7 @@ export default function App() {
async (client: NanobotClient, fallbackSurface: RuntimeSurface) => { async (client: NanobotClient, fallbackSurface: RuntimeSurface) => {
const boot = await fetchBootstrap("", bootstrapSecretRef.current); const boot = await fetchBootstrap("", bootstrapSecretRef.current);
const url = deriveWsUrl(boot.ws_path, boot.token, boot.ws_url); const url = deriveWsUrl(boot.ws_path, boot.token, boot.ws_url);
const runtimeSurface = boot.runtime_surface const runtimeSurface = resolveRuntimeSurface(boot.runtime_surface, fallbackSurface);
? toRuntimeSurface(boot.runtime_surface)
: fallbackSurface;
const runtimeHost = createRuntimeHost(runtimeSurface, boot.runtime_capabilities); const runtimeHost = createRuntimeHost(runtimeSurface, boot.runtime_capabilities);
const tokenExpiresAt = boot.expires_in const tokenExpiresAt = boot.expires_in
? bootstrapTokenExpiresAt(boot.expires_in) ? bootstrapTokenExpiresAt(boot.expires_in)
@@ -827,7 +834,7 @@ export default function App() {
if (cancelled) return; if (cancelled) return;
if (secret) saveSecret(secret); if (secret) saveSecret(secret);
const url = deriveWsUrl(boot.ws_path, boot.token, boot.ws_url); 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 runtimeHost = createRuntimeHost(runtimeSurface, boot.runtime_capabilities);
const client = new NanobotClient({ const client = new NanobotClient({
url, url,
@@ -1086,7 +1093,7 @@ function Shell({
const temporarySessionsRef = useRef<Record<string, ChatSummary>>({}); const temporarySessionsRef = useRef<Record<string, ChatSummary>>({});
const effectiveRuntimeSurface = const effectiveRuntimeSurface =
settingsSnapshot?.surface ?? settingsSnapshot?.runtime_surface ?? runtimeSurface; settingsSnapshot?.surface ?? settingsSnapshot?.runtime_surface ?? runtimeSurface;
const showHostChrome = effectiveRuntimeSurface === "native"; const showHostChrome = isNativeRuntime(effectiveRuntimeSurface);
const showMainSidebar = view !== "settings"; const showMainSidebar = view !== "settings";
const activeTemporarySession = activeKey ? temporarySessions[activeKey] ?? null : null; const activeTemporarySession = activeKey ? temporarySessions[activeKey] ?? null : null;
const temporaryChatId = activeTemporarySession?.chatId ?? null; const temporaryChatId = activeTemporarySession?.chatId ?? null;
+28
View File
@@ -310,6 +310,7 @@ describe("App layout", () => {
sessionUpdateHandlers.clear(); sessionUpdateHandlers.clear();
sidebarStateUpdateHandlers.clear(); sidebarStateUpdateHandlers.clear();
window.history.replaceState(null, "", "/"); window.history.replaceState(null, "", "/");
Reflect.deleteProperty(window, "nanobotHost");
setNavigatorPlatform("Linux x86_64"); setNavigatorPlatform("Linux x86_64");
localStorage.removeItem("nanobot-webui.sidebar"); localStorage.removeItem("nanobot-webui.sidebar");
localStorage.removeItem("nanobot-webui.sidebar.completed-runs.v1"); localStorage.removeItem("nanobot-webui.sidebar.completed-runs.v1");
@@ -335,6 +336,7 @@ describe("App layout", () => {
afterEach(() => { afterEach(() => {
cleanup(); cleanup();
Reflect.deleteProperty(window, "nanobotHost");
vi.useRealTimers(); vi.useRealTimers();
vi.unstubAllGlobals(); vi.unstubAllGlobals();
}); });
@@ -1655,6 +1657,32 @@ describe("App layout", () => {
await waitFor(() => expect(flowSidebar).toHaveStyle({ width: "272px" })); 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(<App />);
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 () => { it("switches to the next session when deleting the active chat", async () => {
mockSessions = [ mockSessions = [
{ {