Add runtime environment detection

This commit is contained in:
Shantur Rathore
2025-11-22 21:46:53 +00:00
parent e9f3c4ee52
commit 3edb0ac09e
3 changed files with 110 additions and 11 deletions

View File

@@ -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?: <T = unknown>(cmd: string, args?: Record<string, unknown>) => Promise<T>
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"

View File

@@ -3,6 +3,7 @@ import App from "./App"
import { ThemeProvider } from "./lib/theme" import { ThemeProvider } from "./lib/theme"
import { ConfigProvider } from "./stores/preferences" import { ConfigProvider } from "./stores/preferences"
import { InstanceConfigProvider } from "./stores/instance-config" import { InstanceConfigProvider } from "./stores/instance-config"
import { runtimeEnv } from "./lib/runtime-env"
import "./index.css" import "./index.css"
import "@git-diff-view/solid/styles/diff-view-pure.css" import "@git-diff-view/solid/styles/diff-view-pure.css"
@@ -12,6 +13,11 @@ if (!root) {
throw new Error("Root element not found") throw new Error("Root element not found")
} }
if (typeof document !== "undefined") {
document.documentElement.dataset.runtimeHost = runtimeEnv.host
document.documentElement.dataset.runtimePlatform = runtimeEnv.platform
}
render( render(
() => ( () => (
<ConfigProvider> <ConfigProvider>

View File

@@ -1,6 +1,7 @@
import { createSignal, onCleanup, onMount } from "solid-js" import { createSignal, onCleanup, onMount } from "solid-js"
import { render } from "solid-js/web" import { render } from "solid-js/web"
import iconUrl from "../../images/CodeNomad-Icon.png" import iconUrl from "../../images/CodeNomad-Icon.png"
import { runtimeEnv, isTauriHost } from "../../lib/runtime-env"
import "../../index.css" import "../../index.css"
import "./loading.css" import "./loading.css"
@@ -17,6 +18,12 @@ const phrases = [
"Persuading the AI to give you keyboard control…", "Persuading the AI to give you keyboard control…",
] ]
const hostStatusMap: Record<typeof runtimeEnv.host, string> = {
electron: "Starting desktop shell…",
tauri: "Starting native shell…",
web: "Connecting to CodeNomad…",
}
interface CliStatus { interface CliStatus {
state?: string state?: string
url?: string | null url?: string | null
@@ -30,12 +37,6 @@ interface TauriBridge {
} }
} }
declare global {
interface Window {
__TAURI__?: TauriBridge
}
}
function pickPhrase(previous?: string) { function pickPhrase(previous?: string) {
const filtered = phrases.filter((phrase) => phrase !== previous) const filtered = phrases.filter((phrase) => phrase !== previous)
const source = filtered.length > 0 ? filtered : phrases const source = filtered.length > 0 ? filtered : phrases
@@ -52,26 +53,34 @@ function getTauriBridge(): TauriBridge | null {
if (typeof window === "undefined") { if (typeof window === "undefined") {
return null return null
} }
const bridge = (window as any).__TAURI__ as TauriBridge | undefined const bridge = (window as { __TAURI__?: TauriBridge }).__TAURI__
if (!bridge || !bridge.event || !bridge.invoke) { if (!bridge || !bridge.event || !bridge.invoke) {
return null return null
} }
return bridge return bridge
} }
function annotateDocument() {
if (typeof document === "undefined") {
return
}
document.documentElement.dataset.runtimeHost = runtimeEnv.host
document.documentElement.dataset.runtimePlatform = runtimeEnv.platform
}
function LoadingApp() { function LoadingApp() {
const [phrase, setPhrase] = createSignal(pickPhrase()) const [phrase, setPhrase] = createSignal(pickPhrase())
const [error, setError] = createSignal<string | null>(null) const [error, setError] = createSignal<string | null>(null)
const [status, setStatus] = createSignal<string>("Starting services…") const [status, setStatus] = createSignal<string>(hostStatusMap[runtimeEnv.host] ?? "Starting services…")
const changePhrase = () => setPhrase(pickPhrase(phrase())) const changePhrase = () => setPhrase(pickPhrase(phrase()))
onMount(() => { onMount(() => {
annotateDocument()
setPhrase(pickPhrase()) setPhrase(pickPhrase())
const tauriBridge = getTauriBridge()
const unsubscribers: Array<() => void> = [] const unsubscribers: Array<() => void> = []
async function bootstrapTauri() { async function bootstrapTauri(tauriBridge: TauriBridge | null) {
if (!tauriBridge || !tauriBridge.event || !tauriBridge.invoke) { if (!tauriBridge || !tauriBridge.event || !tauriBridge.invoke) {
return return
} }
@@ -115,7 +124,9 @@ function LoadingApp() {
} }
} }
void bootstrapTauri() if (isTauriHost()) {
void bootstrapTauri(getTauriBridge())
}
onCleanup(() => { onCleanup(() => {
unsubscribers.forEach((unsubscribe) => { unsubscribers.forEach((unsubscribe) => {