diff --git a/CLAUDE.md b/CLAUDE.md index 586dd7f4bd..1d5891bcbe 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -140,7 +140,7 @@ Pieces are URL-addressable: `aesthetic.computer/piece-name`, params `piece-name: - `kidlisp/` - KidLisp docs and tools - `spec/` - Jasmine tests for KidLisp - `ants/` - AestheticAnts automated maintenance -- `aesthetic-code/` - The terminal coding interface (`ac`); `cd aesthetic-code && ./install.sh` +- `easel/` - Easel, the terminal coding interface (`ac`); `cd easel && ./install.sh` ## Notes diff --git a/aesthetic-code/install.sh b/aesthetic-code/install.sh deleted file mode 100755 index bcd7d9c17d..0000000000 --- a/aesthetic-code/install.sh +++ /dev/null @@ -1,41 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -BIN_DIR="${AESTHETIC_CODE_BIN_DIR:-${HOME}/.local/bin}" -CONFIG_DIR="${AESTHETIC_CODE_CONFIG_DIR:-${HOME}/.config/aesthetic-code}" -FISH_CONFIG="${XDG_CONFIG_HOME:-${HOME}/.config}/fish/config.fish" -ZSH_PROFILE="${ZDOTDIR:-${HOME}}/.zprofile" - -mkdir -p "$BIN_DIR" "$CONFIG_DIR" -chmod +x "$PROJECT_DIR/bin/aesthetic" -ln -sfn "$PROJECT_DIR/bin/aesthetic" "$BIN_DIR/aesthetic" -ln -sfn "$PROJECT_DIR/bin/aesthetic" "$BIN_DIR/ac" - -if [[ -f "$FISH_CONFIG" ]] && ! grep -Fq '# aesthetic-code:start' "$FISH_CONFIG"; then - { - printf '\n# aesthetic-code:start\n' - printf 'if test -f %q\n' "$PROJECT_DIR/shell/aesthetic-code.fish" - printf ' source %q\n' "$PROJECT_DIR/shell/aesthetic-code.fish" - printf 'end\n' - printf '# aesthetic-code:end\n' - } >> "$FISH_CONFIG" -fi - -if [[ -f "$ZSH_PROFILE" ]] && ! grep -Fq '# aesthetic-code:path:start' "$ZSH_PROFILE"; then - { - printf '\n# aesthetic-code:path:start\n' - printf 'export PATH="$HOME/.local/bin:$PATH"\n' - printf '# aesthetic-code:path:end\n' - } >> "$ZSH_PROFILE" -fi - -case ":${PATH}:" in - *":${BIN_DIR}:"*) ;; - *) printf 'Add %s to PATH to use the commands.\n' "$BIN_DIR" >&2 ;; -esac - -printf 'Installed Aesthetic Code:\n' -printf ' %s\n' "$BIN_DIR/ac" -printf ' %s\n' "$BIN_DIR/aesthetic" diff --git a/aesthetic-code/shell/aesthetic-code.fish b/aesthetic-code/shell/aesthetic-code.fish deleted file mode 100644 index ffeb2eeced..0000000000 --- a/aesthetic-code/shell/aesthetic-code.fish +++ /dev/null @@ -1,20 +0,0 @@ -# Aesthetic Code command integration for Fish. -# Preserve older Aesthetic Computer helpers under explicit names before the -# product claims `ac` and `aesthetic`. -if functions -q ac; and not functions -q ac-repo - functions --copy ac ac-repo -end - -if functions -q aesthetic; and not functions -q aesthetic-platform - functions --copy aesthetic aesthetic-platform -end - -functions --erase ac aesthetic - -function ac --description 'Open Aesthetic Code' - command $HOME/.local/bin/aesthetic $argv -end - -function aesthetic --description 'Open Aesthetic Code' - command $HOME/.local/bin/aesthetic $argv -end diff --git a/aesthetic-code/.gitignore b/easel/.gitignore similarity index 100% rename from aesthetic-code/.gitignore rename to easel/.gitignore diff --git a/aesthetic-code/LICENSE b/easel/LICENSE similarity index 100% rename from aesthetic-code/LICENSE rename to easel/LICENSE diff --git a/aesthetic-code/README.md b/easel/README.md similarity index 93% rename from aesthetic-code/README.md rename to easel/README.md index d76249595a..85cd6ecc57 100644 --- a/aesthetic-code/README.md +++ b/easel/README.md @@ -1,10 +1,10 @@ -# Aesthetic Code +# Easel -A coding interface for the terminal. Lives at `aesthetic-code/` in the +A coding interface for the terminal. Lives at `easel/` in the Aesthetic Computer repository. ```sh -cd aesthetic-code && ./install.sh +cd easel && ./install.sh ac ``` @@ -21,7 +21,7 @@ engine bridge: a subprocess on stdio that the interface drives. A native local-inference engine can replace the bridge without changing the interface. Each live TUI publishes its own Slab marker, so the menubar and prox ledger can -name, focus, wake, close, and track it as `aesthetic-code`, including which +name, focus, wake, close, and track it as `easel`, including which @handle it acts as. The marker also carries the piece and its address, which the menubar draws as a scannable code on the session's prompt rock. @@ -59,7 +59,7 @@ model are running. The Claude bridge runs `claude --print --input-format stream-json --output-format stream-json`, the same headless protocol the Claude Agent SDK -speaks, driven directly over a pipe. That is why Aesthetic Code still has no +speaks, driven directly over a pipe. That is why Easel still has no dependencies: a subprocess on stdio is the same shape as `codex app-server --stdio`, and it carries streaming, tool calls and approvals without a package tree behind it. Each bridge signs in with the vendor CLI's own credentials @@ -79,7 +79,7 @@ written down in [`docs/local-contract.md`](docs/local-contract.md). ## The session's piece, live on a phone -Opening Aesthetic Code opens a new blank piece. It gets a random pronounceable +Opening Easel opens a new blank piece. It gets a random pronounceable name, it is a real file in the workspace, and a QR code for it sits in the bottom right of the interface. Scan the code and the piece runs on your phone; every edit the agent makes reaches it a moment later. @@ -122,7 +122,7 @@ comment and declares `setup` or `draw` is taken as Lua at all. ## Account and publishing -Aesthetic Code reads the shared Aesthetic Computer sign-in at `~/.ac-token`, +Easel reads the shared Aesthetic Computer sign-in at `~/.ac-token`, the same file `ac-login` and the AC desktop apps use. `/login` runs the Authorization-Code + PKCE flow in your browser with a loopback callback and writes that file; a sign-in or sign-out anywhere in the suite updates the @@ -142,7 +142,7 @@ camera at. It coalesces — a publish runs once the saves stop, never more than one at a time, and never twice for the same bytes — and it flushes the pending save on exit. Off by default, since it writes to a public route under your own handle: turn it on per session with the flag or -`/autopublish`, or for every session with `AESTHETIC_CODE_AUTOPUBLISH=1`, which +`/autopublish`, or for every session with `EASEL_AUTOPUBLISH=1`, which `--no-autopublish` overrides. With it on the engine is told the piece is already live and told not to ask you to publish. @@ -160,7 +160,7 @@ aesthetic doctor npm test ``` -Aesthetic Code is proprietary. See `LICENSE`. +Easel is proprietary. See `LICENSE`. On Fish installations with existing `ac` or `aesthetic` functions, the installer preserves them as `ac-repo` and `aesthetic-platform`. diff --git a/aesthetic-code/bin/aesthetic b/easel/bin/easel similarity index 91% rename from aesthetic-code/bin/aesthetic rename to easel/bin/easel index cdf6f9f6dd..0198215fd2 100755 --- a/aesthetic-code/bin/aesthetic +++ b/easel/bin/easel @@ -3,7 +3,7 @@ set -euo pipefail VERSION="0.4.0" -DEFAULT_BACKEND="${AESTHETIC_CODE_BACKEND:-claude}" +DEFAULT_BACKEND="${EASEL_BACKEND:-claude}" DEFAULT_CLAUDE_MODEL="claude-opus-5" script_path="${BASH_SOURCE[0]}" while [[ -L "$script_path" ]]; do @@ -27,7 +27,7 @@ Usage: ac [directory] [--runtime mjs|lisp|processing] aesthetic login | logout | whoami aesthetic publish [slug] -Open the Aesthetic Code terminal interface in a workspace, or manage the +Open the Easel terminal interface in a workspace, or manage the shared Aesthetic Computer sign-in (~/.ac-token) and publish a piece under your @handle at https://aesthetic.computer/@handle/slug. @@ -40,7 +40,7 @@ claude-opus-5) or codex. --model names the model on that bridge, and --autopublish publishes the piece under your @handle a couple of seconds after every save, so the session's URL is live the whole time it is being -worked on. Off by default; AESTHETIC_CODE_AUTOPUBLISH=1 turns it on for +worked on. Off by default; EASEL_AUTOPUBLISH=1 turns it on for every session, --no-autopublish overrides that, and /autopublish toggles it mid-session. EOF @@ -56,7 +56,7 @@ has_command() { } doctor() { - printf 'Aesthetic Code %s\n' "$VERSION" + printf 'Easel %s\n' "$VERSION" printf 'interface: %s\n' "$PROJECT_DIR/src/tui.mjs" printf 'control plane: local\n' printf 'telemetry: off\n' @@ -87,7 +87,7 @@ case "${1:-}" in exit 0 ;; --version|-V|version) - printf 'Aesthetic Code %s\n' "$VERSION" + printf 'Easel %s\n' "$VERSION" exit 0 ;; doctor) @@ -108,7 +108,7 @@ runtime="" backend="" model="" # Empty means "whatever the environment says"; the interface reads -# AESTHETIC_CODE_AUTOPUBLISH itself, and these two flags override it. +# EASEL_AUTOPUBLISH itself, and these two flags override it. autopublish="" while [[ $# -gt 0 ]]; do case "$1" in @@ -189,13 +189,13 @@ esac has_command "$selected_backend" || fail "the $selected_backend engine bridge is not installed" -if [[ "${AESTHETIC_CODE_DRY_RUN:-0}" == "1" ]]; then - printf 'interface=aesthetic-code\n' +if [[ "${EASEL_DRY_RUN:-0}" == "1" ]]; then + printf 'interface=easel\n' printf 'directory=%s\n' "$target_directory" printf 'resume=%s\n' "$([[ -n "$resume_thread" ]] && printf yes || printf no)" printf 'initial_prompt=%s\n' "$([[ -n "$initial_prompt" ]] && printf yes || printf no)" printf 'runtime=%s\n' "${runtime:-mjs}" - printf 'autopublish=%s\n' "${autopublish:-$([[ "${AESTHETIC_CODE_AUTOPUBLISH:-0}" =~ ^(1|on|true|yes)$ ]] && printf on || printf off)}" + printf 'autopublish=%s\n' "${autopublish:-$([[ "${EASEL_AUTOPUBLISH:-0}" =~ ^(1|on|true|yes)$ ]] && printf on || printf off)}" printf 'backend=%s\n' "$selected_backend" printf 'model=%s\n' "${model:-$([[ "$selected_backend" == claude ]] && printf '%s' "$DEFAULT_CLAUDE_MODEL")}" exit 0 @@ -204,7 +204,7 @@ fi [[ -t 0 && -t 1 ]] || fail "an interactive terminal is required" export AESTHETIC_CODE=1 -export AESTHETIC_CODE_VERSION="$VERSION" +export EASEL_VERSION="$VERSION" arguments=("--cwd" "$target_directory") if [[ -n "$resume_thread" ]]; then arguments+=("--resume" "$resume_thread"); fi if [[ -n "$initial_prompt" ]]; then arguments+=("--prompt" "$initial_prompt"); fi diff --git a/aesthetic-code/docs/local-contract.md b/easel/docs/local-contract.md similarity index 91% rename from aesthetic-code/docs/local-contract.md rename to easel/docs/local-contract.md index 5da2479d68..fd5de31e41 100644 --- a/aesthetic-code/docs/local-contract.md +++ b/easel/docs/local-contract.md @@ -1,6 +1,6 @@ # Local contract -Aesthetic Code requires no Aesthetic Code server. +Easel requires no Easel server. - No runtime account, telemetry, analytics, cloud sync, or hosted control plane. - Configuration, session records, memory, and credentials remain on machines @@ -21,7 +21,7 @@ and the session token to `aesthetic.computer` only when the user runs `/publish`. The engine bridge never receives the token; publishing is an interface action, not an agent tool. -Auto-publish (`--autopublish`, `AESTHETIC_CODE_AUTOPUBLISH=1`, `/autopublish`) +Auto-publish (`--autopublish`, `EASEL_AUTOPUBLISH=1`, `/autopublish`) is the one way that becomes repeated rather than per-command: with it on, the interface publishes the session's piece a couple of seconds after every save, and the last save is flushed on the way out. It is off by default and has to be @@ -42,13 +42,13 @@ workspace paths, no conversation. The channel token is random per session and is never reused. Pushing is the interface's own action, on a file the user can see, and stops -when the session ends. It is the one thing Aesthetic Code sends without being +when the session ends. It is the one thing Easel sends without being asked each time, so it is worth stating plainly: while the interface is open, the piece on screen is repeatedly leaving the machine. ## Inference boundary -The terminal interface is always Aesthetic Code. Engines are internal bridges, +The terminal interface is always Easel. Engines are internal bridges, not alternate client interfaces or command shortcuts. Two bridges exist, both remote: Claude Code in headless stream-json mode (the @@ -56,7 +56,7 @@ default, on `claude-opus-5`) and Codex app-server. `--backend` and `/backend` choose between them and `--model` and `/model` name the model. The interface labels remote inference before a prompt is sent. Provider terms govern that traffic, and each bridge signs in with its own vendor's existing -credentials on this machine; Aesthetic Code stores no key of its own. +credentials on this machine; Easel stores no key of its own. Neither bridge inherits the user's own agent configuration. Codex is started with `on-request` approvals and a `workspace-write` sandbox regardless of what @@ -89,7 +89,7 @@ Codex runs commands under an operating-system sandbox: writes are confined to the workspace and `networkAccess` is false, so an approved command still cannot reach the network without a second, explicit escalation. -Claude Code has no equivalent sandbox. On that bridge Aesthetic Code confines +Claude Code has no equivalent sandbox. On that bridge Easel confines the file tools to the workspace, removes WebFetch and WebSearch, and routes every prompt to this terminal — and Claude does prompt before a command that touches the network — but the prompt is the whole boundary. A shell command the @@ -97,7 +97,7 @@ user approves runs with the user's own privileges and can reach the network. Read-only commands are auto-approved by Claude's own classifier, as reads inside the sandbox are on the Codex bridge. -Aesthetic Code therefore does not claim that agent tools are network-isolated +Easel therefore does not claim that agent tools are network-isolated on the Claude bridge. Where that matters, `--backend codex` is the bridge with a kernel behind its approvals. diff --git a/aesthetic-code/docs/openrouter-backend.md b/easel/docs/openrouter-backend.md similarity index 97% rename from aesthetic-code/docs/openrouter-backend.md rename to easel/docs/openrouter-backend.md index 803f5716d9..1eb6d9ae7a 100644 --- a/aesthetic-code/docs/openrouter-backend.md +++ b/easel/docs/openrouter-backend.md @@ -3,7 +3,7 @@ Written 2026-09-11. Prices quoted are from `https://openrouter.ai/api/v1/models`, pulled that morning. Nothing here is implemented. -The goal is to stop requiring every person who opens Aesthetic Code to already +The goal is to stop requiring every person who opens Easel to already hold a Claude or Codex subscription. Today both bridges sign in with a vendor CLI's own credentials, which is elegant — no key, no server, no account — and also a wall: the interface is unusable to anyone who has not already paid @@ -62,7 +62,7 @@ Worth noting, because it changes how bad this option feels: OpenRouter has an OAuth PKCE flow at `https://openrouter.ai/auth` that exchanges a code at `POST /api/v1/auth/keys` for a user-scoped key billed to that user's credits, and it documents a headless mode with no `callback_url` where the user copies -the code across. Aesthetic Code already runs an Authorization-Code + PKCE flow +the code across. Easel already runs an Authorization-Code + PKCE flow with a loopback callback in `src/ac-session.mjs`; a second one against a different issuer is the same ninety lines. So "bring your own key" can be `/login`-shaped rather than paste-shaped. @@ -120,7 +120,7 @@ The trust boundary in the recommended option is worth stating flatly: Aesthetic Computer never sees the prompts, but it does mint a bearer token that spends its money, and a user who extracts that token from their own machine — trivially, it is a file they own — can spend the cap however they like, including on -something that is not Aesthetic Code. The cap is therefore the entire control. +something that is not Easel. The cap is therefore the entire control. It should be small, it should reset, and it should be revocable per handle. ## Whether this needs a second kind of engine @@ -251,7 +251,7 @@ product, and writes the answer back to Redis. The invalidation half is in `ticket.js`, which on `customer.subscription.updated` reads `customer.metadata.sub` and deletes the cache entry. That pair — a Redis-cached entitlement keyed on the Auth0 sub, invalidated by a webhook — is precisely what -a paid Aesthetic Code tier needs, and it can be ported by changing the product +a paid Easel tier needs, and it can be ported by changing the product id. `system/netlify/functions/news-toll.mjs` is the cleanest single-file reference @@ -318,7 +318,7 @@ of magnitude. These are @jeffrey's own sessions in the full Aesthetic Computer tree, on a million-token context, and they are an upper bound rather than a typical -Aesthetic Code session — the interface opens on one small piece file in a +Easel session — the interface opens on one small piece file in a workspace the user chose. So, bottom-up: thirty turns, context growing from about 15k tokens to about 60k, averaging 35k. That is roughly 1.05M cache-read tokens, 300k cache-write, 20k output. Priced across the shelf at today's @@ -390,7 +390,7 @@ may be issued or re-issued, plus a short `expires_at` so an abandoned key stops mattering — and it is a partial one. Worth testing before promising a tier. Runaway spend inside a single session is the most likely everyday failure, and -it is not malice. Aesthetic Code auto-approves tool calls by default: +it is not malice. Easel auto-approves tool calls by default: `handleRequest` in `src/tui.mjs` answers `accept` unless `/ask on` is set, because the first real session spent two of its two hours and nineteen minutes parked on prompts with nobody watching. That default is right for a piece @@ -452,7 +452,7 @@ from unauthenticated route probes, not from a working call. cannot answer. The free tier's viability is a quality question, not a price one. - **The realistic monthly-active handle count.** 2,814 handles exist. How many - would open Aesthetic Code in a month is a guess, and the $300 figure moves + would open Easel in a month is a guess, and the $300 figure moves linearly with it. ## What this changes in the contract diff --git a/easel/install.sh b/easel/install.sh new file mode 100755 index 0000000000..71f8889c54 --- /dev/null +++ b/easel/install.sh @@ -0,0 +1,55 @@ +#!/usr/bin/env bash + +set -euo pipefail + +PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +BIN_DIR="${EASEL_BIN_DIR:-${HOME}/.local/bin}" +CONFIG_DIR="${EASEL_CONFIG_DIR:-${HOME}/.config/easel}" +FISH_CONFIG="${XDG_CONFIG_HOME:-${HOME}/.config}/fish/config.fish" +ZSH_PROFILE="${ZDOTDIR:-${HOME}}/.zprofile" + +mkdir -p "$BIN_DIR" "$CONFIG_DIR" +chmod +x "$PROJECT_DIR/bin/easel" +ln -sfn "$PROJECT_DIR/bin/easel" "$BIN_DIR/easel" +ln -sfn "$PROJECT_DIR/bin/easel" "$BIN_DIR/ac" + +# The tool was called `aesthetic` before it was called Easel, and that name +# belongs to the platform helper it displaced. Take it back only if it is still +# our own symlink — never touch a real file someone else put there. +if [[ -L "$BIN_DIR/aesthetic" ]] && [[ "$(readlink "$BIN_DIR/aesthetic")" == *"/bin/aesthetic" || "$(readlink "$BIN_DIR/aesthetic")" == *"/bin/easel" ]]; then + rm -f "$BIN_DIR/aesthetic" +fi + +# The tool shipped as Aesthetic Code first, and its Fish block named that path. +# The block is inert once the path is gone — `test -f` simply fails — but it +# lingers on every machine that ever installed the old name, so take it out. +if [[ -f "$FISH_CONFIG" ]] && grep -Fq '# aesthetic-code:start' "$FISH_CONFIG"; then + /usr/bin/sed -i '' '/# aesthetic-code:start/,/# aesthetic-code:end/d' "$FISH_CONFIG" +fi + +if [[ -f "$FISH_CONFIG" ]] && ! grep -Fq '# easel:start' "$FISH_CONFIG"; then + { + printf '\n# easel:start\n' + printf 'if test -f %q\n' "$PROJECT_DIR/shell/easel.fish" + printf ' source %q\n' "$PROJECT_DIR/shell/easel.fish" + printf 'end\n' + printf '# easel:end\n' + } >> "$FISH_CONFIG" +fi + +if [[ -f "$ZSH_PROFILE" ]] && ! grep -Fq '# easel:path:start' "$ZSH_PROFILE"; then + { + printf '\n# easel:path:start\n' + printf 'export PATH="$HOME/.local/bin:$PATH"\n' + printf '# easel:path:end\n' + } >> "$ZSH_PROFILE" +fi + +case ":${PATH}:" in + *":${BIN_DIR}:"*) ;; + *) printf 'Add %s to PATH to use the commands.\n' "$BIN_DIR" >&2 ;; +esac + +printf 'Installed Easel:\n' +printf ' %s\n' "$BIN_DIR/ac" +printf ' %s\n' "$BIN_DIR/easel" diff --git a/aesthetic-code/package.json b/easel/package.json similarity index 83% rename from aesthetic-code/package.json rename to easel/package.json index 24274b0ffe..2e890b9143 100644 --- a/aesthetic-code/package.json +++ b/easel/package.json @@ -1,5 +1,5 @@ { - "name": "aesthetic-code", + "name": "easel", "version": "0.4.0", "private": true, "type": "module", diff --git a/easel/shell/easel.fish b/easel/shell/easel.fish new file mode 100644 index 0000000000..bbdcb1b0cc --- /dev/null +++ b/easel/shell/easel.fish @@ -0,0 +1,18 @@ +# Easel command integration for Fish. +# +# Easel claims `ac` and `easel`. It deliberately does not claim `aesthetic` +# any more: that name belongs to the Aesthetic Computer platform helper, and +# the tool only held it while it was still called Aesthetic Code. +if functions -q ac; and not functions -q ac-repo + functions --copy ac ac-repo +end + +functions --erase ac easel + +function ac --description 'Open Easel' + command $HOME/.local/bin/easel $argv +end + +function easel --description 'Open Easel' + command $HOME/.local/bin/easel $argv +end diff --git a/aesthetic-code/src/ac-session.mjs b/easel/src/ac-session.mjs similarity index 96% rename from aesthetic-code/src/ac-session.mjs rename to easel/src/ac-session.mjs index 5feb25b90e..8928d4d58e 100644 --- a/aesthetic-code/src/ac-session.mjs +++ b/easel/src/ac-session.mjs @@ -3,7 +3,7 @@ // Every AC desktop app reads one file, ~/.ac-token, minted by `ac-login` with // Auth0 Authorization-Code + PKCE and a loopback callback. This module reads // and watches that file, refreshes the access token, and can run the same -// sign-in flow itself so Aesthetic Code needs no other checkout. Only the +// sign-in flow itself so Easel needs no other checkout. Only the // handle is ever displayed; email and name stay in the file. import { EventEmitter } from "node:events"; import { createHash, randomBytes } from "node:crypto"; @@ -20,17 +20,17 @@ export const SITE = "https://aesthetic.computer"; // Cloudflare fronts aesthetic.computer and rejects a bare user agent (1010). export const USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 " + - "(KHTML, like Gecko) Chrome/125.0 Safari/537.36 aesthetic-code"; + "(KHTML, like Gecko) Chrome/125.0 Safari/537.36 easel"; const base64url = (buffer) => buffer.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, ""); const LANDING = ` -Signed in · Aesthetic Code +Signed in · Easel -

Signed in

Return to Aesthetic Code. You can close this tab.

`; +

Signed in

Return to Easel. You can close this tab.

`; export function openInBrowser(url) { const command = @@ -210,13 +210,13 @@ export class ACSession extends EventEmitter { const failure = url.searchParams.get("error"); if (failure) { response.writeHead(400, { "content-type": "text/plain" }); - response.end("Sign-in failed. Return to Aesthetic Code."); + response.end("Sign-in failed. Return to Easel."); settle(reject, new Error(url.searchParams.get("error_description") || failure)); return; } if (url.searchParams.get("state") !== state) { response.writeHead(400, { "content-type": "text/plain" }); - response.end("State mismatch. Return to Aesthetic Code and retry."); + response.end("State mismatch. Return to Easel and retry."); settle(reject, new Error("sign-in state mismatch — retry /login")); return; } diff --git a/aesthetic-code/src/app-server.mjs b/easel/src/app-server.mjs similarity index 97% rename from aesthetic-code/src/app-server.mjs rename to easel/src/app-server.mjs index 6451144fc4..0cbfff4abd 100644 --- a/aesthetic-code/src/app-server.mjs +++ b/easel/src/app-server.mjs @@ -38,8 +38,8 @@ export class AppServer extends EventEmitter { env: { ...process.env, ...this.environment, - AESTHETIC_CODE: "1", - AESTHETIC_CODE_VERSION: VERSION, + EASEL: "1", + EASEL_VERSION: VERSION, }, stdio: ["pipe", "pipe", "pipe"], }); @@ -66,8 +66,8 @@ export class AppServer extends EventEmitter { await this.request("initialize", { clientInfo: { - name: "aesthetic_code", - title: "Aesthetic Code", + name: "easel", + title: "Easel", version: VERSION, }, capabilities: { experimentalApi: true }, diff --git a/aesthetic-code/src/autopublish.mjs b/easel/src/autopublish.mjs similarity index 98% rename from aesthetic-code/src/autopublish.mjs rename to easel/src/autopublish.mjs index 66c7e168ce..7cbec370cf 100644 --- a/aesthetic-code/src/autopublish.mjs +++ b/easel/src/autopublish.mjs @@ -9,7 +9,7 @@ // // It is off unless asked for. Publishing is outward-facing — the bytes land on // a public route under the user's own name — so it stays an opt-in for the -// session (`/autopublish on`, `--autopublish`, AESTHETIC_CODE_AUTOPUBLISH=1) +// session (`/autopublish on`, `--autopublish`, EASEL_AUTOPUBLISH=1) // rather than something the interface starts doing on its own. // // Saves arrive far faster than a publish should: an agent turn can write a file diff --git a/aesthetic-code/src/backends.mjs b/easel/src/backends.mjs similarity index 96% rename from aesthetic-code/src/backends.mjs rename to easel/src/backends.mjs index 97bcc259a5..e6b3d02aee 100644 --- a/aesthetic-code/src/backends.mjs +++ b/easel/src/backends.mjs @@ -1,4 +1,4 @@ -// backends.mjs — the engine bridges Aesthetic Code can drive. +// backends.mjs — the engine bridges Easel can drive. // // A bridge is a subprocess speaking a line protocol over stdio. The interface // holds the same conversation over either of them — a thread, turns inside it, diff --git a/aesthetic-code/src/claude-server.mjs b/easel/src/claude-server.mjs similarity index 98% rename from aesthetic-code/src/claude-server.mjs rename to easel/src/claude-server.mjs index 52aa4cff5e..5a5f5b2504 100644 --- a/aesthetic-code/src/claude-server.mjs +++ b/easel/src/claude-server.mjs @@ -163,8 +163,8 @@ export class ClaudeServer extends EventEmitter { behavior: "deny", message: decision === "cancel" - ? "Cancelled in Aesthetic Code." - : "Denied in Aesthetic Code.", + ? "Cancelled in Easel." + : "Denied in Easel.", }; } this.#send({ @@ -269,8 +269,8 @@ export class ClaudeServer extends EventEmitter { env: { ...process.env, ...this.environment, - AESTHETIC_CODE: "1", - AESTHETIC_CODE_VERSION: VERSION, + EASEL: "1", + EASEL_VERSION: VERSION, }, stdio: ["pipe", "pipe", "pipe"], }); @@ -468,7 +468,7 @@ export class ClaudeServer extends EventEmitter { response: { subtype: "error", request_id: message.request_id, - error: `Aesthetic Code does not support ${request.subtype} yet`, + error: `Easel does not support ${request.subtype} yet`, }, }); return; diff --git a/aesthetic-code/src/cli.mjs b/easel/src/cli.mjs similarity index 96% rename from aesthetic-code/src/cli.mjs rename to easel/src/cli.mjs index 100081c762..606bf0a394 100644 --- a/aesthetic-code/src/cli.mjs +++ b/easel/src/cli.mjs @@ -28,7 +28,7 @@ try { } else if (command === "publish") { const [file, slug = ""] = rest.filter((argument) => !argument.startsWith("--")); if (!file) fail("usage: aesthetic publish [slug]"); - if (process.env.AESTHETIC_CODE_DRY_RUN === "1") { + if (process.env.EASEL_DRY_RUN === "1") { const plan = planPublish({ file, slug, handle: session.handle || "handle" }); out(`would publish ${plan.path} as ${plan.route}`); } else { diff --git a/easel/src/easel.mjs b/easel/src/easel.mjs new file mode 100644 index 0000000000..cdd98a91bc --- /dev/null +++ b/easel/src/easel.mjs @@ -0,0 +1,116 @@ +// The easel. +// +// The tool is named after the thing a painter stands a canvas on, so a session +// opens by standing one up. What goes on the canvas is the only two facts a +// session starts with that are worth knowing: the name of the piece, and the +// address where it can already be reached. Both are true from the first second +// now — the blank publishes as soon as the handle resolves — so the splash is +// reporting rather than promising. +// +// The name is written on rather than printed, one character at a time, because +// that is what an easel is for. The address follows once the name is finished. +// Nothing else moves: the frame and the legs are standing from the first frame, +// since an easel that assembles itself is a cartoon and this one has to sit +// above a transcript someone is about to read. +// +// Pure, like the mascot. A caller asks what the easel looks like at a given +// number of milliseconds and gets lines back. Nothing schedules, nothing draws, +// nothing reads a clock. + +// Milliseconds per character of the piece name, and the pause before the +// address starts. Slow enough to read as handwriting, short enough that the +// whole thing is over before anyone has finished their first prompt. +const WRITE_MS = 55; +const PAUSE_MS = 260; +const ADDRESS_MS = 16; +// How long the finished easel stands before the caller may stop asking. +const HOLD_MS = 900; + +const MIN_INNER = 13; +const MAX_INNER = 46; + +function innerWidth(piece, address) { + const longest = Math.max(piece.length, address.length); + return Math.min(MAX_INNER, Math.max(MIN_INNER, longest + 2)); +} + +function centre(text, width) { + if (text.length >= width) return text.slice(0, width); + const left = Math.floor((width - text.length) / 2); + return " ".repeat(left) + text + " ".repeat(width - text.length - left); +} + +// The whole reveal, in milliseconds: name written, pause, address written, hold. +export function easelDuration(piece = "", address = "") { + return ( + String(piece).length * WRITE_MS + + PAUSE_MS + + String(address).length * ADDRESS_MS + + HOLD_MS + ); +} + +// How many characters of each line are showing at `elapsed`. +function revealed(elapsed, piece, address) { + const ms = Math.max(0, Number(elapsed) || 0); + const written = Math.min(piece.length, Math.floor(ms / WRITE_MS)); + if (written < piece.length) return { piece: written, address: 0 }; + const after = ms - piece.length * WRITE_MS - PAUSE_MS; + if (after <= 0) return { piece: piece.length, address: 0 }; + return { + piece: piece.length, + address: Math.min(address.length, Math.floor(after / ADDRESS_MS)), + }; +} + +// One frame. `piece` is the slug as it appears on disk; `address` is the bare +// host+path the rock is showing. Either may be empty — a session that is not +// signed in has no address yet, and the canvas simply stays blank there rather +// than apologising for it. +export function easelFrame(elapsed = 0, { piece = "", address = "" } = {}) { + const name = String(piece || ""); + const where = String(address || ""); + const inner = innerWidth(name, where); + const shown = revealed(elapsed, name, where); + + // A cursor while the name is still being written, so the pause between the + // name landing and the address starting does not read as a hang. + const writing = shown.piece < name.length; + const nameText = name.slice(0, shown.piece) + (writing ? "▌" : ""); + + const top = "┌" + "─".repeat(inner) + "┐"; + const blank = "│" + " ".repeat(inner) + "│"; + const nameRow = "│" + centre(nameText, inner) + "│"; + const addressRow = "│" + centre(where.slice(0, shown.address), inner) + "│"; + + // The bottom edge carries the mast the legs hang from, so the easel reads as + // one object rather than a box resting on two sticks. + const mastAt = Math.floor(inner / 2); + const bottom = + "└" + "─".repeat(mastAt) + "┬" + "─".repeat(inner - mastAt - 1) + "┘"; + + const pad = " ".repeat(mastAt); + return [ + top, + blank, + nameRow, + blank, + addressRow, + bottom, + pad + "╱ ╲", + pad.slice(0, Math.max(0, mastAt - 1)) + "╱ ╲", + ]; +} + +// When the easel next needs repainting, or null once it is finished standing +// there and the caller can stop asking. +export function easelNextFrame(elapsed = 0, { piece = "", address = "" } = {}) { + const ms = Math.max(0, Number(elapsed) || 0); + if (ms >= easelDuration(piece, address)) return null; + const shown = revealed(ms, String(piece), String(address)); + if (shown.piece < String(piece).length) return WRITE_MS; + if (shown.address < String(address).length) return ADDRESS_MS; + return HOLD_MS; +} + +export const EASEL_HEIGHT = 8; diff --git a/aesthetic-code/src/live.mjs b/easel/src/live.mjs similarity index 99% rename from aesthetic-code/src/live.mjs rename to easel/src/live.mjs index 286ac2e150..c0b0c84e68 100644 --- a/aesthetic-code/src/live.mjs +++ b/easel/src/live.mjs @@ -1,6 +1,6 @@ // live.mjs — the session's piece, and the channel that carries it to a phone. // -// Every Aesthetic Code session opens on a new blank piece with a random name. +// Every Easel session opens on a new blank piece with a random name. // The piece is a real file in the workspace, so the agent edits it like any // other file, and every save is pushed to Aesthetic Computer's `/run` endpoint // on a private code channel. Anything watching that channel — a phone that diff --git a/aesthetic-code/src/mascot.mjs b/easel/src/mascot.mjs similarity index 100% rename from aesthetic-code/src/mascot.mjs rename to easel/src/mascot.mjs diff --git a/aesthetic-code/src/names.mjs b/easel/src/names.mjs similarity index 100% rename from aesthetic-code/src/names.mjs rename to easel/src/names.mjs diff --git a/aesthetic-code/src/publish.mjs b/easel/src/publish.mjs similarity index 100% rename from aesthetic-code/src/publish.mjs rename to easel/src/publish.mjs diff --git a/aesthetic-code/src/qr.mjs b/easel/src/qr.mjs similarity index 100% rename from aesthetic-code/src/qr.mjs rename to easel/src/qr.mjs diff --git a/aesthetic-code/src/render.mjs b/easel/src/render.mjs similarity index 99% rename from aesthetic-code/src/render.mjs rename to easel/src/render.mjs index 490a0aa334..0a5d6c712f 100644 --- a/aesthetic-code/src/render.mjs +++ b/easel/src/render.mjs @@ -1,4 +1,4 @@ -// render.mjs — one frame of the Aesthetic Code interface. +// render.mjs — one frame of the Easel interface. // // The palette is the Aesthetic Computer prompt's dark scheme (disks/prompt.mjs // `scheme.dark`): purple ground, pink prompt block, orange highlight, magenta @@ -59,7 +59,7 @@ const slabState = join( process.env.SLAB_HOME || join(homedir(), ".local", "share", "slab"), "state", ); -const groundMode = (process.env.AESTHETIC_CODE_GROUND || "").toLowerCase(); +const groundMode = (process.env.EASEL_GROUND || "").toLowerCase(); const slabManagesWindow = process.env.TERM_PROGRAM === "Apple_Terminal" && existsSync(slabState); export const paintsGround = diff --git a/aesthetic-code/src/runtimes.mjs b/easel/src/runtimes.mjs similarity index 100% rename from aesthetic-code/src/runtimes.mjs rename to easel/src/runtimes.mjs diff --git a/aesthetic-code/src/slab-session.mjs b/easel/src/slab-session.mjs similarity index 99% rename from aesthetic-code/src/slab-session.mjs rename to easel/src/slab-session.mjs index 420a795c2b..4a3558a39c 100644 --- a/aesthetic-code/src/slab-session.mjs +++ b/easel/src/slab-session.mjs @@ -57,7 +57,7 @@ export class SlabSession { summary: "aesthetic code", tty, agent_pid: pid, - agent_type: "aesthetic-code", + agent_type: "easel", handle: "", // The piece this session is writing, and the address a phone reaches it // at. The menubar draws these as a scannable code on the rock, which is diff --git a/aesthetic-code/src/tui.mjs b/easel/src/tui.mjs similarity index 93% rename from aesthetic-code/src/tui.mjs rename to easel/src/tui.mjs index f2979d3c54..5eefbb4f42 100755 --- a/aesthetic-code/src/tui.mjs +++ b/easel/src/tui.mjs @@ -5,6 +5,7 @@ import path from "node:path"; import process from "node:process"; import { ACSession } from "./ac-session.mjs"; import { AutoPublisher } from "./autopublish.mjs"; +import { EASEL_HEIGHT, easelFrame, easelNextFrame } from "./easel.mjs"; import { backendFor, backendMenu, DEFAULT_BACKEND } from "./backends.mjs"; import { LivePiece } from "./live.mjs"; import { publishPiece } from "./publish.mjs"; @@ -25,7 +26,7 @@ const resumeThreadId = option("--resume"); const initialPrompt = option("--prompt"); // Which engine bridge drives the conversation, and on which model. The bridge // can be swapped mid-session with /backend, so neither is a constant. -let backend = backendFor(option("--backend") || process.env.AESTHETIC_CODE_BACKEND || DEFAULT_BACKEND); +let backend = backendFor(option("--backend") || process.env.EASEL_BACKEND || DEFAULT_BACKEND); let model = option("--model") || backend.defaultModel; const session = new ACSession(); @@ -93,11 +94,11 @@ const state = { const autopublish = new AutoPublisher({ // On by default. The scanned address is the published one, so a session // that does not publish has nothing to point a camera at; `--no-autopublish` - // and `AESTHETIC_CODE_AUTOPUBLISH=0` both opt out, and a signed-out session + // and `EASEL_AUTOPUBLISH=0` both opt out, and a signed-out session // never reaches the attempt. enabled: !flag("--no-autopublish") && - !/^(0|off|false|no)$/i.test(process.env.AESTHETIC_CODE_AUTOPUBLISH || ""), + !/^(0|off|false|no)$/i.test(process.env.EASEL_AUTOPUBLISH || ""), publish: () => publishPiece({ file: live.file, slug: live.slug, session, cwd }), }); @@ -139,11 +140,11 @@ function developerInstructions() { ] : [ "Publishing: writing a file under system/public/aesthetic.computer/disks/ or anywhere else does NOT make a piece live.", - "A piece is live only after the user runs the Aesthetic Code command `/publish [slug]`, which uploads it under their @handle at https://aesthetic.computer/@handle/slug.", + "A piece is live only after the user runs the Easel command `/publish [slug]`, which uploads it under their @handle at https://aesthetic.computer/@handle/slug.", "When you finish a piece, end with the exact /publish command for the user to run. Never tell the user to visit a route that has not been published.", ]; return [ - "You are running inside Aesthetic Code, a terminal interface for Aesthetic Computer (AC) work.", + "You are running inside Easel, a terminal interface for Aesthetic Computer (AC) work.", account, `This session's piece is ${live.file} (${live.runtime.label}). It already exists as a blank piece. Edit that file unless the user asks for something else.`, ...dialect, @@ -168,7 +169,7 @@ function openEngine({ resume = "" } = {}) { environment: { SLAB_PROMPT_SESSION_ID: slabSession.sessionId, SLAB_TERMINAL_TTY: slabSession.tty, - SLAB_AGENT_TYPE: "aesthetic-code", + SLAB_AGENT_TYPE: "easel", }, }); opened.on("notification", handleNotification); @@ -190,6 +191,11 @@ function openEngine({ resume = "" } = {}) { let engine = openEngine({ resume: resumeThreadId }); let drawing = false; let closing = false; +// The startup easel owns the screen until it is done or dismissed. Declared +// here rather than beside the splash itself because redraw() reads it, and +// redraw() can be called before that block is reached. +let splashing = false; +let splashTimer = null; let streamedMessageId = null; let pasteBuffer = null; @@ -232,7 +238,7 @@ function startDance() { } function redraw() { - if (closing || drawing) return; + if (closing || drawing || splashing) return; drawing = true; try { const frame = renderFrame(state, process.stdout.columns, process.stdout.rows, process.env.NO_COLOR !== "1"); @@ -474,7 +480,7 @@ function handleRequest(request) { redraw(); return; } - engine.reject(request.id, -32601, `Aesthetic Code does not support ${request.method} yet`); + engine.reject(request.id, -32601, `Easel does not support ${request.method} yet`); } function answerApproval(character) { @@ -894,6 +900,13 @@ function insertText(value) { } function handleKey(input) { + // The easel is a greeting, not a gate. Any key puts it away. + if (splashing) { + splashing = false; + clearTimeout(splashTimer); + splashTimer = null; + redraw(); + } if (answerApproval(input)) return; if (input === "\u0003") { @@ -957,6 +970,39 @@ function handleKeys(buffer) { } process.stdout.write("\x1b[?1049h\x1b[?25l\x1b[?2004h"); + +// 🎨 Stand the easel up. Each frame reads the live values rather than a +// snapshot, so the address is written onto the canvas at whatever moment the +// sign-in resolves — which is the honest thing to show, since that is exactly +// when the piece's address starts answering. +function splashTick() { + const elapsed = Date.now() - splashStartedAt; + const canvas = { piece: live.slug, address: live.scanUrl }; + const columns = process.stdout.columns || 80; + const lines = easelFrame(elapsed, canvas); + const pad = Math.max(0, Math.floor((columns - lines[0].length) / 2)); + const gap = Math.max(0, Math.floor(((process.stdout.rows || 24) - EASEL_HEIGHT) / 2)); + const body = lines.map((line) => " ".repeat(pad) + line).join("\n"); + process.stdout.write(`\x1b[H\x1b[2J${color.ground}${"\n".repeat(gap)}${body}`); + + const next = easelNextFrame(elapsed, canvas); + if (next === null) { + splashing = false; + splashTimer = null; + return redraw(); + } + splashTimer = setTimeout(splashTick, next); + splashTimer.unref?.(); +} + +// Skip it on a screen too small to hold it — a clipped easel is worse than +// none — and whenever output is not a terminal at all. +const splashStartedAt = Date.now(); +if (process.stdout.isTTY && (process.stdout.rows || 0) >= EASEL_HEIGHT + 4) { + splashing = true; + splashTick(); +} + process.stdin.setRawMode(true); process.stdin.resume(); process.stdin.on("data", handleKeys); diff --git a/aesthetic-code/src/vendor/qr.mjs b/easel/src/vendor/qr.mjs similarity index 100% rename from aesthetic-code/src/vendor/qr.mjs rename to easel/src/vendor/qr.mjs diff --git a/aesthetic-code/test/ac-session.test.mjs b/easel/test/ac-session.test.mjs similarity index 98% rename from aesthetic-code/test/ac-session.test.mjs rename to easel/test/ac-session.test.mjs index 4b27724725..7fdcf8eab0 100644 --- a/aesthetic-code/test/ac-session.test.mjs +++ b/easel/test/ac-session.test.mjs @@ -9,7 +9,7 @@ const jsonResponse = (body, status = 200) => new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } }); async function scratch(context) { - const root = await mkdtemp(join(tmpdir(), "aesthetic-code-session-")); + const root = await mkdtemp(join(tmpdir(), "easel-session-")); context.after(() => rm(root, { recursive: true, force: true })); return join(root, ".ac-token"); } diff --git a/aesthetic-code/test/app-server.test.mjs b/easel/test/app-server.test.mjs similarity index 100% rename from aesthetic-code/test/app-server.test.mjs rename to easel/test/app-server.test.mjs diff --git a/aesthetic-code/test/autopublish.test.mjs b/easel/test/autopublish.test.mjs similarity index 100% rename from aesthetic-code/test/autopublish.test.mjs rename to easel/test/autopublish.test.mjs diff --git a/aesthetic-code/test/claude-server.test.mjs b/easel/test/claude-server.test.mjs similarity index 100% rename from aesthetic-code/test/claude-server.test.mjs rename to easel/test/claude-server.test.mjs diff --git a/aesthetic-code/test/cli.sh b/easel/test/cli.sh similarity index 63% rename from aesthetic-code/test/cli.sh rename to easel/test/cli.sh index a9816a1408..d951013d94 100755 --- a/aesthetic-code/test/cli.sh +++ b/easel/test/cli.sh @@ -3,14 +3,14 @@ set -euo pipefail PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -CLI="$PROJECT_DIR/bin/aesthetic" -TEST_ROOT="$(mktemp -d "${TMPDIR:-/tmp}/aesthetic-code-test.XXXXXX")" +CLI="$PROJECT_DIR/bin/easel" +TEST_ROOT="$(mktemp -d "${TMPDIR:-/tmp}/easel-test.XXXXXX")" WORK_DIR="$TEST_ROOT/work space" TEST_BIN="$TEST_ROOT/bin" cleanup() { case "$TEST_ROOT" in - "${TMPDIR:-/tmp}"/aesthetic-code-test.*|/private/tmp/aesthetic-code-test.*) + "${TMPDIR:-/tmp}"/easel-test.*|/private/tmp/easel-test.*) rm -rf "$TEST_ROOT" ;; esac @@ -31,17 +31,17 @@ assert_contains() { } output="$($CLI --version)" -assert_contains "$output" 'Aesthetic Code 0.4.0' +assert_contains "$output" 'Easel 0.4.0' -output="$(AESTHETIC_CODE_DRY_RUN=1 "$CLI" "$WORK_DIR")" -assert_contains "$output" 'interface=aesthetic-code' +output="$(EASEL_DRY_RUN=1 "$CLI" "$WORK_DIR")" +assert_contains "$output" 'interface=easel' assert_contains "$output" "directory=$WORK_DIR" -output="$(AESTHETIC_CODE_DRY_RUN=1 "$TEST_BIN/ac" "$WORK_DIR")" -assert_contains "$output" 'interface=aesthetic-code' +output="$(EASEL_DRY_RUN=1 "$TEST_BIN/ac" "$WORK_DIR")" +assert_contains "$output" 'interface=easel' assert_contains "$output" "directory=$WORK_DIR" -output="$(AESTHETIC_CODE_DRY_RUN=1 "$CLI" --resume 00000000-0000-0000-0000-000000000001 --prompt continue "$WORK_DIR")" +output="$(EASEL_DRY_RUN=1 "$CLI" --resume 00000000-0000-0000-0000-000000000001 --prompt continue "$WORK_DIR")" assert_contains "$output" 'resume=yes' assert_contains "$output" 'initial_prompt=yes' @@ -64,46 +64,46 @@ output="$(HOME="$FAKE_HOME" "$CLI" whoami)" assert_contains "$output" '@tester' printf 'export function paint() {}\n' > "$WORK_DIR/smiley.mjs" -output="$(HOME="$FAKE_HOME" AESTHETIC_CODE_DRY_RUN=1 "$CLI" publish "$WORK_DIR/smiley.mjs")" +output="$(HOME="$FAKE_HOME" EASEL_DRY_RUN=1 "$CLI" publish "$WORK_DIR/smiley.mjs")" assert_contains "$output" 'https://aesthetic.computer/@tester/smiley' -if HOME="$FAKE_HOME" AESTHETIC_CODE_DRY_RUN=1 "$CLI" publish >/dev/null 2>&1; then +if HOME="$FAKE_HOME" EASEL_DRY_RUN=1 "$CLI" publish >/dev/null 2>&1; then printf 'Expected publish without a file to fail.\n' >&2 exit 1 fi -output="$(AESTHETIC_CODE_DRY_RUN=1 "$CLI" --runtime lisp "$WORK_DIR")" +output="$(EASEL_DRY_RUN=1 "$CLI" --runtime lisp "$WORK_DIR")" assert_contains "$output" 'runtime=lisp' -output="$(AESTHETIC_CODE_DRY_RUN=1 "$CLI" "$WORK_DIR")" +output="$(EASEL_DRY_RUN=1 "$CLI" "$WORK_DIR")" assert_contains "$output" 'runtime=mjs' # Publishing is outward-facing, so it is off unless this session asked for it — # by flag or by environment, with the flag able to say no to the environment. assert_contains "$output" 'autopublish=off' -output="$(AESTHETIC_CODE_DRY_RUN=1 "$CLI" --autopublish "$WORK_DIR")" +output="$(EASEL_DRY_RUN=1 "$CLI" --autopublish "$WORK_DIR")" assert_contains "$output" 'autopublish=on' -output="$(AESTHETIC_CODE_DRY_RUN=1 AESTHETIC_CODE_AUTOPUBLISH=1 "$CLI" "$WORK_DIR")" +output="$(EASEL_DRY_RUN=1 EASEL_AUTOPUBLISH=1 "$CLI" "$WORK_DIR")" assert_contains "$output" 'autopublish=on' -output="$(AESTHETIC_CODE_DRY_RUN=1 AESTHETIC_CODE_AUTOPUBLISH=1 "$CLI" --no-autopublish "$WORK_DIR")" +output="$(EASEL_DRY_RUN=1 EASEL_AUTOPUBLISH=1 "$CLI" --no-autopublish "$WORK_DIR")" assert_contains "$output" 'autopublish=off' -output="$(AESTHETIC_CODE_DRY_RUN=1 "$CLI" "$WORK_DIR")" +output="$(EASEL_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' -output="$(AESTHETIC_CODE_DRY_RUN=1 "$CLI" --backend codex "$WORK_DIR")" +output="$(EASEL_DRY_RUN=1 "$CLI" --backend codex "$WORK_DIR")" assert_contains "$output" 'backend=codex' -output="$(AESTHETIC_CODE_DRY_RUN=1 "$CLI" --backend claude --model claude-opus-5 "$WORK_DIR")" +output="$(EASEL_DRY_RUN=1 "$CLI" --backend claude --model claude-opus-5 "$WORK_DIR")" assert_contains "$output" 'model=claude-opus-5' -if AESTHETIC_CODE_DRY_RUN=1 "$CLI" --backend gemini "$WORK_DIR" >/dev/null 2>&1; then +if EASEL_DRY_RUN=1 "$CLI" --backend gemini "$WORK_DIR" >/dev/null 2>&1; then printf 'Expected an unknown backend to fail.\n' >&2 exit 1 fi @@ -113,17 +113,17 @@ assert_contains "$output" 'engine bridge claude:' assert_contains "$output" 'engine bridge codex:' assert_contains "$output" 'default engine bridge: claude (claude-opus-5)' -if AESTHETIC_CODE_DRY_RUN=1 "$CLI" --runtime rust "$WORK_DIR" >/dev/null 2>&1; then +if EASEL_DRY_RUN=1 "$CLI" --runtime rust "$WORK_DIR" >/dev/null 2>&1; then printf 'Expected an unknown runtime to fail.\n' >&2 exit 1 fi -if AESTHETIC_CODE_DRY_RUN=1 "$CLI" claude >/dev/null 2>&1; then +if EASEL_DRY_RUN=1 "$CLI" claude >/dev/null 2>&1; then printf 'Expected the removed provider shortcut to fail.\n' >&2 exit 1 fi -if AESTHETIC_CODE_DRY_RUN=1 "$CLI" "$WORK_DIR" extra >/dev/null 2>&1; then +if EASEL_DRY_RUN=1 "$CLI" "$WORK_DIR" extra >/dev/null 2>&1; then printf 'Expected extra launcher arguments to fail.\n' >&2 exit 1 fi diff --git a/easel/test/easel.test.mjs b/easel/test/easel.test.mjs new file mode 100644 index 0000000000..62416198b5 --- /dev/null +++ b/easel/test/easel.test.mjs @@ -0,0 +1,41 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { EASEL_HEIGHT, easelDuration, easelFrame, easelNextFrame } from "../src/easel.mjs"; + +const canvas = { piece: "balozo", address: "prompt.ac/@jeffrey/balozo" }; + +test("the easel is always the same size, whatever is on it", () => { + const widths = new Set(); + for (const ms of [0, 100, 500, 1200, 5000]) { + const lines = easelFrame(ms, canvas); + assert.equal(lines.length, EASEL_HEIGHT, "a splash that changes height jumps"); + widths.add(lines[0].length); + } + assert.equal(widths.size, 1, "the frame must not resize as the name is written"); +}); + +test("the name is written on, then the address follows", () => { + const early = easelFrame(60, canvas).join("\n"); + assert.match(early, /b▌/, "mid-write shows a cursor"); + assert.ok(!early.includes("prompt.ac"), "the address waits its turn"); + + const done = easelFrame(easelDuration(canvas.piece, canvas.address), canvas).join("\n"); + assert.ok(done.includes("balozo"), "the piece name lands"); + assert.ok(done.includes(canvas.address), "and so does the address"); + assert.ok(!done.includes("▌"), "the cursor goes away when writing stops"); +}); + +test("it eventually stops asking to be repainted", () => { + assert.equal(easelNextFrame(easelDuration(canvas.piece, canvas.address), canvas), null); + assert.ok(easelNextFrame(0, canvas) > 0); +}); + +// A session that is not signed in has no address yet, and that is ordinary +// rather than an error state — the canvas just stays empty there. +test("an empty address leaves the canvas blank rather than breaking", () => { + const lines = easelFrame(5000, { piece: "balozo", address: "" }); + assert.equal(lines.length, EASEL_HEIGHT); + assert.ok(lines.join("\n").includes("balozo")); + const lines2 = easelFrame(0, {}); + assert.equal(lines2.length, EASEL_HEIGHT, "and neither does having nothing at all"); +}); diff --git a/aesthetic-code/test/fake-app-server.mjs b/easel/test/fake-app-server.mjs similarity index 100% rename from aesthetic-code/test/fake-app-server.mjs rename to easel/test/fake-app-server.mjs diff --git a/aesthetic-code/test/fake-claude-cli.mjs b/easel/test/fake-claude-cli.mjs similarity index 100% rename from aesthetic-code/test/fake-claude-cli.mjs rename to easel/test/fake-claude-cli.mjs diff --git a/aesthetic-code/test/live.test.mjs b/easel/test/live.test.mjs similarity index 99% rename from aesthetic-code/test/live.test.mjs rename to easel/test/live.test.mjs index bd8b310857..ebf17a7744 100644 --- a/aesthetic-code/test/live.test.mjs +++ b/easel/test/live.test.mjs @@ -10,7 +10,7 @@ import { qrBlock } from "../src/qr.mjs"; import { runtimeFor, runtimeForExtension, runtimeIds, runtimeMenu } from "../src/runtimes.mjs"; async function workspace(context) { - const root = await mkdtemp(join(tmpdir(), "aesthetic-code-live-")); + const root = await mkdtemp(join(tmpdir(), "easel-live-")); context.after(() => rm(root, { recursive: true, force: true })); return root; } diff --git a/aesthetic-code/test/mascot.test.mjs b/easel/test/mascot.test.mjs similarity index 100% rename from aesthetic-code/test/mascot.test.mjs rename to easel/test/mascot.test.mjs diff --git a/aesthetic-code/test/publish.test.mjs b/easel/test/publish.test.mjs similarity index 98% rename from aesthetic-code/test/publish.test.mjs rename to easel/test/publish.test.mjs index dfa630a0ab..a4af2b4205 100644 --- a/aesthetic-code/test/publish.test.mjs +++ b/easel/test/publish.test.mjs @@ -8,7 +8,7 @@ import { looksLikePiece, planPublish, publishPiece, slugFor } from "../src/publi const SOURCE = "export function paint({ wipe }) { wipe(70, 50, 100); }\n"; async function piece(context, name = "smiley.mjs", source = SOURCE) { - const root = await mkdtemp(join(tmpdir(), "aesthetic-code-publish-")); + const root = await mkdtemp(join(tmpdir(), "easel-publish-")); context.after(() => rm(root, { recursive: true, force: true })); const file = join(root, name); await writeFile(file, source); diff --git a/aesthetic-code/test/render.test.mjs b/easel/test/render.test.mjs similarity index 100% rename from aesthetic-code/test/render.test.mjs rename to easel/test/render.test.mjs diff --git a/aesthetic-code/test/slab-session.test.mjs b/easel/test/slab-session.test.mjs similarity index 94% rename from aesthetic-code/test/slab-session.test.mjs rename to easel/test/slab-session.test.mjs index 0764365a95..ff301aea89 100644 --- a/aesthetic-code/test/slab-session.test.mjs +++ b/easel/test/slab-session.test.mjs @@ -9,7 +9,7 @@ const readJson = async (path) => JSON.parse(await readFile(path, "utf8")); const exists = async (path) => stat(path).then(() => true, () => false); test("publishes the full Slab prompt lifecycle", async (context) => { - const root = await mkdtemp(join(tmpdir(), "aesthetic-code-slab-")); + const root = await mkdtemp(join(tmpdir(), "easel-slab-")); context.after(() => rm(root, { recursive: true, force: true })); const session = new SlabSession({ cwd: "/project", @@ -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, "aesthetic-code"); + assert.equal(marker.agent_type, "easel"); 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 849d35118d..bf23534f48 100755 --- a/slab/bin/prox-mcp.mjs +++ b/slab/bin/prox-mcp.mjs @@ -43,7 +43,7 @@ const SLAB_HOME = process.env.SLAB_HOME || join(homedir(), ".local", "share", "s const MARKER_DIRS = [join(SLAB_HOME, "state", "active-prompts"), join(SLAB_HOME, "state", "awaiting-prompts")]; const shellQuote = (s) => `'${String(s).replaceAll("'", `'"'"'`)}'`; -const isCodexBacked = (agent) => agent === "codex" || agent === "aesthetic-code"; +const isCodexBacked = (agent) => agent === "codex" || agent === "easel"; async function findFile(root, suffix) { let entries; @@ -304,7 +304,7 @@ async function toolDump({ handle, destination } = {}) { await writeFile(join(out, "manifest.json"), JSON.stringify(manifest, null, 2) + "\n", { mode: 0o600 }); let resume; - if (agent === "aesthetic-code") { + if (agent === "easel") { resume = `#!/bin/sh set -eu bundle=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) @@ -351,8 +351,8 @@ async function toolLaunch({ host, agent, cwd, prompt = "", by, loopboyContact = const wanted = String(host || "").trim().toLowerCase().replace(/\.local$/, ""); if (!wanted) throw new Error("`host` is required (for example, poorslice)."); const agentName = String(agent || "").trim().toLowerCase(); - if (!new Set(["claude", "codex", "aesthetic-code"]).has(agentName)) { - throw new Error("`agent` must be `claude`, `codex`, or `aesthetic-code`."); + if (!new Set(["claude", "codex", "easel"]).has(agentName)) { + throw new Error("`agent` must be `claude`, `codex`, or `easel`."); } if (String(prompt).length > 4000) throw new Error("`prompt` exceeds 4000 characters."); const contactKey = String(loopboyContact || "").trim().toLowerCase(); @@ -542,14 +542,14 @@ const TOOLS = [ { name: "prox_list", description: - "List the prompt rocks across the Slab fleet — every live Claude, Codex, or Aesthetic Code session and headless agent the menubar advertises, as host:name with its status, kind, owning interface, age, and subject. Reads the local fleet ledger cache (no SSH).", + "List the prompt rocks across the Slab fleet — every live Claude, Codex, or Easel session and headless agent the menubar advertises, as host:name with its status, kind, owning interface, age, and subject. Reads the local fleet ledger cache (no SSH).", inputSchema: { type: "object", properties: { host: { type: "string", description: "Only rocks on this machine (e.g. neo, blueberry, panda)." }, status: { type: "string", description: "Filter by status: working | awaiting | complete | rendering | blank | interrupted." }, kind: { type: "string", description: "Filter by kind: session | agent." }, - agent: { type: "string", description: "Filter by owning interface: claude | codex | aesthetic-code." }, + agent: { type: "string", description: "Filter by owning interface: claude | codex | easel." }, }, }, }, @@ -606,12 +606,12 @@ const TOOLS = [ { name: "prox_launch", description: - "Launch a new interactive Claude, Codex, or Aesthetic Code prompt in Terminal.app on a Slab fleet host. SIDE EFFECT: opens a live agent session and may consume account usage. The target accepts only fixed allowlisted launchers, limits cwd to that user's home folder, and binds the endpoint to its tailnet IP; no arbitrary command is accepted.", + "Launch a new interactive Claude, Codex, or Easel prompt in Terminal.app on a Slab fleet host. SIDE EFFECT: opens a live agent session and may consume account usage. The target accepts only fixed allowlisted launchers, limits cwd to that user's home folder, and binds the endpoint to its tailnet IP; no arbitrary command is accepted.", inputSchema: { type: "object", properties: { host: { type: "string", description: "Target Slab hostname, for example poorslice." }, - agent: { type: "string", enum: ["claude", "codex", "aesthetic-code"], description: "Interface to launch." }, + agent: { type: "string", enum: ["claude", "codex", "easel"], description: "Interface to launch." }, cwd: { type: "string", description: "Optional absolute directory on the target. Defaults to its aesthetic-computer checkout and must stay under its home folder." }, prompt: { type: "string", description: "Optional initial prompt, at most 4000 characters. Omit to open an idle TUI." }, by: { type: "string", description: "Optional caller label recorded by the target." }, diff --git a/slab/bin/slab-aesthetic-profiles.swift b/slab/bin/slab-aesthetic-profiles.swift index 2d3fb52dd2..fec043bd07 100644 --- a/slab/bin/slab-aesthetic-profiles.swift +++ b/slab/bin/slab-aesthetic-profiles.swift @@ -3,7 +3,7 @@ // The menubar already switches a tab's `current settings` per session status, // picking a settings set by name (see AppDelegate.profileName). Claude sessions // get the plain `Slab--` family and Codex ones a cooler -// `-codex` twin. Aesthetic Code sessions had no family of their own and were +// `-codex` twin. Easel sessions had no family of their own and were // borrowing Codex's, so the one interface that IS Aesthetic Computer was the // one wearing somebody else's colours. // diff --git a/slab/menubar-swift/Sources/SlabMenubar/AppDelegate.swift b/slab/menubar-swift/Sources/SlabMenubar/AppDelegate.swift index e5f1a2887a..cc1833e879 100644 --- a/slab/menubar-swift/Sources/SlabMenubar/AppDelegate.swift +++ b/slab/menubar-swift/Sources/SlabMenubar/AppDelegate.swift @@ -2310,7 +2310,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate { ) -> (palette: Palette, glyph: String) { // Codex-backed completion is a stronger attention cue than Claude's // calm slate: coral/red, distinct from approval/elicitation amber. - if (agentType == "codex" || agentType == "aesthetic-code"), state == .complete { + if (agentType == "codex" || agentType == "easel"), state == .complete { if dark { return blink ? (Palette(bg: (23500, 3200, 4200), text: (65535, 48000, 46000), @@ -2325,7 +2325,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate { bold: (21000, 300, 800), cursor: (62000, 5000, 5000)), "✓ complete") } let base = baseStatusDecor(for: state, dark: dark, blink: blink) - guard agentType == "codex" || agentType == "aesthetic-code" else { return base } + guard agentType == "codex" || agentType == "easel" else { return base } return (palette: codexTint(base.palette, dark: dark), glyph: base.glyph) } @@ -2491,14 +2491,14 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate { } // Codex-backed interfaces share a settings-set family so their // slightly-cooler palette is provisioned independently of Claude's. - // Aesthetic Code has its own: it borrowed Codex's until now, which put + // Easel has its own: it borrowed Codex's until now, which put // the one interface that IS Aesthetic Computer in somebody else's // colours. `-aesthetic` is the prompt's own scheme, the same values // the harness paints itself with — and it names the palette rather // than the tool, which is still deciding what it is called. let agentSuffix: String switch agentType { - case "aesthetic-code": agentSuffix = "-aesthetic" + case "easel": agentSuffix = "-aesthetic" case "codex": agentSuffix = "-codex" default: agentSuffix = "" } @@ -3080,7 +3080,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate { _ = fontSize let safeCwd = cwd.replacingOccurrences(of: "'", with: "'\\''") let shellCmd: String - if agentType == "aesthetic-code" { + if agentType == "easel" { let safeSid = providerSessionId.replacingOccurrences(of: "'", with: "'\\''") shellCmd = providerSessionId.isEmpty ? "cd '\(safeCwd)' && aesthetic" diff --git a/slab/menubar-swift/Sources/SlabMenubar/ClaudeSession.swift b/slab/menubar-swift/Sources/SlabMenubar/ClaudeSession.swift index 26cdd08390..5786c99906 100644 --- a/slab/menubar-swift/Sources/SlabMenubar/ClaudeSession.swift +++ b/slab/menubar-swift/Sources/SlabMenubar/ClaudeSession.swift @@ -1,7 +1,7 @@ import Foundation /// Generic alias — a tracked agent session is no longer Claude-specific -/// (Codex and Aesthetic Code sessions flow through the same markers + reducer). +/// (Codex and Easel sessions flow through the same markers + reducer). /// New code should /// prefer `AgentSession`; the old name stays valid so existing call sites /// (overlay, snapshot, menu) keep compiling. @@ -79,17 +79,17 @@ struct ClaudeSession { var remoteHost: String = "" /// Which interface owns this session — "claude" (default), "codex", or - /// "aesthetic-code". + /// "easel". /// Read from the marker's `agent_type`; drives per-agent labels/tooltips /// in the menu. The state/color engine is agent-agnostic, so this is /// display-only. var agentType: String = "claude" - /// Aesthetic Code owns its UI while using Codex app-server as its current + /// Easel owns its UI while using Codex app-server as its current /// provider bridge. Provider-thread operations therefore share Codex's /// transcript and resume path without presenting the Codex UI. var isCodexBacked: Bool { - agentType == "codex" || agentType == "aesthetic-code" + agentType == "codex" || agentType == "easel" } /// Optional hardware/platform destination for this prompt (for example @@ -100,13 +100,13 @@ struct ClaudeSession { /// Where a phone can reach the piece this session is holding, as a bare /// host+path (`aesthetic.computer/prompt~…`) with no scheme. Only - /// Aesthetic Code mints one, and only once its piece exists, so an empty + /// Easel mints one, and only once its piece exists, so an empty /// string is the ordinary case and means "no scannable destination yet". /// The prompt rock turns this into a QR surface; everything else ignores it. var scanURL: String = "" /// The piece this session is writing, with its extension — `balozo.mjs`. - /// Only Aesthetic Code sets it, and it names the rock: a session that is + /// Only Easel sets it, and it names the rock: a session that is /// holding a piece should be addressable by that piece's name rather than /// by a second unrelated word drawn from its session id. var piece: String = "" @@ -124,7 +124,7 @@ struct ClaudeSession { /// Human label for the owning interface. var agentLabel: String { - if agentType == "aesthetic-code" { return "Aesthetic Code" } + if agentType == "easel" { return "Easel" } return agentType.isEmpty ? "Claude" : agentType.prefix(1).uppercased() + agentType.dropFirst() @@ -215,9 +215,9 @@ enum ClaudeSessionReader { // has overwritten it yet) — preserve so applyTerminalDecor // paints the appearance-matched bg. // (no-op: keep s.state == .blank) - } else if s.agentType == "aesthetic-code", + } else if s.agentType == "easel", s.state == .complete || s.state == .awaiting || s.state == .interrupted { - // Aesthetic Code receives app-server lifecycle events + // Easel receives app-server lifecycle events // directly, so its marker can state this transition without // waiting for hook-derived side channels. } else if isInterrupted(sessionId: s.sessionId, markerPath: path) { @@ -340,15 +340,15 @@ enum ClaudeSessionReader { }() // Hook-backed clients still derive most state from the side-channel - // markers. Aesthetic Code receives direct app-server lifecycle events + // markers. Easel receives direct app-server lifecycle events // and can publish exact idle/interrupted state itself. let agentType = (obj["agent_type"] as? String) ?? "claude" let parsedState: ClaudeSession.State = { switch obj["state"] as? String { case "blank": return .blank - case "complete" where agentType == "aesthetic-code": return .complete - case "awaiting" where agentType == "aesthetic-code": return .awaiting - case "interrupted" where agentType == "aesthetic-code": return .interrupted + case "complete" where agentType == "easel": return .complete + case "awaiting" where agentType == "easel": return .awaiting + case "interrupted" where agentType == "easel": return .interrupted default: return .working } }() diff --git a/slab/menubar-swift/Sources/SlabMenubar/Ledger.swift b/slab/menubar-swift/Sources/SlabMenubar/Ledger.swift index f1049af999..6b228398fa 100644 --- a/slab/menubar-swift/Sources/SlabMenubar/Ledger.swift +++ b/slab/menubar-swift/Sources/SlabMenubar/Ledger.swift @@ -49,7 +49,7 @@ struct LedgerEntry: Codable, Equatable { /// unlike the route registry, this originates in the live marker. var loopboyContact: String? /// Bare host+path a phone can scan to reach the piece this prompt is - /// holding. Only Aesthetic Code mints one. Optional both because older + /// holding. Only Easel mints one. Optional both because older /// fleet ledgers predate it and because most prompts never have one. var scanURL: String? } @@ -159,9 +159,9 @@ final class LedgerStore { /// home folder. private static func launchPrompt(_ body: [String: Any]) -> [String: Any] { let requestedAgent = ((body["agent"] as? String) ?? "").lowercased() - let agent = requestedAgent == "aesthetic" ? "aesthetic-code" : requestedAgent - guard agent == "claude" || agent == "codex" || agent == "aesthetic-code" else { - return ["ok": false, "error": "agent must be claude, codex, or aesthetic-code"] + let agent = requestedAgent == "aesthetic" ? "easel" : requestedAgent + guard agent == "claude" || agent == "codex" || agent == "easel" else { + return ["ok": false, "error": "agent must be claude, codex, or easel"] } let prompt = (body["prompt"] as? String) ?? "" @@ -203,8 +203,8 @@ final class LedgerStore { let binary: String if agent == "codex" { binary = "\(Paths.slabBin)/codex-slab" - } else if agent == "aesthetic-code" { - binary = "\(Paths.home)/.local/bin/aesthetic" + } else if agent == "easel" { + binary = "\(Paths.home)/.local/bin/easel" } else { binary = "\(Paths.home)/.local/bin/claude" } @@ -228,7 +228,7 @@ final class LedgerStore { command = "cd \(shellQuote(cwd)) && exec \(shellQuote(binary))" } if !prompt.isEmpty { - command += agent == "aesthetic-code" + command += agent == "easel" ? " --prompt \(shellQuote(prompt))" : " \(shellQuote(prompt))" } diff --git a/slab/menubar-swift/Sources/SlabMenubar/PromptScanCode.swift b/slab/menubar-swift/Sources/SlabMenubar/PromptScanCode.swift index ca46193768..c84a16c9d5 100644 --- a/slab/menubar-swift/Sources/SlabMenubar/PromptScanCode.swift +++ b/slab/menubar-swift/Sources/SlabMenubar/PromptScanCode.swift @@ -1,6 +1,6 @@ // PROMPT SCAN CODES. // -// An Aesthetic Code session is holding a live piece, and the way you reach that +// An Easel session is holding a live piece, and the way you reach that // piece from a phone is a QR. Printed into the terminal it costs seventeen rows // of half-blocks and shoves the actual conversation off the top of the pane; // carried by the session's prompt rock it costs nothing but the stone's own diff --git a/slab/menubar-swift/Sources/SlabMenubar/PromptSigilOverlay.swift b/slab/menubar-swift/Sources/SlabMenubar/PromptSigilOverlay.swift index 630108e999..1d0ee76af1 100644 --- a/slab/menubar-swift/Sources/SlabMenubar/PromptSigilOverlay.swift +++ b/slab/menubar-swift/Sources/SlabMenubar/PromptSigilOverlay.swift @@ -2634,12 +2634,12 @@ final class PromptSigilOverlayController { /// Reconcile the badge set with the live sessions. Off when `enabled` is /// false. Only sessions with a real local tty get a badge. /// The code this session's rock should be showing, or nil for the stone. - /// Every gate lives here: only Aesthetic Code mints scan URLs, the URL has + /// Every gate lives here: only Easel mints scan URLs, the URL has /// to exist, and it has to survive the generator — a payload too long for a /// QR, or one that would only fit at a module size no camera can resolve, /// comes back nil and the session keeps an ordinary rock. private func scanCode(for s: ClaudeSession) -> CGImage? { - guard s.agentType == "aesthetic-code" else { return nil } + guard s.agentType == "easel" else { return nil } let url = s.scanURL guard !url.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty, !unscannableURLs.contains(url) else { return nil } @@ -2691,7 +2691,7 @@ final class PromptSigilOverlayController { for s in live { let bare = (s.tty as NSString).lastPathComponent liveParticleTtys.insert(bare) - // An Aesthetic Code session is holding a piece, and the client for + // An Easel session is holding a piece, and the client for // that piece is a phone. When the marker says where to send it, the // rock trades its stone for a code the camera can take. Nil — an // older session, a piece that hasn't minted yet, another agent diff --git a/slab/menubar-swift/Sources/SlabMenubar/ProxMemoirs.swift b/slab/menubar-swift/Sources/SlabMenubar/ProxMemoirs.swift index 0679aac96a..5f4c2b581e 100644 --- a/slab/menubar-swift/Sources/SlabMenubar/ProxMemoirs.swift +++ b/slab/menubar-swift/Sources/SlabMenubar/ProxMemoirs.swift @@ -325,7 +325,7 @@ final class ProxMemoirs { else { continue } var role = "" var content: Any? - if agentType == "codex" || agentType == "aesthetic-code" { + if agentType == "codex" || agentType == "easel" { guard (obj["type"] as? String) == "response_item", let payload = obj["payload"] as? [String: Any], (payload["type"] as? String) == "message" else { continue } diff --git a/slab/menubar-swift/Sources/SlabMenubar/SigilRenderer.swift b/slab/menubar-swift/Sources/SlabMenubar/SigilRenderer.swift index 80ef4ad44b..fa85f4d0d3 100644 --- a/slab/menubar-swift/Sources/SlabMenubar/SigilRenderer.swift +++ b/slab/menubar-swift/Sources/SlabMenubar/SigilRenderer.swift @@ -70,13 +70,13 @@ enum SigilRenderer { return routeName } } - // An Aesthetic Code session is holding one named piece, and that name is + // An Easel session is holding one named piece, and that name is // already the session's public identity: it is the slug in the address on // the rock and the channel the piece is pushed on. Drawing a second, // unrelated word from the session id meant the rock and the piece // disagreed about what the session was called — you would open `balozo` // and be handed `muzad`. The piece wins, and follows a rename. - if session.agentType == "aesthetic-code" { + if session.agentType == "easel" { let slug = (session.piece as NSString).deletingPathExtension let clean = slug.filter { $0.isLetter || $0.isNumber || $0 == "-" || $0 == "_" } if !clean.isEmpty { return String(clean.prefix(16)) }