Unify loader assets across shells
This commit is contained in:
@@ -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,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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 AI’s 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>
|
|
||||||
@@ -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()
|
||||||
|
|||||||
@@ -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 |
@@ -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 AI’s 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>
|
|
||||||
26
packages/ui/src/renderer/loading.html
Normal file
26
packages/ui/src/renderer/loading.html
Normal 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>
|
||||||
111
packages/ui/src/renderer/loading/loading.css
Normal file
111
packages/ui/src/renderer/loading/loading.css
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
160
packages/ui/src/renderer/loading/main.tsx
Normal file
160
packages/ui/src/renderer/loading/main.tsx
Normal 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 AI’s 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)
|
||||||
@@ -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"),
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user