Files
NomadArch/packages/ui/src/renderer/loading/main.tsx
Gemini AI 4bd2893864 v0.5.0: Binary-Free Mode - No OpenCode binary required
 Major Features:
- Native session management without OpenCode binary
- Provider routing: OpenCode Zen (free), Qwen OAuth, Z.AI
- Streaming chat with tool execution loop
- Mode detection API (/api/meta/mode)
- MCP integration fix (resolved infinite loading)
- NomadArch Native option in UI with comparison info

🆓 Free Models (No API Key):
- GPT-5 Nano (400K context)
- Grok Code Fast 1 (256K context)
- GLM-4.7 (205K context)
- Doubao Seed Code (256K context)
- Big Pickle (200K context)

📦 New Files:
- session-store.ts: Native session persistence
- native-sessions.ts: REST API for sessions
- lite-mode.ts: UI mode detection client
- native-sessions.ts (UI): SolidJS store

🔧 Updated:
- All installers: Optional binary download
- All launchers: Mode detection display
- Binary selector: Added NomadArch Native option
- README: Binary-Free Mode documentation
2025-12-26 02:12:42 +04:00

231 lines
7.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { Show, createSignal, onCleanup, onMount } from "solid-js"
import { render } from "solid-js/web"
import iconUrl from "../../images/NomadArch-Icon.png"
import { runtimeEnv, isTauriHost } from "../../lib/runtime-env"
import "../../index.css"
import "./loading.css"
const phrases = [
"Warming up the AI neurons…",
"Convincing the AI to stop daydreaming…",
"Polishing the AIs code goggles…",
"Asking the AI to stop reorganizing your files…",
"Feeding the AI additional coffee…",
"Teaching the AI not to delete node_modules (again)…",
"Telling the AI to act natural before you arrive…",
"Asking the AI to please stop rewriting history…",
"Letting the AI stretch before its coding sprint…",
"Persuading the AI to give you keyboard control…",
]
interface CliStatus {
state?: string
url?: string | null
error?: string | null
}
interface TauriBridge {
invoke?: <T = unknown>(cmd: string, args?: Record<string, unknown>) => Promise<T>
event?: {
listen: (event: string, handler: (payload: { payload: unknown }) => void) => Promise<() => void>
}
}
function pickPhrase(previous?: string) {
const filtered = phrases.filter((phrase) => phrase !== previous)
const source = filtered.length > 0 ? filtered : phrases
const index = Math.floor(Math.random() * source.length)
return source[index]
}
function navigateTo(url?: string | null) {
if (!url) return
window.location.replace(url)
}
function getTauriBridge(): TauriBridge | null {
if (typeof window === "undefined") {
return null
}
const bridge = (window as any).__TAURI__ as TauriBridge | undefined
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
}
interface electronAPI {
onCliStatus: (callback: (data: CliStatus) => void) => () => void
onCliError: (callback: (data: { message: string }) => void) => () => void
getCliStatus: () => Promise<CliStatus>
}
function getElectronAPI(): electronAPI | null {
return (window as any).electronAPI || null
}
function LoadingApp() {
const [phrase, setPhrase] = createSignal(pickPhrase())
const [error, setError] = createSignal<string | null>(null)
const [status, setStatus] = createSignal<string | null>(null)
const changePhrase = () => setPhrase(pickPhrase(phrase()))
onMount(() => {
console.info("[loading] mounted")
annotateDocument()
setPhrase(pickPhrase())
const unsubscribers: Array<() => void> = []
async function bootstrapElectron(api: electronAPI) {
console.info("[loading] bootstrapping electron")
try {
const statusUnsubscribe = api.onCliStatus((payload) => {
console.info("[loading] received cli status:", payload)
if (payload.state === "ready" && payload.url) {
setError(null)
setStatus(null)
// Navigate to main app, not CLI API
// In dev, main app is on localhost:3000, in production it's the same origin as loading screen
const mainAppUrl = runtimeEnv.host === "electron" ? window.location.origin.replace(/:\d+/, ":3000") : payload.url
navigateTo(mainAppUrl)
} else if (payload.state === "error" && payload.error) {
setError(payload.error)
setStatus("Encountered an issue")
}
})
const errorUnsubscribe = api.onCliError((payload) => {
console.info("[loading] received cli error:", payload)
if (payload.message) {
setError(payload.message)
setStatus("Encountered an issue")
}
})
unsubscribers.push(statusUnsubscribe, errorUnsubscribe)
const initialStatus = await api.getCliStatus()
console.info("[loading] initial status:", initialStatus)
if (initialStatus?.state === "ready" && initialStatus.url) {
// Navigate to main app, not CLI API
const mainAppUrl = runtimeEnv.host === "electron" ? window.location.origin.replace(/:\d+/, ":3000") : initialStatus.url
navigateTo(mainAppUrl)
} else if (initialStatus?.state === "error" && initialStatus.error) {
setError(initialStatus.error)
setStatus("Encountered an issue")
}
} catch (err) {
console.error("[loading] bootstrap error:", err)
setError(String(err))
setStatus("Encountered an issue")
}
}
async function bootstrapTauri(tauriBridge: TauriBridge | null) {
if (!tauriBridge || !tauriBridge.event || !tauriBridge.invoke) {
return
}
try {
const readyUnlisten = await tauriBridge.event.listen("cli:ready", (event) => {
const payload = (event?.payload as CliStatus) || {}
setError(null)
setStatus(null)
navigateTo(payload.url)
})
const errorUnlisten = await tauriBridge.event.listen("cli:error", (event) => {
const payload = (event?.payload as CliStatus) || {}
if (payload.error) {
setError(payload.error)
setStatus("Encountered an issue")
}
})
const statusUnlisten = await tauriBridge.event.listen("cli:status", (event) => {
const payload = (event?.payload as CliStatus) || {}
if (payload.state === "error" && payload.error) {
setError(payload.error)
setStatus("Encountered an issue")
return
}
if (payload.state && payload.state !== "ready") {
setError(null)
setStatus(null)
}
})
unsubscribers.push(readyUnlisten, errorUnlisten, statusUnlisten)
const result = await tauriBridge.invoke<CliStatus>("cli_get_status")
if (result?.state === "ready" && result.url) {
navigateTo(result.url)
} else if (result?.state === "error" && result.error) {
setError(result.error)
setStatus("Encountered an issue")
}
} catch (err) {
setError(String(err))
setStatus("Encountered an issue")
}
}
console.info("[loading] runtimeHost:", runtimeEnv.host)
if (isTauriHost()) {
void bootstrapTauri(getTauriBridge())
} else if (runtimeEnv.host === "electron") {
const api = getElectronAPI()
console.info("[loading] electronAPI available:", !!api)
if (api) {
void bootstrapElectron(api)
}
}
onCleanup(() => {
unsubscribers.forEach((unsubscribe) => {
try {
unsubscribe()
} catch {
/* noop */
}
})
})
})
return (
<div class="loading-wrapper" role="status" aria-live="polite">
<img src={iconUrl} alt="NomadArch" class="loading-logo" width="180" height="180" />
<div class="loading-heading">
<h1 class="loading-title">NomadArch 1.0</h1>
<p class="loading-subtitle" style={{ "font-size": '14px', "color": '#666', "margin-top": '4px' }}>An enhanced fork of CodeNomad</p>
<Show when={status()}>{(statusText) => <p class="loading-status">{statusText()}</p>}</Show>
</div>
<div class="loading-card">
<div class="loading-row">
<div class="spinner" aria-hidden="true" />
<span>{phrase()}</span>
</div>
<div class="phrase-controls">
<button type="button" onClick={changePhrase}>
Show another
</button>
</div>
{error() && <div class="loading-error">{error()}</div>}
</div>
</div>
)
}
const root = document.getElementById("loading-root")
if (!root) {
throw new Error("Loading root element not found")
}
render(() => <LoadingApp />, root)