From d2edd97ac57e880327f4185c748505e13593cf44 Mon Sep 17 00:00:00 2001 From: "prompt.ac/@jeffrey" Date: Mon, 28 Sep 2026 16:10:42 -0700 Subject: [PATCH] Aesel 0.8.15: ac check runs a piece and prints its errors; the launcher stops forcing claude MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ac check plays a piece in headless Chrome on aesthetic.computer (a local file is served in place of a scratch piece, bypassing the site's service worker), taps and presses space, and prints every page error, console.error and AC's own caught failures ("🎨 Paint failure…" warnings, with the real error and location), once each with a count. The bumblebee parsed and published but threw in wipe() every frame; this finds it before and after. - Pro's loop is now write β†’ ac check β†’ fix until βœ“ β†’ publish β†’ ac check the published piece; the context map says API functions exist only as parameters (the bumblebee's helper called a wipe it never received). - The launcher passed --backend claude on every launch, so the TUI ignored the provider you last chose and /update came back on Claude. It now passes a backend only when asked; a first launch starts on the hosted provider. - Aesel writes agent_type "aesel" now that the fleet's menubars read it; prox prints sessionΒ·aesel and prox:aesel:… addresses. - Clicking the update banner runs /update. Co-Authored-By: Claude Opus 5.5 --- aesel/ACCOUNT.md | 2 + aesel/bin/easel | 16 +++-- aesel/context/account.md | 2 + aesel/package.json | 2 +- aesel/src/backends.mjs | 4 +- aesel/src/check-piece.mjs | 104 +++++++++++++++++++++++++++++++ aesel/src/cli.mjs | 11 ++++ aesel/src/render.mjs | 16 ++++- aesel/src/slab-session.mjs | 13 ++-- aesel/src/tui.mjs | 5 +- aesel/src/workspace-tools.mjs | 3 +- aesel/test/cli.sh | 8 +-- aesel/test/slab-session.test.mjs | 2 +- slab/bin/prox-mcp.mjs | 10 +-- 14 files changed, 167 insertions(+), 31 deletions(-) create mode 100644 aesel/src/check-piece.mjs diff --git a/aesel/ACCOUNT.md b/aesel/ACCOUNT.md index 80e12df43e..eb40c86ca3 100644 --- a/aesel/ACCOUNT.md +++ b/aesel/ACCOUNT.md @@ -11,6 +11,8 @@ ac colors blue cyan # the @handle's letter colours: names or hex, cycling ac colors "#3b6cf0" teal # (orange, teal, pink, gold, purple, … or #rrggbb) ac mood "working on a jump game" # set their mood (shows on their profile) ac handle newname # change their handle β€” ask first; it changes their URLs +ac check piece.mjs # run a piece in a browser; prints every error it throws +ac check @handle/slug # (or a published piece) ac publish piece.mjs slug # publish a piece to aesthetic.computer/@handle/slug ``` diff --git a/aesel/bin/easel b/aesel/bin/easel index 526c96cc89..da5778ada6 100755 --- a/aesel/bin/easel +++ b/aesel/bin/easel @@ -3,7 +3,10 @@ set -euo pipefail # AESEL_* first; EASEL_* is the same setting under the old name. -DEFAULT_BACKEND="${AESEL_BACKEND:-${EASEL_BACKEND:-claude}}" +# Empty unless asked for: the interface remembers the provider you last +# chose, and a first launch starts on the hosted one, which needs nothing +# installed. Passing a default here overrode that memory on every launch. +DEFAULT_BACKEND="${AESEL_BACKEND:-${EASEL_BACKEND:-}}" DEFAULT_CLAUDE_MODEL="claude-opus-5" script_path="${BASH_SOURCE[0]}" while [[ -L "$script_path" ]]; do @@ -110,7 +113,8 @@ doctor() { printf 'engine bridge ac: needs an @handle β€” run ac, then /login\n' fi - printf 'default engine bridge: %s (%s)\n' "$DEFAULT_BACKEND" "$DEFAULT_CLAUDE_MODEL" + if [[ -n "$DEFAULT_BACKEND" ]]; then printf 'default engine bridge: %s\n' "$DEFAULT_BACKEND" + else printf 'default engine bridge: the one you chose last (first launch: aesthetic)\n'; fi if has_command node; then printf 'account: %s\n' "$(node "$PROJECT_DIR/src/cli.mjs" whoami 2>/dev/null || printf 'not signed in')" @@ -137,7 +141,7 @@ case "${1:-}" in shift exec node "$PROJECT_DIR/src/history-cli.mjs" "$@" ;; - login|logout|whoami|publish|colors|profile|mood|handle) + login|logout|whoami|publish|colors|profile|mood|handle|check) has_command node || fail "Node.js is required" exec node "$PROJECT_DIR/src/cli.mjs" "$@" ;; @@ -275,7 +279,7 @@ esac # Aesthetic Computer's own account over HTTP, so there is nothing to install and # a missing binary is not how it fails β€” it fails on a missing @handle, in the # interface, where that can be explained. -if [[ "$selected_backend" != "ac" && "$selected_backend" != "open" ]]; then +if [[ -n "$selected_backend" && "$selected_backend" != "ac" && "$selected_backend" != "open" ]]; then has_command "$selected_backend" || fail "the $selected_backend engine bridge is not installed" fi @@ -291,7 +295,7 @@ if [[ "${AESEL_DRY_RUN:-${EASEL_DRY_RUN:-0}}" == "1" ]]; then printf 'runtime=%s\n' "$([[ "$genre" == nopaint ]] && printf mjs || printf '%s' "${runtime:-mjs}")" printf 'genre=%s\n' "${genre:-choose}" printf 'autopublish=%s\n' "${autopublish:-$([[ "${AESEL_AUTOPUBLISH:-${EASEL_AUTOPUBLISH:-0}}" =~ ^(1|on|true|yes)$ ]] && printf on || printf off)}" - printf 'backend=%s\n' "$selected_backend" + printf 'backend=%s\n' "${selected_backend:-remembered}" printf 'model=%s\n' "${model:-$([[ "$selected_backend" == claude ]] && printf '%s' "$DEFAULT_CLAUDE_MODEL")}" printf 'pro=%s\n' "${pro:-off}" printf 'private=%s\n' "${private:-off}" @@ -313,6 +317,6 @@ if [[ "$autopublish" == "on" ]]; then arguments+=("--autopublish"); fi if [[ "$autopublish" == "off" ]]; then arguments+=("--no-autopublish"); fi if [[ "$pro" == "on" ]]; then arguments+=("--pro"); fi if [[ "$private" == "on" ]]; then arguments+=("--private"); fi -arguments+=("--backend" "$selected_backend") +if [[ -n "$selected_backend" ]]; then arguments+=("--backend" "$selected_backend"); fi if [[ -n "$model" ]]; then arguments+=("--model" "$model"); fi exec node "$PROJECT_DIR/src/tui.mjs" "${arguments[@]}" diff --git a/aesel/context/account.md b/aesel/context/account.md index 02cb527db0..2f23d3a48a 100644 --- a/aesel/context/account.md +++ b/aesel/context/account.md @@ -15,6 +15,8 @@ ac colors blue cyan # the @handle's letter colours: names or hex, cycling ac colors "#3b6cf0" teal # (orange, teal, pink, gold, purple, … or #rrggbb) ac mood "working on a jump game" # set their mood (shows on their profile) ac handle newname # change their handle β€” ask first; it changes their URLs +ac check piece.mjs # run a piece in a browser; prints every error it throws +ac check @handle/slug # (or a published piece) ac publish piece.mjs slug # publish a piece to aesthetic.computer/@handle/slug ``` diff --git a/aesel/package.json b/aesel/package.json index f856c37772..a7006fece5 100644 --- a/aesel/package.json +++ b/aesel/package.json @@ -1,6 +1,6 @@ { "name": "aesel", - "version": "0.8.14", + "version": "0.8.15", "private": true, "type": "module", "scripts": { diff --git a/aesel/src/backends.mjs b/aesel/src/backends.mjs index 7f0783009c..53f9df8dfb 100644 --- a/aesel/src/backends.mjs +++ b/aesel/src/backends.mjs @@ -75,7 +75,9 @@ export const BACKEND_ALIASES = { openrouter: "open", }; -export const DEFAULT_BACKEND = "claude"; +// A first launch with nothing remembered: the hosted provider, which needs +// no vendor CLI or subscription β€” only an @handle. +export const DEFAULT_BACKEND = "ac"; // A model for the hosted provider: one of the open models, by name or id, or // "" (Automatic) for anything else β€” a remembered older model included. diff --git a/aesel/src/check-piece.mjs b/aesel/src/check-piece.mjs new file mode 100644 index 0000000000..9ec7b32271 --- /dev/null +++ b/aesel/src/check-piece.mjs @@ -0,0 +1,104 @@ +// check-piece.mjs β€” run a piece for real and report what its console says. +// +// `node --check` only proves a piece parses. The bumblebee parsed, published, +// and threw in the browser, and nothing in Aesel could see it. This opens the +// piece in headless Chrome on aesthetic.computer, lets it run, taps and presses +// a key so act() and sim() are exercised, and returns every page error and +// console error it printed β€” the same errors a person would see. +// +// A local file is checked before it is published: the page asks for a scratch +// piece, and the request for that piece's source is answered with the file. +import { existsSync, readFileSync } from "node:fs"; +import { createRequire } from "node:module"; +import { basename, dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +const SITE = "https://aesthetic.computer"; +const CHROME = "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"; + +// Puppeteer lives in the repository's node_modules; a tarball install has none. +function loadPuppeteer() { + for (let dir = dirname(fileURLToPath(import.meta.url)); dir !== dirname(dir); dir = dirname(dir)) { + const candidate = resolve(dir, "node_modules/puppeteer/package.json"); + if (existsSync(candidate)) return createRequire(candidate)("puppeteer"); + } + return null; +} + +export async function checkPiece(target, { seconds = 4, cwd = process.cwd() } = {}) { + const puppeteer = loadPuppeteer(); + if (!puppeteer) throw new Error("ac check needs the aesthetic-computer checkout (puppeteer); this install has none."); + const file = /\.(mjs|lisp)$/.test(target) ? resolve(cwd, target) : ""; + if (file && !existsSync(file)) throw new Error(`no such file: ${target}`); + const scratch = `aesel-check-${Date.now().toString(36)}`; + const address = file ? `${SITE}/${scratch}` : target.startsWith("http") ? target : `${SITE}/${target.replace(/^\/+/, "")}`; + const url = `${address}${address.includes("?") ? "&" : "?"}nogap=true&nolabel=true&noauth=true`; + + const browser = await puppeteer.launch({ + headless: "new", + ...(existsSync(CHROME) ? { executablePath: CHROME } : {}), + args: ["--no-sandbox", "--mute-audio", "--autoplay-policy=no-user-gesture-required"], + timeout: 90000, + protocolTimeout: 90000, + }); + // A failing paint() or sim() fails every frame; say it once, with a count. + const seen = new Map(), details = new Map(); + const note = (kind, text) => { + const line = `${kind}: ${String(text).trim().split("\n").slice(0, 2).join(" Β· ").slice(0, 400)}`; + seen.set(line, (seen.get(line) || 0) + 1); + }; + try { + const page = await browser.newPage(); + await page.setViewport({ width: 480, height: 360, deviceScaleFactor: 1 }); + if (file) { + // The site's service worker would fetch the piece itself, out of reach + // of the interception below. + await page.setBypassServiceWorker(true); + const source = readFileSync(file, "utf8"); + const ext = file.endsWith(".lisp") ? ".lisp" : ".mjs"; + await page.setRequestInterception(true); + page.on("request", (request) => { + const path = new URL(request.url()).pathname; + if (path.endsWith(`/${scratch}${ext}`)) { + request.respond({ status: 200, contentType: ext === ".mjs" ? "text/javascript" : "text/plain", body: source }); + } else request.continue(); + }); + } + page.on("pageerror", (error) => note("error", error.message)); + page.on("console", async (message) => { + const type = message.type(), text = message.text(); + // The page's own network chatter (analytics, auth, sockets) is not the piece. + if (/favicon|net::ERR|Failed to load resource|firebase|auth0|socket|session-server|WebSocket/i.test(text)) return; + // AC runs the piece in a worker, catches what it throws, and reports it + // as a warning β€” "🎨 Paint failure…" with the error object attached. + const failure = type === "warn" && /failure|error|exception/i.test(text); + if (type !== "error" && !failure) return; + // Read the attached error once per distinct warning; the rest just count. + const key = failure ? text : `error:${text}`; + if (details.has(key)) { note(details.get(key).kind, details.get(key).detail); return; } + details.set(key, { kind: failure ? text.replace(/\s*JSHandle@\w+/g, "").replace(/\.\.\.$/, "").trim() : "console.error", detail: text }); + let detail = ""; + for (const arg of message.args()) { + const value = await arg.evaluate((v) => v instanceof Error ? `${v.name}: ${v.message}\n${(v.stack || "").split("\n").find((l) => /\.mjs|\.lisp|blob:|disks\//.test(l)) || ""}` : "").catch(() => ""); + if (value) { detail = value; break; } + } + const entry = details.get(key); + entry.detail = detail || text; + note(entry.kind, entry.detail); + }); + await page.goto(url, { waitUntil: "domcontentloaded", timeout: 30000 }); + await page.waitForFunction(() => window.acBOOTED === true, { timeout: 30000 }).catch(() => note("error", "the piece never finished booting")); + await new Promise((r) => setTimeout(r, 1200)); + // Exercise input once: a tap in the middle and a space. + await page.mouse.click(240, 180).catch(() => {}); + await page.keyboard.press("Space").catch(() => {}); + await new Promise((r) => setTimeout(r, Math.max(1000, seconds * 1000 - 1200))); + } finally { + await browser.close(); + } + // A warning whose error was gone before it could be read says nothing the + // readable one did not; drop it when a readable one exists. + const lines = [...seen].filter(([line]) => !/JSHandle@/.test(line) || ![...seen.keys()].some((other) => other !== line && !/JSHandle@/.test(other))); + const problems = lines.map(([line, count]) => count > 1 ? `${line} (Γ—${count})` : line); + return { target: file ? basename(file) : address, problems }; +} diff --git a/aesel/src/cli.mjs b/aesel/src/cli.mjs index bd003c5025..e74e00c33a 100644 --- a/aesel/src/cli.mjs +++ b/aesel/src/cli.mjs @@ -52,6 +52,17 @@ try { out(result.route); if (!result.verified) note("published, but the live file did not read back yet"); } + } else if (command === "check") { + // Run a piece for real (headless Chrome) and print its errors: a local + // file before publishing, or a published @handle/slug or URL after. + const target = rest[0]; + if (!target) fail("usage: ac check "); + const { checkPiece } = await import("./check-piece.mjs"); + const { target: name, problems } = await checkPiece(target); + if (!problems.length) { out(`βœ“ ${name} ran without errors`); process.exit(0); } + out(`βœ• ${name}: ${problems.length} problem${problems.length === 1 ? "" : "s"}`); + for (const p of problems) out(` ${p}`); + process.exit(1); } else if (command === "profile") { // What your profile shows, read from the same public endpoints the // profile page uses: handle, colours, latest mood, and where it lives. diff --git a/aesel/src/render.mjs b/aesel/src/render.mjs index 41445b7638..2d97131be2 100644 --- a/aesel/src/render.mjs +++ b/aesel/src/render.mjs @@ -960,18 +960,26 @@ export function dropdownGeometry(state, width, height, shape = state.layout || { // A newer Aesel is ready: a banner in the middle of the conversation, flashing // red and yellow on a slow beat so a window left open for a week still says so. // It holds until /update restarts into the new code or /update later hides it. -function paintUpdate(rows, state, width, area, useColor) { +function updateBanner(state, width, area) { const update = state.update; - if (!update || update.dismissed) return rows; + if (!update || update.dismissed) return null; const lines = [ `✦ Aesel ${update.version} is ready ✦`, update.kind === "checkout" ? "/update restarts into it Β· this thread comes with you" : "/update installs it and restarts Β· this thread comes with you", "/update later hides this", ]; const inner = Math.min(width - 4, Math.max(...lines.map((line) => textWidth(line))) + 4); - if (inner < 20 || area < lines.length + 2) return rows; + if (inner < 20 || area < lines.length + 2) return null; const x = Math.max(0, Math.floor((width - inner) / 2)); const top = Math.max(0, Math.floor((area - lines.length - 2) / 2)); + return { lines, inner, x, top, height: lines.length + 2 }; +} + +// The banner is also a button: a click on it is /update. +function paintUpdate(rows, state, width, area, useColor) { + const banner = updateBanner(state, width, area); + if (!banner) return rows; + const { lines, inner, x, top } = banner; const red = Math.floor((state.nowMs ?? Date.now()) / 650) % 2 === 0; const paintRow = (text) => useColor ? `${bg(red ? [215, 40, 50] : [250, 205, 40])}${fg(red ? [255, 250, 235] : [40, 20, 10])}\x1b[1m${fit(` ${text}`, inner)}\x1b[22m${color.reset}${color.ground}` @@ -1240,6 +1248,8 @@ export function headerAction(state, columns, rows, x, y) { if (row >= 0 && row < g.count && x >= g.x + 1 && x <= g.x + g.boxWidth) return `pick:${g.start + row}`; return "dismiss"; } + const banner = updateBanner(state, Math.max(32, columns), Math.max(10, rows) - shape.bottom.length); + if (banner && y - 1 >= banner.top && y - 1 < banner.top + banner.height && x - 1 >= banner.x && x - 1 < banner.x + banner.inner) return "update"; const row = shape.bottom.lastIndexOf("status"); if (row < 0 || y !== rows - (shape.bottom.length - 1 - row)) return ""; const hit = proStatus(state, Math.max(32, columns), false, shape).spans.find((span) => (span.name === "model" || span.name === "engine") && x >= span.x + 1 && x <= span.x + span.width); diff --git a/aesel/src/slab-session.mjs b/aesel/src/slab-session.mjs index 230f1aec67..5c818c99c1 100644 --- a/aesel/src/slab-session.mjs +++ b/aesel/src/slab-session.mjs @@ -60,15 +60,14 @@ export class SlabSession { this.record = { session_id: sessionId, cwd, - subject: this.private ? "private" : "easel", - summary: this.private ? "private" : "easel", + subject: this.private ? "private" : "aesel", + summary: this.private ? "private" : "aesel", tty, agent_pid: pid, - // TODO(aesel rename): write "aesel" here (and SLAB_AGENT_TYPE in tui.mjs) - // only once every fleet menubar and prox-mcp runs a build that accepts - // both β€” `npm run menubar:parity -- audit` shows each machine's build. - // An older menubar drops a rock whose agent_type it does not know. - agent_type: "easel", + // "aesel" since 2026-09-28, once blueberry, neo and frisbee ran menubars + // that read both names. A menubar from before then drops the rock: + // update it with `npm run menubar:parity -- deploy `. + agent_type: "aesel", ...(process.env.AESEL_DESKTOP === '1' ? { host_app:'computer.aesthetic.easel', host_bundle_id:process.env.AESEL_HOST_BUNDLE_ID || 'computer.aesthetic.easel', diff --git a/aesel/src/tui.mjs b/aesel/src/tui.mjs index ecf8cbe35c..21a6576966 100755 --- a/aesel/src/tui.mjs +++ b/aesel/src/tui.mjs @@ -528,7 +528,7 @@ function proInstructions() { // What Aesel is for. Without this a model asked for "a jumping game" built // an HTML page, served it and read screenshots β€” nothing anyone could open // on Aesthetic Computer. - `When asked to make something to see, play or hear β€” a game, a toy, a drawing, an animation, an instrument β€” make it as an Aesthetic Computer piece: one .mjs file using the piece API, not an HTML page. The guides are in ${path.join(aeselRoot, "context")}: read pieces.md first (the piece lifecycle and API) and skim screen.md (layout); for any function's signature or an example, call the ac_api tool β€” don't search api.json yourself. Write a first working version early, then improve it. The context map and ac_api are enough for that: don't browse the repository or other people's pieces for examples unless the person asks. Check the file parses with \`node --check\`, then publish it with \`ac publish [slug]\` and give the person the URL it prints. Only make other kinds of files when they ask for them.`, + `When asked to make something to see, play or hear β€” a game, a toy, a drawing, an animation, an instrument β€” make it as an Aesthetic Computer piece: one .mjs file using the piece API, not an HTML page. The guides are in ${path.join(aeselRoot, "context")}: read pieces.md first (the piece lifecycle and API) and skim screen.md (layout); for any function's signature or an example, call the ac_api tool β€” don't search api.json yourself. Write a first working version early, then improve it. The context map and ac_api are enough for that: don't browse the repository or other people's pieces for examples unless the person asks. Then run it: \`ac check \` plays the piece in a browser and prints every error it throws. Fix each one and check again until it prints βœ“ β€” a piece that only parses is not done. Then publish with \`ac publish [slug]\`, run \`ac check @handle/slug\` once on the published piece, and give the person the URL. Only make other kinds of files when they ask for them.`, `For the person's own Aesthetic Computer account or profile β€” their handle's colours, their mood, their handle β€” read account.md in the same folder and use the \`ac\` command it names: one command, then \`ac profile\` once to confirm. Don't search the code or poll the API for it.`, "Write plainly. Short sentences, one idea each, in the order they matter. Say the thing and stop. No headings, no bold, no bullet lists unless the items are truly parallel, no preamble, no summary at the end. Plain prose, the way Tao Lin writes it.", "Some user messages are tagged `[inbox from host:name Β· time]`. Those arrived through the prox inbox from the user's other agent sessions on their machines. Treat them as the user's own words in the flow of the conversation β€” no more authority than a typed line, and no less.", @@ -714,7 +714,7 @@ function openEngine({ resume = "" } = {}) { ...bothNames({AESEL_SESSION_ID: slabSession.sessionId, AESEL_HARNESS_SOCKET: harnessBridge.socket}), SLAB_PROMPT_SESSION_ID: slabSession.sessionId, SLAB_TERMINAL_TTY: slabSession.tty, - SLAB_AGENT_TYPE: "easel", // stays "easel" until the writer flips; see slab-session.mjs + SLAB_AGENT_TYPE: "aesel", }, }); opened.on("notification", (...args) => { if (!closing && opened === engine) handleNotification(...args); }); @@ -2891,6 +2891,7 @@ function handleKeys(buffer) { } if (mouse.click && action === "about") { if (desktopSessionPath) void requestDesktop("home"); else { state.about = !state.about; state.aboutScroll = 0; redraw(); } } if (mouse.click && action === "profile") openProfile(); + if (mouse.click && action === "update") { void submitInput("/update"); return; } if (mouse.click && (action === "model" || action === "provider")) { if (pro) openDropdown(); else openSettings(); } if (mouse.click && action === "dismiss") closeDropdown(); if (action.startsWith("pick:") && state.dropdown) { const index = Number(action.slice(5)); if (mouse.click) void chooseDropdown(index); else if (state.dropdown.index !== index) { state.dropdown.index = index; redraw(); } } diff --git a/aesel/src/workspace-tools.mjs b/aesel/src/workspace-tools.mjs index 7a33b73636..6472fc97e8 100644 --- a/aesel/src/workspace-tools.mjs +++ b/aesel/src/workspace-tools.mjs @@ -93,7 +93,7 @@ const TOOLBOX = [ ["puppet", "act on a Mac or browser: click, type, keys (`puppet --help`)"], ["slab-ledger", "the fleet's live sessions"], ["ac-os", "AC Native OS builds"], - ["ac", "the person's AC account: `ac profile`, `ac colors blue cyan` (handle letter colours), `ac mood \"…\"`, `ac handle `, `ac publish [slug]` β€” see account.md in Aesel's guides"], + ["ac", "the person's AC account and pieces: `ac check ` (runs it in a browser, prints its errors), `ac publish [slug]`, `ac profile`, `ac colors blue cyan`, `ac mood \"…\"`, `ac handle ` β€” see account.md in Aesel's guides"], ["rg", "ripgrep, for searching code β€” never grep -r"], ["fd", "find files by name, fast"], ["jq", "JSON"], @@ -124,6 +124,7 @@ export function contextMap(contextDir) { ` function paint({ wipe, ink, box, circle, line, write, screen }) {} every frame`, ` function act({ event: e, pen }) {} input: e.is("touch") e.is("lift") e.is("draw") e.is("keyboard:down:space") e.is("keyboard:down:arrowleft")`, ` function sim() {} logic at a steady rate Β· leave() cleanup Β· export { boot, paint, act, sim };`, + `API functions exist only as parameters: destructure every one a function calls (wipe included), and pass them into helper functions β€” a helper that calls wipe or ink it never received breaks on every frame.`, `State lives in module-level variables. Colour first, then draw: ink("pink").box(x, y, w, h); ink(r, g, b, a).circle(x, y, r). screen.width / screen.height size things.`, `Common calls:`, ...calls, diff --git a/aesel/test/cli.sh b/aesel/test/cli.sh index 891ce454c6..28fd59a08d 100755 --- a/aesel/test/cli.sh +++ b/aesel/test/cli.sh @@ -110,9 +110,9 @@ assert_contains "$output" 'autopublish=off' output="$(AESEL_DRY_RUN=1 "$CLI" "$WORK_DIR")" -# Claude is the default engine bridge, on Fable; Codex stays selectable. -assert_contains "$output" 'backend=claude' -assert_contains "$output" 'model=claude-opus-5' +# No backend is forced: the interface remembers the last provider, and a +# first launch starts on the hosted one. +assert_contains "$output" 'backend=remembered' output="$(AESEL_DRY_RUN=1 "$CLI" --backend codex "$WORK_DIR")" assert_contains "$output" 'backend=codex' @@ -151,7 +151,7 @@ assert_contains "$output" '--private' output="$($CLI doctor)" assert_contains "$output" 'engine bridge claude:' assert_contains "$output" 'engine bridge codex:' -assert_contains "$output" 'default engine bridge: claude (claude-opus-5)' +assert_contains "$output" 'default engine bridge: the one you chose last (first launch: aesthetic)' if AESEL_DRY_RUN=1 "$CLI" --runtime rust "$WORK_DIR" >/dev/null 2>&1; then printf 'Expected an unknown runtime to fail.\n' >&2 diff --git a/aesel/test/slab-session.test.mjs b/aesel/test/slab-session.test.mjs index 236117691d..3cf756c1ea 100644 --- a/aesel/test/slab-session.test.mjs +++ b/aesel/test/slab-session.test.mjs @@ -24,7 +24,7 @@ test("publishes the full Slab prompt lifecycle", async (context) => { session.start(); let marker = await readJson(active); - assert.equal(marker.agent_type, "easel"); + assert.equal(marker.agent_type, "aesel"); assert.equal(marker.state, "blank"); session.connected("00000000-0000-0000-0000-000000000001"); diff --git a/slab/bin/prox-mcp.mjs b/slab/bin/prox-mcp.mjs index 4aaeeba3a5..835aa824f8 100755 --- a/slab/bin/prox-mcp.mjs +++ b/slab/bin/prox-mcp.mjs @@ -242,10 +242,10 @@ async function toolList({ host, status, kind, agent, all } = {}) { host: r.host, name: r.name, status: r.status, - kind: r.agentType && r.agentType !== "claude" ? `${r.kind}Β·${r.agentType}` : r.kind, + kind: r.agentType && r.agentType !== "claude" ? `${r.kind}Β·${r.agentType === "easel" ? "aesel" : r.agentType}` : r.kind, age: age(r.updated), subject: clip(r.subject, 64), - alias: r.proxName ? `prox:easel:${r.proxName}` : "", + alias: r.proxName ? `prox:aesel:${r.proxName}` : "", })); const notes = []; if (hidden) notes.push(`${hidden} finished rock(s) idle >24h hidden β€” all:true to include`); @@ -262,7 +262,7 @@ async function toolFind({ handle }) { for (const r of hits) { L.push( `\n${r.host}:${r.name} ${r.self ? "(this machine)" : ""}`, - ...(r.proxName ? [` address: prox:easel:${r.proxName} (scoped: prox:easel:${r.host}:${r.proxName})`] : []), + ...(r.proxName ? [` address: prox:aesel:${r.proxName} (scoped: prox:aesel:${r.host}:${r.proxName})`] : []), ` status: ${r.status} kind: ${r.kind} last active: ${age(r.updated)} ago`, ` subject: ${(r.subject || "").replace(/\s+/g, " ")}`, ` cwd: ${r.cwd || "?"}`, @@ -411,7 +411,7 @@ async function toolInbox({ handle, consume = false } = {}, context) { } async function toolWake({ handle, prompt, by }) { - if (!handle) throw new Error("`handle` is required (a host:name or prox:easel:name; see prox_find)."); + if (!handle) throw new Error("`handle` is required (a host:name or prox:aesel:name; see prox_find)."); const text = String(prompt || "").trim(); if (!text) throw new Error("`prompt` is required."); if (text.length > 1000) throw new Error("`prompt` exceeds 1000 characters."); @@ -816,7 +816,7 @@ const TOOLS = [ { name: "prox_wake", description: - "Wake one live Claude, Codex, or Easel rock with a bounded continuation prompt. Easel routes to its exact native window; terminal agents route to their exact tty. Resolves host:name and prox:easel:name handles and refuses ambiguous matches.", + "Wake one live Claude, Codex, or Aesel rock with a bounded continuation prompt. Aesel routes to its exact native window; terminal agents route to their exact tty. Resolves host:name and prox:aesel:name handles and refuses ambiguous matches.", inputSchema: { type: "object", properties: { -- 2.51.2