- Settings… lists the folders the agent may work in, with the system's folder dialog, remove, max mode and terminal, all through 'lembas remote … --json' (CLI 1.2.0). Turning the agent on with no folder opens it; the title bar says 'agent: no folders' instead of green. - The content webview's User-Agent carries LLeMbasDesktop/<v> (host=…), so the server can show this computer's device as Local. - The title bar menu pops up at the button (popup_menu_at): Wayland gave the pointer-less popup the middle of the window. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
13 files changed
+419
-17
No files matched your search
+51
-1
@@ -33,7 +33,7 @@ p { margin: 0; }
|
||||
|
||||
/* 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); }
|
||||
label, .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); }
|
||||
|
||||
@@ -77,3 +77,53 @@ input:focus-visible, .btn:focus-visible { outline: 2px solid var(--accent); outl
|
||||
.row { grid-template-columns: 1fr; grid-template-rows: var(--control-h) var(--control-h); }
|
||||
.field { grid-template-rows: auto auto auto; }
|
||||
}
|
||||
|
||||
/* The folder list: every row the same two tracks, the path cut from the left (its end is the
|
||||
part that tells folders apart), the full path in the title. */
|
||||
.folders { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--sp-2); }
|
||||
.folders li {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
gap: var(--sp-2);
|
||||
align-items: center;
|
||||
min-height: var(--control-h);
|
||||
padding: 0 0 0 var(--sp-3);
|
||||
background: var(--bg);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
.folders .path {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
direction: rtl;
|
||||
text-align: left;
|
||||
font: var(--text-sm) var(--font-ui);
|
||||
}
|
||||
.folders .path bdi { direction: ltr; unicode-bidi: isolate; }
|
||||
.folders .btn { height: calc(var(--control-h) - 2px); box-shadow: none; border-width: 0 0 0 1px; }
|
||||
.folders .empty { padding: var(--sp-3); color: var(--warning); font-size: var(--text-sm); }
|
||||
|
||||
.settings-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(min(100%, 14rem), 1fr));
|
||||
grid-auto-rows: auto var(--control-h) auto;
|
||||
column-gap: var(--sp-4);
|
||||
row-gap: var(--sp-2);
|
||||
margin-top: var(--sp-4);
|
||||
}
|
||||
/* Each field spans three shared rows (label, control, hint), so the controls of side-by-side
|
||||
fields sit on one line whatever their labels and hints do. */
|
||||
.settings-grid .field { grid-row: span 3; grid-template-rows: subgrid; }
|
||||
select {
|
||||
height: var(--control-h);
|
||||
min-width: 0;
|
||||
padding: 0 var(--sp-2);
|
||||
font: var(--text-sm) var(--font-ui);
|
||||
color: var(--ink);
|
||||
background: var(--bg);
|
||||
border: 1px solid var(--border-strong);
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
.check { display: flex; align-items: center; gap: var(--sp-2); color: var(--ink); }
|
||||
.check input { width: 1.1rem; height: 1.1rem; margin: 0; accent-color: var(--accent); }
|
||||
@@ -20,13 +20,53 @@ 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 noFolders = s.remote && !(s.remote.enabled && s.remote.roots.length)
|
||||
const [text, cls] =
|
||||
s.agent.state === "running" && noFolders
|
||||
? ["The agent is connected, but may open no folder yet: add one below.", "is-error"]
|
||||
: (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
|
||||
renderFolders(s)
|
||||
}
|
||||
|
||||
function renderFolders(s) {
|
||||
const r = s.remote
|
||||
$("folders-section").hidden = !s.server || !s.linked || !r
|
||||
if (!r) return
|
||||
const list = $("folders")
|
||||
list.replaceChildren()
|
||||
if (!r.enabled || !r.roots.length) {
|
||||
const li = document.createElement("li")
|
||||
li.className = "empty"
|
||||
li.textContent = "No folder yet: the agent can open nothing. Add the folder of a project."
|
||||
list.append(li)
|
||||
}
|
||||
for (const dir of r.enabled ? r.roots : []) {
|
||||
const li = document.createElement("li")
|
||||
const path = document.createElement("span")
|
||||
path.className = "path"
|
||||
path.title = dir
|
||||
const inner = document.createElement("bdi")
|
||||
inner.textContent = dir
|
||||
path.append(inner)
|
||||
const remove = document.createElement("button")
|
||||
remove.type = "button"
|
||||
remove.className = "btn"
|
||||
remove.textContent = "Remove"
|
||||
remove.setAttribute("aria-label", `Remove ${dir}`)
|
||||
remove.addEventListener("click", () => invoke("remote_remove", { dir }))
|
||||
li.append(path, remove)
|
||||
list.append(li)
|
||||
}
|
||||
if (r.max_mode) $("max-mode").value = r.max_mode
|
||||
$("terminal").checked = !!r.terminal
|
||||
$("folders-status").className = "status"
|
||||
$("folders-status").textContent = ""
|
||||
}
|
||||
|
||||
async function refresh() {
|
||||
@@ -70,5 +110,14 @@ $("open").addEventListener("click", async () => {
|
||||
if (s.server) await invoke("set_server", { address: s.server })
|
||||
})
|
||||
|
||||
$("add-folder").addEventListener("click", () => invoke("remote_add"))
|
||||
$("max-mode").addEventListener("change", (e) => invoke("remote_mode", { mode: e.target.value }))
|
||||
$("terminal").addEventListener("change", (e) => invoke("remote_terminal", { on: e.target.checked }))
|
||||
|
||||
listen("remote-error", (e) => {
|
||||
$("folders-status").className = "status is-error"
|
||||
$("folders-status").textContent = String(e.payload)
|
||||
})
|
||||
listen("status", (e) => render(e.payload))
|
||||
refresh()
|
||||
invoke("remote_refresh")
|
||||
@@ -43,6 +43,35 @@
|
||||
<button type="button" class="btn btn--primary" id="open">Open the server</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section aria-labelledby="folders-h" id="folders-section" hidden>
|
||||
<h2 id="folders-h">[ PROJECT FOLDERS ]</h2>
|
||||
<p class="muted">Agent chats from your server can work in these folders and anything inside
|
||||
them — nowhere else on this computer. Start one in the server's Agents tab, on
|
||||
<strong>Local</strong>.</p>
|
||||
<ul class="folders" id="folders"></ul>
|
||||
<p class="status" id="folders-status" aria-live="polite"></p>
|
||||
<div class="row row--buttons">
|
||||
<button type="button" class="btn btn--primary" id="add-folder">Add folder…</button>
|
||||
</div>
|
||||
<div class="settings-grid">
|
||||
<div class="field">
|
||||
<label for="max-mode">The most an agent chat may do here</label>
|
||||
<select id="max-mode">
|
||||
<option value="manual">Manual — asks first</option>
|
||||
<option value="edit">Edit — asks before commands</option>
|
||||
<option value="auto">Auto — asks nothing</option>
|
||||
<option value="plan">Plan — changes nothing</option>
|
||||
</select>
|
||||
<p class="hint">A chat asking for more is lowered to this.</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<span class="label">Terminal</span>
|
||||
<label class="check"><input type="checkbox" id="terminal"> Let the server open a terminal here</label>
|
||||
<p class="hint">A shell as you, in one of these folders.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
+2
-1
@@ -64,7 +64,8 @@ button:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
|
||||
.bar__agent[data-state="elsewhere"] .dot { background: var(--leaf); }
|
||||
.bar__agent[data-state="starting"] .dot { background: var(--accent); }
|
||||
.bar__agent[data-state="failed"] .dot,
|
||||
.bar__agent[data-state="not_linked"] .dot { background: var(--warning); }
|
||||
.bar__agent[data-state="not_linked"] .dot,
|
||||
.bar__agent[data-state="no_folders"] .dot { background: var(--warning); }
|
||||
|
||||
.bar__update {
|
||||
padding: 0 var(--sp-3);
|
||||
|
||||
+11
-3
@@ -16,6 +16,7 @@ const WORDS = {
|
||||
running: "agent running",
|
||||
elsewhere: "agent running (service)",
|
||||
failed: "agent stopped",
|
||||
no_folders: "agent: no folders",
|
||||
}
|
||||
|
||||
function render(s) {
|
||||
@@ -25,7 +26,9 @@ function render(s) {
|
||||
} catch {}
|
||||
$("host").textContent = host
|
||||
$("host").title = s.server || ""
|
||||
const state = s.agent.state
|
||||
// Connected but allowed into no folder: a green light would say it works when it cannot.
|
||||
const noFolders = s.remote && !(s.remote.enabled && s.remote.roots.length)
|
||||
const state = s.agent.state === "running" && noFolders ? "no_folders" : s.agent.state
|
||||
$("agent").dataset.state = state
|
||||
$("agent-text").textContent = WORDS[state] ?? state
|
||||
$("agent").title = state === "failed" ? `Agent stopped: ${s.agent.detail}` : "Agent"
|
||||
@@ -45,8 +48,13 @@ $("minimize").addEventListener("click", () => win.minimize())
|
||||
$("maximize").addEventListener("click", () => win.toggleMaximize())
|
||||
$("close").addEventListener("click", () => win.close())
|
||||
$("update").addEventListener("click", () => invoke("update_app"))
|
||||
$("menu").addEventListener("click", () => invoke("titlebar_menu"))
|
||||
$("agent").addEventListener("click", () => invoke("titlebar_menu"))
|
||||
// The menu opens under the button that was clicked: its bottom-left corner, in page pixels.
|
||||
function menuAt(button) {
|
||||
const r = button.getBoundingClientRect()
|
||||
return invoke("titlebar_menu", { x: r.left, y: r.bottom })
|
||||
}
|
||||
$("menu").addEventListener("click", (e) => menuAt(e.currentTarget))
|
||||
$("agent").addEventListener("click", (e) => menuAt(e.currentTarget))
|
||||
|
||||
listen("status", (e) => render(e.payload))
|
||||
listen("maximized", (e) => maximized(!!e.payload))
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@
|
||||
--radius: 2px;
|
||||
--press: 2px;
|
||||
--control-h: 2.5rem;
|
||||
--card-max: 34rem;
|
||||
--card-max: 40rem;
|
||||
|
||||
color-scheme: dark;
|
||||
--bg: #0B0D10;
|
||||
|
||||
Reference in new issue
Block a user