Unify loader assets across shells

This commit is contained in:
Shantur Rathore
2025-11-22 21:20:29 +00:00
parent 92420d9e02
commit e9f3c4ee52
11 changed files with 437 additions and 451 deletions

View File

@@ -6,6 +6,7 @@ const uiRoot = resolve(__dirname, "../ui")
const uiSrc = resolve(uiRoot, "src") const uiSrc = resolve(uiRoot, "src")
const uiRendererRoot = resolve(uiRoot, "src/renderer") const uiRendererRoot = resolve(uiRoot, "src/renderer")
const uiRendererEntry = resolve(uiRendererRoot, "index.html") const uiRendererEntry = resolve(uiRendererRoot, "index.html")
const uiRendererLoadingEntry = resolve(uiRendererRoot, "loading.html")
export default defineConfig({ export default defineConfig({
main: { main: {
@@ -54,7 +55,10 @@ export default defineConfig({
build: { build: {
outDir: resolve(__dirname, "dist/renderer"), outDir: resolve(__dirname, "dist/renderer"),
rollupOptions: { rollupOptions: {
input: uiRendererEntry, input: {
main: uiRendererEntry,
loading: uiRendererLoadingEntry,
},
}, },
}, },
}, },

View File

@@ -30,22 +30,63 @@ function getIconPath() {
return join(mainDirname, "../resources/icon.png") return join(mainDirname, "../resources/icon.png")
} }
function getLoadingHtmlPath() { type LoadingTarget =
| { type: "url"; source: string }
| { type: "file"; source: string }
function resolveDevLoadingUrl(): string | null {
if (app.isPackaged) { if (app.isPackaged) {
return join(process.resourcesPath, "loading.html") return null
}
const devBase = process.env.VITE_DEV_SERVER_URL || process.env.ELECTRON_RENDERER_URL
if (!devBase) {
return null
} }
const distResources = join(mainDirname, "../resources/loading.html") try {
if (existsSync(distResources)) { const normalized = devBase.endsWith("/") ? devBase : `${devBase}/`
return distResources return new URL("loading.html", normalized).toString()
} catch (error) {
console.warn("[cli] failed to construct dev loading URL", devBase, error)
return null
}
} }
const devResources = join(mainDirname, "../electron/resources/loading.html") function resolveLoadingTarget(): LoadingTarget {
if (existsSync(devResources)) { const devUrl = resolveDevLoadingUrl()
return devResources if (devUrl) {
return { type: "url", source: devUrl }
}
const filePath = resolveLoadingFilePath()
return { type: "file", source: filePath }
} }
return join(process.cwd(), "electron/resources/loading.html") function resolveLoadingFilePath() {
const candidates = [
join(app.getAppPath(), "dist/renderer/loading.html"),
join(process.resourcesPath, "dist/renderer/loading.html"),
join(mainDirname, "../dist/renderer/loading.html"),
]
for (const candidate of candidates) {
if (existsSync(candidate)) {
return candidate
}
}
return join(app.getAppPath(), "dist/renderer/loading.html")
}
function loadLoadingScreen(window: BrowserWindow) {
const target = resolveLoadingTarget()
const loader =
target.type === "url"
? window.loadURL(target.source)
: window.loadFile(target.source)
loader.catch((error) => {
console.error("[cli] failed to load loading screen:", error)
})
} }
let cachedPreloadPath: string | null = null let cachedPreloadPath: string | null = null
@@ -116,10 +157,9 @@ function createWindow() {
mainWindow.webContents.session.setSpellCheckerEnabled(false) mainWindow.webContents.session.setSpellCheckerEnabled(false)
} }
const loadingHtml = getLoadingHtmlPath()
showingLoadingScreen = true showingLoadingScreen = true
currentCliUrl = null currentCliUrl = null
mainWindow.loadFile(loadingHtml).catch((error) => console.error("[cli] failed to load loading screen:", error)) loadLoadingScreen(mainWindow)
if (process.env.NODE_ENV === "development") { if (process.env.NODE_ENV === "development") {
mainWindow.webContents.openDevTools({ mode: "detach" }) mainWindow.webContents.openDevTools({ mode: "detach" })
@@ -156,8 +196,7 @@ function showLoadingScreen(force = false) {
showingLoadingScreen = true showingLoadingScreen = true
currentCliUrl = null currentCliUrl = null
pendingCliUrl = null pendingCliUrl = null
const loadingHtml = getLoadingHtmlPath() loadLoadingScreen(mainWindow)
mainWindow.loadFile(loadingHtml).catch((error) => console.error("[cli] failed to load loading screen:", error))
} }
function startCliPreload(url: string) { function startCliPreload(url: string) {

View File

@@ -1,206 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>CodeNomad</title>
<style>
:root {
color-scheme: dark;
}
body {
margin: 0;
min-height: 100vh;
font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
background-color: #1a1a1a;
color: #cfd4dc;
display: flex;
align-items: center;
justify-content: center;
padding: 32px;
text-align: center;
}
button {
border: none;
background: none;
font: inherit;
color: inherit;
}
.wrapper {
display: flex;
flex-direction: column;
align-items: center;
gap: 20px;
max-width: 520px;
}
.logo {
width: 180px;
height: auto;
filter: drop-shadow(0 15px 40px rgba(0, 0, 0, 0.35));
}
.title {
font-size: 2.7rem;
font-weight: 600;
margin: 0;
color: #f4f6fb;
}
.loading-card {
margin-top: 12px;
width: 100%;
max-width: 420px;
padding: 22px;
border-radius: 18px;
background: #151a23;
border: 1px solid rgba(255, 255, 255, 0.08);
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.45);
}
.loading-row {
display: flex;
align-items: center;
justify-content: center;
gap: 14px;
font-size: 0.95rem;
color: #cfd4dc;
}
.spinner {
width: 18px;
height: 18px;
border-radius: 50%;
border: 2px solid rgba(255, 255, 255, 0.18);
border-top-color: #6ce3ff;
animation: spin 0.9s linear infinite;
}
.phrase-controls {
margin-top: 12px;
font-size: 0.9rem;
color: #8f96a9;
display: flex;
justify-content: center;
gap: 8px;
}
.phrase-controls button {
color: #8fb5ff;
cursor: pointer;
}
.logo {
width: 180px;
height: auto;
filter: drop-shadow(0 15px 40px rgba(0, 0, 0, 0.45));
}
.title {
font-size: 2.7rem;
font-weight: 600;
margin: 0;
color: #f4f6fb;
}
.loading-card {
margin-top: 12px;
width: 100%;
padding: 22px;
border-radius: 18px;
background: #0f1421;
border: 1px solid rgba(255, 255, 255, 0.08);
box-shadow: 0 25px 60px rgba(5, 6, 10, 0.6);
}
.wrapper {
display: flex;
flex-direction: column;
align-items: center;
gap: 20px;
max-width: 520px;
}
.logo {
width: 180px;
height: auto;
}
.title {
font-size: 2.7rem;
font-weight: 600;
margin: 0;
}
.subtitle {
margin: 0;
font-size: 1.1rem;
color: #aeb3c4;
}
.loading-card {
margin-top: 12px;
width: 100%;
padding: 20px;
border-radius: 14px;
background: rgba(13, 16, 24, 0.8);
border: 1px solid rgba(255, 255, 255, 0.08);
box-shadow: 0 25px 60px rgba(0, 0, 0, 0.45);
}
.loading-row {
display: flex;
align-items: center;
justify-content: center;
gap: 14px;
font-size: 0.95rem;
color: #cad0dd;
}
.spinner {
width: 18px;
height: 18px;
border-radius: 50%;
border: 2px solid rgba(255, 255, 255, 0.18);
border-top-color: #6ce3ff;
animation: spin 0.9s linear infinite;
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
</style>
</head>
<body>
<div class="wrapper" role="status" aria-live="polite">
<img src="./icon.png" alt="CodeNomad" class="logo" />
<div>
<h1 class="title">CodeNomad</h1>
</div>
<div class="loading-card">
<div class="loading-row">
<div class="spinner" aria-hidden="true"></div>
<span id="loading-phrase">Warming up the AI neurons…</span>
</div>
<div class="phrase-controls">
<button id="phrase-toggle" type="button">Show another</button>
</div>
</div>
</div>
<script>
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…"
]
const phraseEl = document.getElementById("loading-phrase")
const button = document.getElementById("phrase-toggle")
function pickPhrase() {
const next = phrases[Math.floor(Math.random() * phrases.length)]
phraseEl.textContent = next
}
pickPhrase()
button?.addEventListener("click", pickPhrase)
</script>
</body>
</html>

View File

@@ -1,45 +1,89 @@
#!/usr/bin/env node #!/usr/bin/env node
const fs = require("fs"); const fs = require("fs")
const path = require("path"); const path = require("path")
const { execSync } = require("child_process"); const { execSync } = require("child_process")
const root = path.resolve(__dirname, ".."); const root = path.resolve(__dirname, "..")
const workspaceRoot = path.resolve(root, "..", ".."); const workspaceRoot = path.resolve(root, "..", "..")
const serverRoot = path.resolve(root, "..", "server"); const serverRoot = path.resolve(root, "..", "server")
const dest = path.resolve(root, "src-tauri", "resources", "server"); const uiRoot = path.resolve(root, "..", "ui")
const uiDist = path.resolve(uiRoot, "src", "renderer", "dist")
const serverDest = path.resolve(root, "src-tauri", "resources", "server")
const uiLoadingDest = path.resolve(root, "src-tauri", "resources", "ui-loading")
const sources = ["dist", "public", "node_modules", "package.json"]; const sources = ["dist", "public", "node_modules", "package.json"]
function ensureServerBuild() { function ensureServerBuild() {
const distPath = path.join(serverRoot, "dist"); const distPath = path.join(serverRoot, "dist")
const publicPath = path.join(serverRoot, "public"); const publicPath = path.join(serverRoot, "public")
if (fs.existsSync(distPath) && fs.existsSync(publicPath)) { if (fs.existsSync(distPath) && fs.existsSync(publicPath)) {
return; return
} }
console.log("[prebuild] server build missing; running workspace build..."); console.log("[prebuild] server build missing; running workspace build...")
execSync("npm --workspace @neuralnomads/codenomad run build", { execSync("npm --workspace @neuralnomads/codenomad run build", {
cwd: workspaceRoot, cwd: workspaceRoot,
stdio: "inherit", stdio: "inherit",
}); })
if (!fs.existsSync(distPath) || !fs.existsSync(publicPath)) { if (!fs.existsSync(distPath) || !fs.existsSync(publicPath)) {
throw new Error("[prebuild] server artifacts still missing after build"); throw new Error("[prebuild] server artifacts still missing after build")
} }
} }
ensureServerBuild(); function ensureUiBuild() {
const loadingHtml = path.join(uiDist, "loading.html")
if (fs.existsSync(loadingHtml)) {
return
}
fs.rmSync(dest, { recursive: true, force: true }); console.log("[prebuild] ui build missing; running workspace build...")
fs.mkdirSync(dest, { recursive: true }); execSync("npm --workspace @codenomad/ui run build", {
cwd: workspaceRoot,
stdio: "inherit",
})
if (!fs.existsSync(loadingHtml)) {
throw new Error("[prebuild] ui loading assets missing after build")
}
}
function copyServerArtifacts() {
fs.rmSync(serverDest, { recursive: true, force: true })
fs.mkdirSync(serverDest, { recursive: true })
for (const name of sources) { for (const name of sources) {
const from = path.join(serverRoot, name); const from = path.join(serverRoot, name)
const to = path.join(dest, name); const to = path.join(serverDest, name)
if (!fs.existsSync(from)) { if (!fs.existsSync(from)) {
console.warn(`[prebuild] skipped missing ${from}`); console.warn(`[prebuild] skipped missing ${from}`)
continue; continue
} }
fs.cpSync(from, to, { recursive: true }); fs.cpSync(from, to, { recursive: true })
console.log(`[prebuild] copied ${from} -> ${to}`); console.log(`[prebuild] copied ${from} -> ${to}`)
} }
}
function copyUiLoadingAssets() {
const loadingSource = path.join(uiDist, "loading.html")
const assetsSource = path.join(uiDist, "assets")
if (!fs.existsSync(loadingSource)) {
throw new Error("[prebuild] cannot find built loading.html")
}
fs.rmSync(uiLoadingDest, { recursive: true, force: true })
fs.mkdirSync(uiLoadingDest, { recursive: true })
fs.copyFileSync(loadingSource, path.join(uiLoadingDest, "loading.html"))
if (fs.existsSync(assetsSource)) {
fs.cpSync(assetsSource, path.join(uiLoadingDest, "assets"), { recursive: true })
}
console.log(`[prebuild] prepared UI loading assets from ${uiDist}`)
}
ensureServerBuild()
ensureUiBuild()
copyServerArtifacts()
copyUiLoadingAssets()

View File

@@ -4,9 +4,9 @@
"version": "0.1.0", "version": "0.1.0",
"identifier": "ai.opencode.client", "identifier": "ai.opencode.client",
"build": { "build": {
"beforeDevCommand": "", "beforeDevCommand": "npm run prebuild",
"beforeBuildCommand": "npm run bundle:server", "beforeBuildCommand": "npm run bundle:server",
"frontendDist": "../src" "frontendDist": "resources/ui-loading"
}, },
"app": { "app": {
"withGlobalTauri": true, "withGlobalTauri": true,
@@ -14,7 +14,7 @@
{ {
"label": "main", "label": "main",
"title": "CodeNomad", "title": "CodeNomad",
"url": "index.html", "url": "loading.html",
"width": 1400, "width": 1400,
"height": 900, "height": 900,
"minWidth": 800, "minWidth": 800,
@@ -34,9 +34,8 @@
"bundle": { "bundle": {
"active": true, "active": true,
"resources": [ "resources": [
"../src/index.html", "resources/server",
"../src/icon.png", "resources/ui-loading"
"resources/server"
], ],
"icon": ["icon.icns", "icon.ico", "icon.png"], "icon": ["icon.icns", "icon.ico", "icon.png"],
"targets": ["app", "appimage", "deb", "rpm", "nsis"] "targets": ["app", "appimage", "deb", "rpm", "nsis"]

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

View File

@@ -1,197 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>CodeNomad</title>
<style>
:root {
color-scheme: dark;
}
body {
margin: 0;
min-height: 100vh;
font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
background-color: #1a1a1a;
color: #cfd4dc;
display: flex;
align-items: center;
justify-content: center;
padding: 32px;
text-align: center;
}
button {
border: none;
background: none;
font: inherit;
color: inherit;
}
.wrapper {
display: flex;
flex-direction: column;
align-items: center;
gap: 20px;
max-width: 520px;
}
.logo {
width: 180px;
height: auto;
filter: drop-shadow(0 15px 40px rgba(0, 0, 0, 0.35));
}
.title {
font-size: 2.7rem;
font-weight: 600;
margin: 0;
color: #f4f6fb;
}
.loading-card {
margin-top: 12px;
width: 100%;
max-width: 420px;
padding: 22px;
border-radius: 18px;
background: #151a23;
border: 1px solid rgba(255, 255, 255, 0.08);
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.45);
}
.loading-row {
display: flex;
align-items: center;
justify-content: center;
gap: 14px;
font-size: 0.95rem;
color: #cfd4dc;
}
.spinner {
width: 18px;
height: 18px;
border-radius: 50%;
border: 2px solid rgba(255, 255, 255, 0.18);
border-top-color: #6ce3ff;
animation: spin 0.9s linear infinite;
}
.phrase-controls {
margin-top: 12px;
font-size: 0.9rem;
color: #8f96a9;
display: flex;
justify-content: center;
gap: 8px;
}
.phrase-controls button {
color: #8fb5ff;
cursor: pointer;
}
.error {
margin-top: 12px;
color: #ff9ea9;
font-size: 0.95rem;
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
</style>
</head>
<body>
<div class="wrapper" role="status" aria-live="polite">
<img src="./icon.png" alt="CodeNomad" class="logo" />
<div>
<h1 class="title">CodeNomad</h1>
</div>
<div class="loading-card">
<div class="loading-row">
<div class="spinner" aria-hidden="true"></div>
<span id="loading-phrase">Warming up the AI neurons…</span>
</div>
<div class="phrase-controls">
<button id="phrase-toggle" type="button">Show another</button>
</div>
<div class="error" id="error"></div>
</div>
</div>
<script>
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…",
]
const phraseEl = document.getElementById("loading-phrase")
const button = document.getElementById("phrase-toggle")
const errorEl = document.getElementById("error")
function pickPhrase() {
const next = phrases[Math.floor(Math.random() * phrases.length)]
phraseEl.textContent = next
}
function setError(message) {
errorEl.textContent = message || ""
}
function navigateTo(url) {
if (!url) return
window.location.replace(url)
}
async function bootstrap() {
pickPhrase()
button?.addEventListener("click", pickPhrase)
if (!window.__TAURI__ || !window.__TAURI__.event || !window.__TAURI__.invoke) {
return
}
const { listen } = window.__TAURI__.event
const invoke = window.__TAURI__.invoke
listen("cli:ready", (event) => {
const payload = event?.payload || {}
if (payload.url) {
navigateTo(payload.url)
}
})
listen("cli:error", (event) => {
const payload = event?.payload || {}
if (payload.message) {
setError(payload.message)
}
})
listen("cli:status", (event) => {
const payload = event?.payload || {}
if (payload.state !== "ready") {
setError("")
}
})
try {
const status = await invoke("cli_get_status")
if (status?.state === "ready" && status.url) {
navigateTo(status.url)
}
if (status?.state === "error" && status.error) {
setError(status.error)
}
} catch (error) {
setError(String(error))
}
}
bootstrap()
</script>
</body>
</html>

View File

@@ -0,0 +1,26 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>CodeNomad</title>
<script>
;(function () {
try {
const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches
if (prefersDark) {
document.documentElement.setAttribute('data-theme', 'dark')
} else {
document.documentElement.setAttribute('data-theme', 'light')
}
} catch (error) {
console.warn('Failed to apply initial theme', error)
}
})()
</script>
</head>
<body>
<div id="loading-root"></div>
<script type="module" src="./loading/main.tsx"></script>
</body>
</html>

View File

@@ -0,0 +1,111 @@
:root {
color-scheme: dark;
}
body {
margin: 0;
min-height: 100vh;
background-color: var(--surface-base, #0f141f);
color: var(--text-primary, #cfd4dc);
font-family: var(--font-family-sans, "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif);
display: flex;
align-items: center;
justify-content: center;
padding: 32px;
}
button {
border: none;
background: none;
font: inherit;
color: inherit;
}
.loading-wrapper {
display: flex;
flex-direction: column;
align-items: center;
gap: 24px;
max-width: 520px;
width: 100%;
text-align: center;
}
.loading-logo {
width: 180px;
height: auto;
filter: drop-shadow(0 20px 60px rgba(0, 0, 0, 0.45));
}
.loading-heading {
display: flex;
flex-direction: column;
gap: 4px;
}
.loading-title {
font-size: 2.8rem;
font-weight: 600;
margin: 0;
color: var(--text-primary, #f4f6fb);
}
.loading-status {
margin: 0;
font-size: 1rem;
color: var(--text-muted, #aeb3c4);
}
.loading-card {
margin-top: 12px;
width: 100%;
max-width: 420px;
padding: 22px;
border-radius: 18px;
background: rgba(13, 16, 24, 0.85);
border: 1px solid rgba(255, 255, 255, 0.08);
box-shadow: 0 25px 60px rgba(0, 0, 0, 0.55);
}
.loading-row {
display: flex;
align-items: center;
justify-content: center;
gap: 14px;
font-size: 0.95rem;
}
.spinner {
width: 20px;
height: 20px;
border-radius: 50%;
border: 2px solid rgba(255, 255, 255, 0.18);
border-top-color: #6ce3ff;
animation: spin 0.9s linear infinite;
}
.phrase-controls {
margin-top: 12px;
font-size: 0.9rem;
color: var(--text-muted, #8f96a9);
}
.phrase-controls button {
color: #8fb5ff;
cursor: pointer;
}
.loading-error {
margin-top: 12px;
color: #ff9ea9;
font-size: 0.95rem;
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}

View File

@@ -0,0 +1,160 @@
import { createSignal, onCleanup, onMount } from "solid-js"
import { render } from "solid-js/web"
import iconUrl from "../../images/CodeNomad-Icon.png"
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>
}
}
declare global {
interface Window {
__TAURI__?: TauriBridge
}
}
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 LoadingApp() {
const [phrase, setPhrase] = createSignal(pickPhrase())
const [error, setError] = createSignal<string | null>(null)
const [status, setStatus] = createSignal<string>("Starting services…")
const changePhrase = () => setPhrase(pickPhrase(phrase()))
onMount(() => {
setPhrase(pickPhrase())
const tauriBridge = getTauriBridge()
const unsubscribers: Array<() => void> = []
async function bootstrapTauri() {
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("Launching CodeNomad…")
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 && payload.state !== "ready") {
setStatus(payload.state === "starting" ? "Starting services…" : "Preparing CodeNomad…")
setError(null)
}
if (payload.state === "error" && payload.error) {
setError(payload.error)
setStatus("Encountered an issue")
}
})
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")
}
}
void bootstrapTauri()
onCleanup(() => {
unsubscribers.forEach((unsubscribe) => {
try {
unsubscribe()
} catch {
/* noop */
}
})
})
})
return (
<div class="loading-wrapper" role="status" aria-live="polite">
<img src={iconUrl} alt="CodeNomad" class="loading-logo" width="180" height="180" />
<div class="loading-heading">
<h1 class="loading-title">CodeNomad</h1>
<p class="loading-status">{status()}</p>
</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)

View File

@@ -24,5 +24,11 @@ export default defineConfig({
}, },
build: { build: {
outDir: "dist", outDir: "dist",
rollupOptions: {
input: {
main: resolve(__dirname, "./src/renderer/index.html"),
loading: resolve(__dirname, "./src/renderer/loading.html"),
},
},
}, },
}) })