LLeMbas Desktop: the server in a window, the CLI's agent on this computer
ci / check (push) Failing after 24s
ci / check (push) Failing after 24s
Tauri 2, no Node. A first-run page chooses the server; then the window is the server's web UI, whose pages get no IPC. The bundled LLeMbas CLI (fetched and signature-checked by scripts/fetch-cli.sh) links the computer with 'lembas login --json' and runs as 'lembas service' while the app runs. A tray menu starts and stops it. Linux packages from GitHub Actions on a tag. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
28 files changed
+6801
No files matched your search
+130
@@ -0,0 +1,130 @@
|
||||
/* The first-run page: the LLeMbas look (Moria dark, Shire light), no build step. */
|
||||
:root {
|
||||
--font-ui: ui-monospace, "JetBrains Mono", "SF Mono", "Cascadia Code", Menlo, Consolas, monospace;
|
||||
--font-body: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
||||
--text-sm: 0.8125rem;
|
||||
--text-base: 0.9375rem;
|
||||
--text-xl: 1.5rem;
|
||||
--sp-2: 0.5rem;
|
||||
--sp-3: 0.75rem;
|
||||
--sp-4: 1rem;
|
||||
--sp-6: 1.5rem;
|
||||
--sp-8: 2rem;
|
||||
--radius: 2px;
|
||||
--press: 2px;
|
||||
--control-h: 2.5rem;
|
||||
--card-max: 34rem;
|
||||
|
||||
color-scheme: dark;
|
||||
--bg: #0B0D10;
|
||||
--surface: #11151A;
|
||||
--surface-hover: #1D232B;
|
||||
--border: #26303A;
|
||||
--border-strong: #5C6976;
|
||||
--ink: #E2E8EC;
|
||||
--ink-muted: #A6B2BC;
|
||||
--accent: #8FD8E8;
|
||||
--accent-hover: #B4E6F1;
|
||||
--accent-ink: #061418;
|
||||
--leaf: #7EE081;
|
||||
--warning: #F0A840;
|
||||
--shadow-card: 4px 4px 0 rgba(0, 0, 0, 0.55);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
color-scheme: light;
|
||||
--bg: #F1EAD6;
|
||||
--surface: #F8F3E5;
|
||||
--surface-hover: #ECE4CF;
|
||||
--border: #CBBF9D;
|
||||
--border-strong: #2A2E22;
|
||||
--ink: #1F2418;
|
||||
--ink-muted: #474D3B;
|
||||
--accent: #285F3C;
|
||||
--accent-hover: #1F4D30;
|
||||
--accent-ink: #F8F3E5;
|
||||
--leaf: #3D6B1C;
|
||||
--warning: #7E5200;
|
||||
--shadow-card: 4px 4px 0 var(--border-strong);
|
||||
}
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
|
||||
html, body { margin: 0; min-height: 100%; }
|
||||
|
||||
body {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
min-height: 100vh;
|
||||
padding: var(--sp-4);
|
||||
background: var(--bg);
|
||||
color: var(--ink);
|
||||
font: var(--text-base) / 1.6 var(--font-body);
|
||||
}
|
||||
|
||||
.card {
|
||||
width: min(100%, var(--card-max));
|
||||
display: grid;
|
||||
gap: var(--sp-6);
|
||||
padding: var(--sp-8);
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
box-shadow: var(--shadow-card);
|
||||
}
|
||||
|
||||
.brand { display: grid; grid-template-columns: 40px 1fr; gap: var(--sp-3); align-items: center; }
|
||||
h1 { margin: 0; font: 600 var(--text-xl) / 1.2 var(--font-ui); }
|
||||
h1 b { color: var(--leaf); font-weight: 700; }
|
||||
h2 { margin: 0 0 var(--sp-3); font: 600 var(--text-sm) / 1.2 var(--font-ui); letter-spacing: 0.08em; color: var(--accent); }
|
||||
p { margin: 0; }
|
||||
.muted { color: var(--ink-muted); font-size: var(--text-sm); }
|
||||
|
||||
/* A field is three stacked parts on fixed rows: label, control row, hint. */
|
||||
.field { display: grid; grid-template-rows: auto var(--control-h) auto; gap: var(--sp-2); }
|
||||
label { font: var(--text-sm) / 1.4 var(--font-ui); color: var(--ink-muted); }
|
||||
.hint { font-size: var(--text-sm); color: var(--ink-muted); min-height: 1.4em; }
|
||||
.hint.is-error, .status.is-error { color: var(--warning); }
|
||||
|
||||
.row { display: grid; grid-template-columns: 1fr auto; gap: var(--sp-2); }
|
||||
.row--buttons { grid-template-columns: repeat(auto-fit, minmax(min(100%, 9rem), 1fr)); margin-top: var(--sp-3); }
|
||||
|
||||
input {
|
||||
height: var(--control-h);
|
||||
min-width: 0;
|
||||
padding: 0 var(--sp-3);
|
||||
font: var(--text-base) var(--font-ui);
|
||||
color: var(--ink);
|
||||
background: var(--bg);
|
||||
border: 1px solid var(--border-strong);
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
input:focus-visible, .btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
||||
|
||||
.btn {
|
||||
height: var(--control-h);
|
||||
padding: 0 var(--sp-4);
|
||||
font: 600 var(--text-sm) var(--font-ui);
|
||||
color: var(--ink);
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border-strong);
|
||||
border-radius: var(--radius);
|
||||
box-shadow: var(--press) var(--press) 0 rgba(0, 0, 0, 0.45);
|
||||
cursor: pointer;
|
||||
}
|
||||
.btn:hover { background: var(--surface-hover); }
|
||||
.btn:active { transform: translate(var(--press), var(--press)); box-shadow: none; }
|
||||
.btn:disabled { opacity: 0.55; cursor: default; }
|
||||
.btn--primary { color: var(--accent-ink); background: var(--accent); border-color: var(--accent); }
|
||||
.btn--primary:hover { background: var(--accent-hover); }
|
||||
|
||||
.status { font: var(--text-sm) / 1.4 var(--font-ui); margin-top: var(--sp-3); }
|
||||
.status.is-ok { color: var(--leaf); }
|
||||
|
||||
@media (max-width: 30rem) {
|
||||
.card { padding: var(--sp-4); gap: var(--sp-4); }
|
||||
.row { grid-template-columns: 1fr; grid-template-rows: var(--control-h) var(--control-h); }
|
||||
.field { grid-template-rows: auto auto auto; }
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
// The first-run page. It talks to the app over Tauri's IPC — four commands, see src-tauri/src/lib.rs.
|
||||
// The server's own pages never get this: they are remote content with no capability.
|
||||
"use strict"
|
||||
|
||||
const { invoke } = window.__TAURI__.core
|
||||
const { listen } = window.__TAURI__.event
|
||||
|
||||
const $ = (id) => document.getElementById(id)
|
||||
|
||||
const AGENT = {
|
||||
off: () => ["The agent is off.", ""],
|
||||
not_linked: () => ["This computer is not linked to the server yet.", ""],
|
||||
starting: () => ["The agent is starting…", ""],
|
||||
running: () => ["The agent is running: agent chats can use this computer.", "is-ok"],
|
||||
elsewhere: (pid) => [`A LLeMbas CLI service already runs here (pid ${pid}); it is this computer's agent.`, "is-ok"],
|
||||
failed: (why) => [`The agent stopped: ${why}. Trying again shortly.`, "is-error"],
|
||||
}
|
||||
|
||||
function render(s) {
|
||||
$("version").textContent = s.version
|
||||
if (s.server && !$("address").value) $("address").value = s.server
|
||||
$("agent-section").hidden = !s.server
|
||||
const [text, cls] = (AGENT[s.agent.state] ?? AGENT.off)(s.agent.detail)
|
||||
const status = $("agent-status")
|
||||
status.textContent = text
|
||||
status.className = `status ${cls}`
|
||||
$("link").textContent = s.linked ? "Link again" : "Link this computer"
|
||||
$("toggle").textContent = s.agent_wanted ? "Stop the agent" : "Start the agent"
|
||||
$("toggle").disabled = !s.linked && !s.agent_wanted
|
||||
}
|
||||
|
||||
async function refresh() {
|
||||
render(await invoke("get_status"))
|
||||
}
|
||||
|
||||
$("server-form").addEventListener("submit", async (e) => {
|
||||
e.preventDefault()
|
||||
const hint = $("server-hint")
|
||||
const button = e.target.querySelector("button")
|
||||
button.disabled = true
|
||||
hint.className = "hint"
|
||||
hint.textContent = "Looking for a LLeMbas server there…"
|
||||
try {
|
||||
await invoke("set_server", { address: $("address").value })
|
||||
// The window now shows the server.
|
||||
} catch (err) {
|
||||
hint.className = "hint is-error"
|
||||
hint.textContent = String(err)
|
||||
} finally {
|
||||
button.disabled = false
|
||||
}
|
||||
})
|
||||
|
||||
$("link").addEventListener("click", async () => {
|
||||
try {
|
||||
await invoke("link_computer")
|
||||
} catch (err) {
|
||||
$("agent-status").className = "status is-error"
|
||||
$("agent-status").textContent = String(err)
|
||||
}
|
||||
})
|
||||
|
||||
$("toggle").addEventListener("click", async () => {
|
||||
const s = await invoke("get_status")
|
||||
await invoke("set_agent", { on: !s.agent_wanted })
|
||||
})
|
||||
|
||||
$("open").addEventListener("click", async () => {
|
||||
const s = await invoke("get_status")
|
||||
if (s.server) await invoke("set_server", { address: s.server })
|
||||
})
|
||||
|
||||
listen("status", (e) => render(e.payload))
|
||||
refresh()
|
||||
@@ -0,0 +1,47 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="color-scheme" content="dark light">
|
||||
<title>LLeMbas</title>
|
||||
<link rel="stylesheet" href="app.css">
|
||||
<script src="app.js" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
<main class="card" aria-labelledby="title">
|
||||
<header class="brand">
|
||||
<img src="logo-mark.svg" alt="" width="40" height="40">
|
||||
<div>
|
||||
<h1 id="title"><b>LL</b>e<b>M</b>bas</h1>
|
||||
<p class="muted">Desktop <span id="version"></span></p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section aria-labelledby="server-h">
|
||||
<h2 id="server-h">[ SERVER ]</h2>
|
||||
<form id="server-form" class="field" novalidate>
|
||||
<label for="address">Address of your LLeMbas server</label>
|
||||
<div class="row">
|
||||
<input id="address" name="address" type="text" inputmode="url" autocomplete="url"
|
||||
placeholder="https://ai.example" spellcheck="false" required>
|
||||
<button type="submit" class="btn btn--primary">Connect</button>
|
||||
</div>
|
||||
<p class="hint" id="server-hint" aria-live="polite">Sign in there as you would in a browser.</p>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section aria-labelledby="agent-h" id="agent-section" hidden>
|
||||
<h2 id="agent-h">[ AGENT ]</h2>
|
||||
<p class="muted">The agent lets agent chats on your server work in this computer's projects.
|
||||
It is the LLeMbas CLI, built in, running in the background while the app runs.</p>
|
||||
<p class="status" id="agent-status" aria-live="polite"></p>
|
||||
<div class="row row--buttons">
|
||||
<button type="button" class="btn" id="link">Link this computer</button>
|
||||
<button type="button" class="btn" id="toggle">Start the agent</button>
|
||||
<button type="button" class="btn btn--primary" id="open">Open the server</button>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="64" height="64"
|
||||
shape-rendering="crispEdges" role="img" aria-label="LLeMbas">
|
||||
<title>LLeMbas</title>
|
||||
<desc>A pixel mallorn leaf with a dark one-pixel edge.</desc>
|
||||
<g transform="translate(0 0) scale(1)" shape-rendering="crispEdges"><rect x="10" y="0" width="5" height="1" fill="#0B0D10"/><rect x="8" y="1" width="2" height="1" fill="#0B0D10"/><rect x="10" y="1" width="1" height="1" fill="#B6F2A6"/><rect x="11" y="1" width="4" height="1" fill="#7EE081"/><rect x="15" y="1" width="1" height="1" fill="#0B0D10"/><rect x="6" y="2" width="2" height="1" fill="#0B0D10"/><rect x="8" y="2" width="2" height="1" fill="#B6F2A6"/><rect x="10" y="2" width="2" height="1" fill="#7EE081"/><rect x="12" y="2" width="2" height="1" fill="#D8FFD0"/><rect x="14" y="2" width="1" height="1" fill="#4FAE5A"/><rect x="15" y="2" width="1" height="1" fill="#0B0D10"/><rect x="5" y="3" width="1" height="1" fill="#0B0D10"/><rect x="6" y="3" width="3" height="1" fill="#B6F2A6"/><rect x="9" y="3" width="2" height="1" fill="#7EE081"/><rect x="11" y="3" width="2" height="1" fill="#D8FFD0"/><rect x="13" y="3" width="2" height="1" fill="#4FAE5A"/><rect x="15" y="3" width="1" height="1" fill="#0B0D10"/><rect x="5" y="4" width="1" height="1" fill="#0B0D10"/><rect x="6" y="4" width="2" height="1" fill="#B6F2A6"/><rect x="8" y="4" width="2" height="1" fill="#7EE081"/><rect x="10" y="4" width="2" height="1" fill="#D8FFD0"/><rect x="12" y="4" width="2" height="1" fill="#4FAE5A"/><rect x="14" y="4" width="1" height="1" fill="#0B0D10"/><rect x="4" y="5" width="1" height="1" fill="#0B0D10"/><rect x="5" y="5" width="2" height="1" fill="#B6F2A6"/><rect x="7" y="5" width="2" height="1" fill="#7EE081"/><rect x="9" y="5" width="2" height="1" fill="#D8FFD0"/><rect x="11" y="5" width="3" height="1" fill="#4FAE5A"/><rect x="14" y="5" width="1" height="1" fill="#0B0D10"/><rect x="4" y="6" width="1" height="1" fill="#0B0D10"/><rect x="5" y="6" width="1" height="1" fill="#B6F2A6"/><rect x="6" y="6" width="2" height="1" fill="#7EE081"/><rect x="8" y="6" width="2" height="1" fill="#D8FFD0"/><rect x="10" y="6" width="4" height="1" fill="#4FAE5A"/><rect x="14" y="6" width="1" height="1" fill="#0B0D10"/><rect x="3" y="7" width="1" height="1" fill="#0B0D10"/><rect x="4" y="7" width="1" height="1" fill="#B6F2A6"/><rect x="5" y="7" width="2" height="1" fill="#7EE081"/><rect x="7" y="7" width="2" height="1" fill="#D8FFD0"/><rect x="9" y="7" width="4" height="1" fill="#4FAE5A"/><rect x="13" y="7" width="1" height="1" fill="#0B0D10"/><rect x="3" y="8" width="1" height="1" fill="#0B0D10"/><rect x="4" y="8" width="2" height="1" fill="#7EE081"/><rect x="6" y="8" width="2" height="1" fill="#D8FFD0"/><rect x="8" y="8" width="4" height="1" fill="#4FAE5A"/><rect x="12" y="8" width="1" height="1" fill="#0B0D10"/><rect x="3" y="9" width="1" height="1" fill="#0B0D10"/><rect x="4" y="9" width="1" height="1" fill="#7EE081"/><rect x="5" y="9" width="2" height="1" fill="#D8FFD0"/><rect x="7" y="9" width="4" height="1" fill="#4FAE5A"/><rect x="11" y="9" width="1" height="1" fill="#0B0D10"/><rect x="3" y="10" width="1" height="1" fill="#0B0D10"/><rect x="4" y="10" width="2" height="1" fill="#D8FFD0"/><rect x="6" y="10" width="4" height="1" fill="#4FAE5A"/><rect x="10" y="10" width="1" height="1" fill="#0B0D10"/><rect x="3" y="11" width="1" height="1" fill="#0B0D10"/><rect x="4" y="11" width="1" height="1" fill="#7EE081"/><rect x="5" y="11" width="2" height="1" fill="#4FAE5A"/><rect x="7" y="11" width="3" height="1" fill="#0B0D10"/><rect x="2" y="12" width="1" height="1" fill="#0B0D10"/><rect x="3" y="12" width="1" height="1" fill="#4FAE5A"/><rect x="4" y="12" width="3" height="1" fill="#0B0D10"/><rect x="1" y="13" width="1" height="1" fill="#0B0D10"/><rect x="2" y="13" width="1" height="1" fill="#4FAE5A"/><rect x="3" y="13" width="1" height="1" fill="#0B0D10"/><rect x="0" y="14" width="1" height="1" fill="#0B0D10"/><rect x="1" y="14" width="1" height="1" fill="#4FAE5A"/><rect x="2" y="14" width="1" height="1" fill="#0B0D10"/><rect x="1" y="15" width="1" height="1" fill="#0B0D10"/></g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.1 KiB |
Reference in new issue
Block a user