diff --git a/apps/web/src/components/ChatView.desktop-window.browser.tsx b/apps/web/src/components/ChatView.desktop-window.browser.tsx index b1af382a7..6f38db70d 100644 --- a/apps/web/src/components/ChatView.desktop-window.browser.tsx +++ b/apps/web/src/components/ChatView.desktop-window.browser.tsx @@ -94,4 +94,25 @@ describe("ChatView (full app): the desktop app's window", () => { root.classList.remove("electron", "wco"); } }); + + it("mounts the layer that holds every thread's browser pages", async () => { + // Its code is a chunk only the desktop app fetches (routes/__root.tsx), so + // the window has to load it and mount it by itself, with no panel open. + const mounted = await mountChatView({ + viewport: DEFAULT_VIEWPORT, + snapshot: createSnapshotForTargetUser({ + targetMessageId: "msg-user-desktop-browser-host" as MessageId, + targetText: "desktop browser host", + }), + }); + + try { + await waitForElement( + () => document.querySelector('[data-testid="browser-host-layer"]'), + "The desktop app should mount the browser host layer at its root.", + ); + } finally { + await mounted.cleanup(); + } + }); }); diff --git a/apps/web/src/routes/__root.tsx b/apps/web/src/routes/__root.tsx index 6c4f13b5d..2e6b8f566 100644 --- a/apps/web/src/routes/__root.tsx +++ b/apps/web/src/routes/__root.tsx @@ -16,7 +16,7 @@ import { useNavigate, useRouter, } from "@tanstack/react-router"; -import { useCallback, useEffect, useEffectEvent, useRef, useState } from "react"; +import { Suspense, useCallback, useEffect, useEffectEvent, useRef, useState } from "react"; import { QueryClient, useQueryClient } from "@tanstack/react-query"; import { APP_DISPLAY_NAME } from "../branding"; @@ -24,7 +24,6 @@ import { AppSidebarLayout } from "../components/AppSidebarLayout"; import { CommandPalette, preloadCommandPaletteDialog } from "../components/CommandPalette"; import { preloadDestinationLists } from "../components/sidebar/destinationLists"; import { PushAlertsRuntime } from "../pushAlerts/PushAlertsRuntime"; -import { BrowserHostLayer } from "../components/browser/BrowserHostLayer"; import { BrowserProfileReconciler } from "../components/desktop/BrowserProfileReconciler"; import { DesktopUpdateInstallDialog } from "../components/desktop/DesktopUpdateInstallDialog"; import { SshPasswordPromptDialog } from "../components/desktop/SshPasswordPromptDialog"; @@ -163,6 +162,17 @@ import { type StaleBuildRecovery, } from "../staleBuild"; +// Every thread's browser pages and this window's browser connection for +// agents: the desktop app only, since the web build has no ``. A +// chunk of its own, so a browser tab or a phone never downloads it; the +// desktop app fetches it as it starts, beside everything else. +const BrowserHostLayerLazy = lazyWithPreload(() => + import("../components/browser/BrowserHostLayer").then((module) => module.BrowserHostLayer), +); +if (isElectron) { + void BrowserHostLayerLazy.preload(); +} + // The theme library's sync across computers: after first paint, never at startup. const ThemeLibrarySyncLazy = lazyWithPreload(() => import("../themeLibrary/themeLibraryState").then((module) => module.ThemeLibrarySync), @@ -276,7 +286,12 @@ function RootRouteView() { {/* The Keep bar every theme trial shows, the import dialog, theme file drops. */} {isElectron ? : null} - {isElectron ? : null} + {/* Plain Suspense: the browser panel needs it, so a failure shows. */} + {isElectron ? ( + + + + ) : null} diff --git a/scripts/check-web-bundle-size.ts b/scripts/check-web-bundle-size.ts index 5431208db..aa98a97ec 100644 --- a/scripts/check-web-bundle-size.ts +++ b/scripts/check-web-bundle-size.ts @@ -51,9 +51,11 @@ const DEFAULT_ENTRY_GZIP_BUDGET_BYTES = 255_000; // Raised to 915 KB (2026-11-13, Will's OK): main sat 49 B under the budget // (911,951 B in CI), so the browser's search engine setting (#907), about // 200 B of Settings catalog and schema once its address rules and search URLs -// were moved behind the browser panel, read 912,165 B. Worth trimming next: -// the browser panel's host and automation load at startup on the web and -// phones too, which have no browser panel. +// were moved behind the browser panel, read 912,165 B. +// Measured 890,863 B here (2026-11-14) once the browser panel's host and +// automation, which only the desktop app has a use for, became a chunk only +// it fetches (routes/__root.tsx): about 22 KB back. The budget stays, as +// headroom. const DEFAULT_STARTUP_GZIP_BUDGET_BYTES = 915_000; const entryBudgetBytes = Number(