| 1 | import "./lib/compat"; |
| 2 | import { StrictMode } from "react"; |
| 3 | import { createRoot } from "react-dom/client"; |
| 4 | import App from "./App"; |
| 5 | import { ErrorBoundary } from "./components/ErrorBoundary"; |
| 6 | import { installPerformancePressureMonitor } from "./lib/crash"; |
| 7 | import { installGlobalCrashHandlers } from "./lib/globalCrashHandlers"; |
| 8 | import { desktopHost } from "./lib/desktopHost"; |
| 9 | import { installBreadcrumbConsoleHook } from "./lib/breadcrumbs"; |
| 10 | import { installPerfDebugHook } from "./lib/perfDebug"; |
| 11 | import { LocaleProvider, preloadDetectedLocale } from "./lib/i18n"; |
| 12 | import { ToastProvider } from "./lib/toast"; |
| 13 | import { initFontFamily } from "./lib/fontFamily"; |
| 14 | import { initTextSize } from "./lib/textSize"; |
| 15 | import { initTypographyPreferences } from "./lib/typographyPreferences"; |
| 16 | import { initTheme } from "./lib/theme"; |
| 17 | import { initConversationWidth } from "./lib/conversationWidth"; |
| 18 | import appShellStylesheetURL from "./styles.css?url"; |
| 19 | |
| 20 | // Install first so startup/runtime failures paint a useful error instead of a |
| 21 | // featureless webview background, with the recent console trail attached. |
| 22 | installGlobalCrashHandlers(); |
| 23 | installBreadcrumbConsoleHook(); |
| 24 | installPerformancePressureMonitor(); |
| 25 | installPerfDebugHook(); |
| 26 | |
| 27 | // Apply the saved appearance (auto/light/dark) before the first paint. |
| 28 | function initTypographyPlatform() { |
| 29 | if (typeof document === "undefined" || typeof navigator === "undefined") return; |
| 30 | const params = new URLSearchParams(window.location.search); |
| 31 | const override = params.get("platform"); |
| 32 | const marker = `${navigator.platform} ${navigator.userAgent}`; |
| 33 | const platform = |
| 34 | override === "darwin" || override === "windows" || override === "linux" |
| 35 | ? override |
| 36 | : /Win/i.test(marker) |
| 37 | ? "windows" |
| 38 | : /Mac/i.test(marker) |
| 39 | ? "darwin" |
| 40 | : "linux"; |
| 41 | document.documentElement.setAttribute("data-platform", platform); |
| 42 | } |
| 43 | |
| 44 | initTypographyPlatform(); |
| 45 | initTheme(); |
| 46 | initConversationWidth(); |
| 47 | initTextSize(); |
| 48 | initFontFamily(); |
| 49 | initTypographyPreferences(); |
| 50 | |
| 51 | // Pre-warm font fallback stacks so the first frame doesn't flicker between the |
| 52 | // browser default font and the app's configured typeface. Inserting a hidden span |
| 53 | // with CJK + emoji + math glyphs forces the OS font subsystem to resolve and |
| 54 | // cache the fallback chains before React mounts. |
| 55 | function prewarmFontFallbacks() { |
| 56 | const span = document.createElement("span"); |
| 57 | span.style.cssText = "position:absolute;visibility:hidden;font-size:1px;pointer-events:none"; |
| 58 | span.textContent = "中文日本語한국어 математика 😀🎉✓⚠∑∏∫"; |
| 59 | document.body.appendChild(span); |
| 60 | // Force layout so the browser resolves font fallback chains. |
| 61 | void span.offsetHeight; |
| 62 | requestAnimationFrame(() => { |
| 63 | requestAnimationFrame(() => { |
| 64 | span.remove(); |
| 65 | }); |
| 66 | }); |
| 67 | } |
| 68 | prewarmFontFallbacks(); |
| 69 | |
| 70 | |
| 71 | // Inside the desktop shell, suppress the webview's default right-click menu — its |
| 72 | // Reload / Back / Inspect entries are easy to hit by accident and can reset or |
| 73 | // navigate away from the app. Text inputs keep their native Cut/Copy/Paste menu; |
| 74 | // the terminal area is exempt so its own context menu can offer copy/paste. |
| 75 | // Left alone in a plain browser (pnpm dev) so devtools stay reachable. |
| 76 | if (desktopHost().kind !== "none") { |
| 77 | window.addEventListener("contextmenu", (e) => { |
| 78 | const target = e.target as HTMLElement | null; |
| 79 | if (!target?.closest("input, textarea, .chat-transcript") && !target?.closest(".terminal-view")) e.preventDefault(); |
| 80 | }); |
| 81 | } |
| 82 | |
| 83 | const root = document.getElementById("root"); |
| 84 | if (!root) throw new Error("missing #root"); |
| 85 | const rootElement = root; |
| 86 | |
| 87 | async function mountApp() { |
| 88 | // The HTML boot shell paints immediately with critical inline styles. Load |
| 89 | // the full stylesheet and detected locale in parallel, then replace that |
| 90 | // shell in one React commit so users never see an unstyled application. |
| 91 | const preloadLocaleForMount = async () => { |
| 92 | await preloadDetectedLocale(); |
| 93 | }; |
| 94 | const stylesResult = await Promise.allSettled([ |
| 95 | new Promise<void>((resolve, reject) => { |
| 96 | const link = document.createElement("link"); |
| 97 | link.rel = "stylesheet"; |
| 98 | link.href = appShellStylesheetURL; |
| 99 | link.onload = () => resolve(); |
| 100 | link.onerror = () => reject(new Error(`failed to load desktop stylesheet: ${appShellStylesheetURL}`)); |
| 101 | document.head.appendChild(link); |
| 102 | }), |
| 103 | preloadLocaleForMount(), |
| 104 | ]); |
| 105 | const [styleResult, localeResult] = stylesResult; |
| 106 | if (styleResult.status === "rejected") { |
| 107 | console.error("failed to load desktop stylesheet", styleResult.reason); |
| 108 | return; |
| 109 | } |
| 110 | if (localeResult.status === "rejected") console.error("failed to preload desktop locale", localeResult.reason); |
| 111 | createRoot(rootElement).render( |
| 112 | <StrictMode> |
| 113 | <ErrorBoundary> |
| 114 | <LocaleProvider> |
| 115 | <ToastProvider> |
| 116 | <App /> |
| 117 | </ToastProvider> |
| 118 | </LocaleProvider> |
| 119 | </ErrorBoundary> |
| 120 | </StrictMode>, |
| 121 | ); |
| 122 | |
| 123 | void import("./lib/desktopWebViewHeartbeat").then(({ installDesktopWebViewHeartbeat }) => { |
| 124 | installDesktopWebViewHeartbeat(); |
| 125 | }); |
| 126 | } |
| 127 | |
| 128 | void mountApp(); |
| 129 |