Something went wrong. Try again.
Monorepo for Aesthetic.Computer aesthetic.computer
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216#!/usr/bin/env node// design-loop — look at aesel's own furniture, change it, look again.//// aesel does not draw all of itself. The QR you scan, the little live card of// the piece, the stone that carries the session's status: those are Slab// menubar overlays parked on top of the terminal, and they are the first thing// anyone sees. Adjusting them used to be guesswork, for two reasons that// compounded://// 1. `frame` deliberately filters Slab's own overlays out of every capture —// the usual job is reading the machine's real content *beneath* them — so// a screenshot taken to check the card's padding showed the terminal// where the card is. Changing a number and looking at the result meant// looking at the wrong picture. `--overlays` is the opt-out, and this// tool always passes it.// 2. The overlays are placed once, when a session's window appears. Editing// the placement and reinstalling the menubar does not move the surfaces// already on screen in any way you can trust, so the only honest check is// a *fresh* session. That is the restart this tool performs.//// So: close the aesel session, open a new one, wait for its overlays to exist,// point the camera at them. One command per iteration, and the picture that// comes back is the thing being designed.//// node easel/bin/design-loop.mjs # restart aesel, then shoot// node easel/bin/design-loop.mjs --shot # shoot what is already open// node easel/bin/design-loop.mjs --out ~/x.jpg//// After an overlay edit the order is: install.sh in slab/menubar-swift, then// this. Reinstalling without restarting the session measures the old layout.
import { spawn } from "node:child_process";import { readFile, readdir, mkdir } from "node:fs/promises";import { homedir, tmpdir } from "node:os";import { join, dirname } from "node:path";import { fileURLToPath } from "node:url";
const REPO = join(dirname(fileURLToPath(import.meta.url)), "..", "..");const FRAME = join(REPO, "slab", "bin", "frame.mjs");const MARKERS = join(homedir(), ".local", "share", "slab", "state", "active-prompts");const LEDGER = join(homedir(), ".config", "slab", "ledger", "local.json");
// The resident prox daemon, shared by every Claude session on this machine.// Going through it rather than reimplementing close/launch means this tool// inherits prox's own refusals — it will not close the session calling it, and// it will not launch anything outside the allowlist.const PROX = "http://127.0.0.1:7773";
const argv = process.argv.slice(2);const flag = (name) => argv.includes(name);const opt = (name) => { const i = argv.indexOf(name); return i === -1 ? null : argv[i + 1]; };
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
async function prox(name, args = {}) { const response = await fetch(`${PROX}/`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ jsonrpc: "2.0", id: Date.now(), method: "tools/call", params: { name, arguments: args }, }), signal: AbortSignal.timeout(30_000), }).catch((e) => { throw new Error(`prox daemon unreachable on ${PROX} — is the menubar's MCP up? (${e.message})`); }); const body = await response.json(); const text = (body?.result?.content || []).map((c) => c.text || "").join("\n"); if (body?.result?.isError) throw new Error(`${name}: ${text}`); return text;}
/// Every live aesel session on this machine, newest marker first. Read from/// the marker files rather than asked of prox: this runs between a close and a/// launch, when the ledger cache is the one thing guaranteed to be stale.async function aeselSessions() { let names = []; try { names = await readdir(MARKERS); } catch { return []; } const out = []; for (const name of names) { try { const marker = JSON.parse(await readFile(join(MARKERS, name), "utf8")); if (marker.agent_type !== "easel" && marker.agent_type !== "aesel") continue; out.push({ ...marker, id: marker.session_id || name }); } catch { /* a marker mid-write is not an error, it is a retry */ } } return out.sort((a, b) => String(b.updated || "").localeCompare(String(a.updated || "")));}
/// Wait for an aesel session that is not one we already knew about, and that/// has got far enough to own a terminal and a piece. `scan_url` is the signal/// that matters: it is set at the moment the session has an address to encode,/// which is the moment the QR and the preview card come into existence. Waiting/// on the marker's mere existence would photograph the overlays mid-placement.async function waitForFreshSession(known, timeoutMs = 45_000) { const deadline = Date.now() + timeoutMs; while (Date.now() < deadline) { for (const session of await aeselSessions()) { if (known.has(session.id)) continue; if (session.tty && session.scan_url) return session; } await sleep(500); } return null;}
/// Bring a session's Terminal window to the front by its tty, because the/// window id is not in the marker and the tty is. A frame of an unfocused/// window still captures pixels, but the overlays belong to whichever window/// is on top — photographing a buried one photographs somebody else's rock.async function focusTty(tty) { const script = ` tell application "Terminal" activate repeat with w in windows repeat with t in tabs of w if tty of t is "/dev/${tty}" then set selected tab of w to t set index of w to 1 return "ok" end if end repeat end repeat return "missing" end tell`; const result = await run("osascript", ["-e", script]); return result.stdout.trim() === "ok";}
function run(command, args) { return new Promise((resolve, reject) => { const child = spawn(command, args, { stdio: ["ignore", "pipe", "pipe"] }); let stdout = "", stderr = ""; child.stdout.on("data", (d) => { stdout += d; }); child.stderr.on("data", (d) => { stderr += d; }); child.on("error", reject); child.on("close", (code) => { if (code === 0) resolve({ stdout, stderr }); else reject(new Error(`${command} exited ${code}: ${stderr.trim().slice(0, 400)}`)); }); });}
/// The capture itself. `--overlays` is the whole point; `--quiet-overlay` keeps/// OCR from drawing its own boxes over the thing being judged, and `--no-ocr`/// skips the text pass entirely, because this picture is read by an eye.async function shoot(out) { await mkdir(dirname(out), { recursive: true }).catch(() => {}); const { stdout } = await run(process.execPath, [ FRAME, "local", "--overlays", "--no-ocr", "--quiet-overlay", "--json", "--out", out, ]); let envelope = {}; try { envelope = JSON.parse(stdout); } catch { /* the file is what matters */ } return envelope;}
async function selfHost() { try { return JSON.parse(await readFile(LEDGER, "utf8")).host; } catch { return null; }}
const out = opt("--out") || join(tmpdir(), `easel-design-${Date.now()}.jpg`);
if (flag("--help") || flag("-h")) { console.log([ "design-loop — restart aesel and photograph its overlays", "", " node easel/bin/design-loop.mjs [--shot] [--out file.jpg] [--cwd dir]", "", " --shot: skip the restart and shoot the session already open", " --out: where the JPEG lands (default: a temp file, path printed)", " --cwd: working directory for the new session (default: this repo)", " --settle: seconds to let the overlays land first (default 4)", "", " Edit an overlay, run slab/menubar-swift/install.sh, then run this.", ].join("\n")); process.exit(0);}
let session = (await aeselSessions())[0] || null;
if (!flag("--shot")) { const host = await selfHost(); if (!host) throw new Error(`no local ledger at ${LEDGER} — is the Slab menubar running?`); const known = new Set((await aeselSessions()).map((s) => s.id));
if (session) { console.log(`⟲ closing ${host}:easel (${session.piece || session.id.slice(0, 8)})`); await prox("prox_close", { handle: session.id }); } else { console.log("⟲ no aesel session open — launching a first one"); }
console.log("⟳ launching a fresh aesel"); await prox("prox_launch", { host, agent: "easel", cwd: opt("--cwd") || REPO, by: "easel:design-loop" });
session = await waitForFreshSession(known); if (!session) throw new Error("the new aesel never reported a scan URL — nothing to photograph."); // The marker is written the moment the address exists; the overlays are // placed on the menubar's next walk of the window list, which is a separate // clock this tool cannot read. Measured at about three seconds on blueberry — // a shot at 1.5s caught the terminal with no card and no QR on it, which is // the same wrong picture the `--overlays` flag exists to prevent. Four is // that with room. If the overlays still have not landed, `--shot` takes // another look without paying for another session. await sleep(Number(opt("--settle")) * 1000 || 4000);}
if (!session) throw new Error("no aesel session is open — drop --shot to launch one.");if (!(await focusTty(session.tty))) { console.log(`! could not focus /dev/${session.tty} — shooting whatever is frontmost`);}
const envelope = await shoot(out);console.log(`📷 ${out} (${envelope.capture_scope || "?"})`);console.log(` ${session.handle ? "@" + session.handle + "/" : ""}${session.piece || "?"} · ${session.scan_url || "no scan url"}`);