diff --git a/packages/ui/src/lib/runtime-env.ts b/packages/ui/src/lib/runtime-env.ts new file mode 100644 index 00000000..1bfde961 --- /dev/null +++ b/packages/ui/src/lib/runtime-env.ts @@ -0,0 +1,82 @@ +export type HostRuntime = "electron" | "tauri" | "web" +export type PlatformKind = "desktop" | "mobile" + +export interface RuntimeEnvironment { + host: HostRuntime + platform: PlatformKind +} + +declare global { + interface Window { + electronAPI?: unknown + __TAURI__?: { + invoke?: (cmd: string, args?: Record) => Promise + event?: { + listen: (event: string, handler: (payload: { payload: unknown }) => void) => Promise<() => void> + } + } + } +} + +function detectHost(): HostRuntime { + if (typeof window === "undefined") { + return "web" + } + + const win = window as Window & { electronAPI?: unknown } + if (typeof win.electronAPI !== "undefined") { + return "electron" + } + + if (typeof win.__TAURI__ !== "undefined") { + return "tauri" + } + + if (typeof navigator !== "undefined" && /tauri/i.test(navigator.userAgent)) { + return "tauri" + } + + return "web" +} + +function detectPlatform(): PlatformKind { + if (typeof navigator === "undefined") { + return "desktop" + } + + const uaData = (navigator as any).userAgentData + if (uaData?.mobile) { + return "mobile" + } + + const ua = navigator.userAgent.toLowerCase() + if (/android|iphone|ipad|ipod|blackberry|mini|windows phone|mobile|silk/.test(ua)) { + return "mobile" + } + + return "desktop" +} + +let cachedEnv: RuntimeEnvironment | null = null + +export function detectRuntimeEnvironment(): RuntimeEnvironment { + if (cachedEnv) { + return cachedEnv + } + cachedEnv = { + host: detectHost(), + platform: detectPlatform(), + } + if (typeof console !== "undefined") { + const message = `[runtime] host=${cachedEnv.host} platform=${cachedEnv.platform}` + console.info(message) + } + return cachedEnv +} + +export const runtimeEnv = detectRuntimeEnvironment() + +export const isElectronHost = () => runtimeEnv.host === "electron" +export const isTauriHost = () => runtimeEnv.host === "tauri" +export const isWebHost = () => runtimeEnv.host === "web" +export const isMobilePlatform = () => runtimeEnv.platform === "mobile" diff --git a/packages/ui/src/main.tsx b/packages/ui/src/main.tsx index cbcf57d6..6f74c473 100644 --- a/packages/ui/src/main.tsx +++ b/packages/ui/src/main.tsx @@ -3,6 +3,7 @@ import App from "./App" import { ThemeProvider } from "./lib/theme" import { ConfigProvider } from "./stores/preferences" import { InstanceConfigProvider } from "./stores/instance-config" +import { runtimeEnv } from "./lib/runtime-env" import "./index.css" import "@git-diff-view/solid/styles/diff-view-pure.css" @@ -12,6 +13,11 @@ if (!root) { throw new Error("Root element not found") } +if (typeof document !== "undefined") { + document.documentElement.dataset.runtimeHost = runtimeEnv.host + document.documentElement.dataset.runtimePlatform = runtimeEnv.platform +} + render( () => ( diff --git a/packages/ui/src/renderer/loading/main.tsx b/packages/ui/src/renderer/loading/main.tsx index 65c0c581..da12874e 100644 --- a/packages/ui/src/renderer/loading/main.tsx +++ b/packages/ui/src/renderer/loading/main.tsx @@ -1,6 +1,7 @@ import { createSignal, onCleanup, onMount } from "solid-js" import { render } from "solid-js/web" import iconUrl from "../../images/CodeNomad-Icon.png" +import { runtimeEnv, isTauriHost } from "../../lib/runtime-env" import "../../index.css" import "./loading.css" @@ -17,6 +18,12 @@ const phrases = [ "Persuading the AI to give you keyboard control…", ] +const hostStatusMap: Record = { + electron: "Starting desktop shell…", + tauri: "Starting native shell…", + web: "Connecting to CodeNomad…", +} + interface CliStatus { state?: string url?: string | null @@ -30,12 +37,6 @@ interface TauriBridge { } } -declare global { - interface Window { - __TAURI__?: TauriBridge - } -} - function pickPhrase(previous?: string) { const filtered = phrases.filter((phrase) => phrase !== previous) const source = filtered.length > 0 ? filtered : phrases @@ -52,26 +53,34 @@ function getTauriBridge(): TauriBridge | null { if (typeof window === "undefined") { return null } - const bridge = (window as any).__TAURI__ as TauriBridge | undefined + const bridge = (window as { __TAURI__?: TauriBridge }).__TAURI__ if (!bridge || !bridge.event || !bridge.invoke) { return null } return bridge } +function annotateDocument() { + if (typeof document === "undefined") { + return + } + document.documentElement.dataset.runtimeHost = runtimeEnv.host + document.documentElement.dataset.runtimePlatform = runtimeEnv.platform +} + function LoadingApp() { const [phrase, setPhrase] = createSignal(pickPhrase()) const [error, setError] = createSignal(null) - const [status, setStatus] = createSignal("Starting services…") + const [status, setStatus] = createSignal(hostStatusMap[runtimeEnv.host] ?? "Starting services…") const changePhrase = () => setPhrase(pickPhrase(phrase())) onMount(() => { + annotateDocument() setPhrase(pickPhrase()) - const tauriBridge = getTauriBridge() const unsubscribers: Array<() => void> = [] - async function bootstrapTauri() { + async function bootstrapTauri(tauriBridge: TauriBridge | null) { if (!tauriBridge || !tauriBridge.event || !tauriBridge.invoke) { return } @@ -115,7 +124,9 @@ function LoadingApp() { } } - void bootstrapTauri() + if (isTauriHost()) { + void bootstrapTauri(getTauriBridge()) + } onCleanup(() => { unsubscribers.forEach((unsubscribe) => {