From 3f4820111fb79020b16d196ed24e17dd37fd0bb8 Mon Sep 17 00:00:00 2001 From: "prompt.ac/@jeffrey" Date: Mon, 28 Sep 2026 17:07:08 -0700 Subject: [PATCH] Aesel 0.8.17: a rock keeps its name, /preview shows any piece, ac check 4x faster A session is not its piece. The rock and its prox address used to take the piece's name the moment one was published (bunido became butterfly) and again on every swap. Now a rock keeps the name it was born with and prox:aesel: keeps pointing at the same session; the piece is named on its card and in its address. /preview notepat, /preview @handle/slug, /preview $cow or a URL puts any piece on the rock and its card without making it the session's piece. The session keeps writing its own underneath; bare /preview hands the card back to it. ac check launches puppeteer's headless shell instead of a fresh full Chrome profile and kills the browser rather than waiting on close. Back to back under the same load: 70-78s before, 16-20s after, same findings. Co-Authored-By: Claude Opus 5.5 --- aesel/package.json | 2 +- aesel/src/check-piece.mjs | 22 ++++++++-- aesel/src/slab-session.mjs | 41 +++++++++++++++++-- aesel/src/tui.mjs | 10 ++++- aesel/test/slab-session.test.mjs | 39 +++++++++++++++++- .../Sources/SlabMenubar/ClaudeSession.swift | 5 +-- .../Sources/SlabMenubar/Ledger.swift | 23 ++++------- .../Sources/SlabMenubar/SigilRenderer.swift | 16 +++----- 8 files changed, 119 insertions(+), 39 deletions(-) diff --git a/aesel/package.json b/aesel/package.json index b3b41c432b..97f6f9e7c1 100644 --- a/aesel/package.json +++ b/aesel/package.json @@ -1,6 +1,6 @@ { "name": "aesel", - "version": "0.8.16", + "version": "0.8.17", "private": true, "type": "module", "scripts": { diff --git a/aesel/src/check-piece.mjs b/aesel/src/check-piece.mjs index 9ec7b32271..7429651d00 100644 --- a/aesel/src/check-piece.mjs +++ b/aesel/src/check-piece.mjs @@ -34,10 +34,20 @@ export async function checkPiece(target, { seconds = 4, cwd = process.cwd() } = const address = file ? `${SITE}/${scratch}` : target.startsWith("http") ? target : `${SITE}/${target.replace(/^\/+/, "")}`; const url = `${address}${address.includes("?") ? "&" : "?"}nogap=true&nolabel=true&noauth=true`; + // Puppeteer's own headless shell when it is installed: a fresh profile of + // full Chrome took ~9s to launch and ~13s to close, most of a 40s check, + // and under load booted AC four times slower than the shell did. + let shell = ""; + try { shell = puppeteer.executablePath({ headless: "shell" }); } catch {} const browser = await puppeteer.launch({ - headless: "new", - ...(existsSync(CHROME) ? { executablePath: CHROME } : {}), - args: ["--no-sandbox", "--mute-audio", "--autoplay-policy=no-user-gesture-required"], + ...(shell && existsSync(shell) + ? { headless: "shell", executablePath: shell } + : { headless: "new", ...(existsSync(CHROME) ? { executablePath: CHROME } : {}) }), + args: [ + "--no-sandbox", "--mute-audio", "--autoplay-policy=no-user-gesture-required", + "--no-first-run", "--no-default-browser-check", "--disable-extensions", + "--disable-component-update", "--disable-background-networking", "--disable-sync", + ], timeout: 90000, protocolTimeout: 90000, }); @@ -94,7 +104,11 @@ export async function checkPiece(target, { seconds = 4, cwd = process.cwd() } = await page.keyboard.press("Space").catch(() => {}); await new Promise((r) => setTimeout(r, Math.max(1000, seconds * 1000 - 1200))); } finally { - await browser.close(); + // Nothing is kept from this profile, so there is nothing for a graceful + // close to save; it only made every check wait on Chrome's teardown. + const chrome = browser.process(); + if (chrome) chrome.kill("SIGKILL"); + else 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. diff --git a/aesel/src/slab-session.mjs b/aesel/src/slab-session.mjs index 83482e6eb8..c0d74f2b7b 100644 --- a/aesel/src/slab-session.mjs +++ b/aesel/src/slab-session.mjs @@ -33,6 +33,17 @@ function summary(text) { return words.length > 7 ? `${value.slice(0, 47)}…` : value; } +// Any way of naming a piece → the bare host+path the rock encodes: +// `notepat`, `/notepat:c`, `@jeffrey/butterfly`, `$cow`, +// `https://aesthetic.computer/@jeffrey/butterfly` → `prompt.ac/…`. +export function previewAddress(target = "") { + const path = String(target || "").trim().split(/\s+/)[0] + .replace(/^https?:\/\//, "") + .replace(/^(www\.)?(aesthetic\.computer|prompt\.ac)(\/|$)/, "") + .replace(/^\/+/, ""); + return path ? `prompt.ac/${path}` : ""; +} + export class SlabSession { constructor({ cwd, @@ -58,6 +69,8 @@ export class SlabSession { this.running = join(this.stateDir, "running-tools", sessionId); this.enabled = false; this.heartbeat = null; + // Set while the rock shows a piece that is not this session's (`preview`). + this.pinned = ""; this.cursorSocket = join(this.stateDir, "cursor.sock"); this.shape = "arrow"; this.cursor = null; @@ -135,14 +148,31 @@ export class SlabSession { // session renames its piece, or switches runtime — so the code on the rock // always points at what is actually running. live(piece = "", scanUrl = "", channel = "") { + this.own = { piece: String(piece || ""), piece_channel: String(channel || ""), scan_url: String(scanUrl || "") }; + if (this.pinned) return; this.#update({ - ...(this.record.scan_url !== String(scanUrl || "") ? {piece_published_at:""} : {}), - piece: String(piece || ""), - piece_channel: String(channel || ""), - scan_url: String(scanUrl || ""), + ...(this.record.scan_url !== this.own.scan_url ? {piece_published_at:""} : {}), + ...this.own, }); } + // Point the rock and its preview at any piece — `notepat`, `@handle/slug`, + // `$code`, a full URL — without it becoming this session's piece. The + // session goes on writing its own; `preview()` with nothing hands the card + // back to it. Returns the address shown, or "" when back on its own. + preview(target = "") { + const address = previewAddress(target); + if (!address) { + if (!this.pinned) return ""; + this.pinned = ""; + this.#update({ ...(this.own || { piece: "", piece_channel: "", scan_url: "" }), piece_published_at: "", flow: this.ownFlow || "live" }); + return ""; + } + this.pinned = address; + this.#update({ piece: "", piece_channel: "", scan_url: address, piece_published_at: "", flow: "live" }); + return address; + } + published() { this.#update({piece_published_at:new Date().toISOString()}); } revision(revision) { @@ -162,6 +192,9 @@ export class SlabSession { // question about which to draw that nobody has to answer if it cannot arise. flow(state = "live") { const clean = ["live", "ahead", "pushing"].includes(state) ? state : "live"; + this.ownFlow = clean; + // Someone else's piece on the card is never ahead of our file. + if (this.pinned) return; if (this.record.flow === clean) return; this.#update({ flow: clean }); } diff --git a/aesel/src/tui.mjs b/aesel/src/tui.mjs index e0d17064b4..a908023bd6 100755 --- a/aesel/src/tui.mjs +++ b/aesel/src/tui.mjs @@ -2368,7 +2368,7 @@ async function submitInput(submittedText, submittedMessages = null) { "notice", pro ? "/ask [on|off] · /provider · /model [name] · /mouse [on|off] · /layout · /inbox · /mode · /backend [id] · /login · /logout · /whoami · /handle [name] · /update · /new · /clear · /close · /quit ctrl-c interrupts a running turn" - : "/about · /medium · /artifacts · /select UUID · /artifact · /export FILE · /sharing · /transcript · /profile · /inbox · /mode · /mouse [on|off] · /performance [frames] · /energy · /latest · /login · /logout · /whoami · /publish [file] · /autopublish [on|off] · /ask [on|off] · /piece [name] · /versions · /rollback vN · /runtime [id] · /frame [ocr] · /settings · /backend [id] · /model [name] · /effort · /handle [name] · /update · /open · /qr · /new [thread] · /clear · /quit ctrl-c interrupts a running turn", + : "/about · /medium · /artifacts · /select UUID · /artifact · /export FILE · /sharing · /transcript · /profile · /inbox · /mode · /mouse [on|off] · /performance [frames] · /energy · /latest · /login · /logout · /whoami · /publish [file] · /autopublish [on|off] · /ask [on|off] · /piece [name] · /preview [piece] · /versions · /rollback vN · /runtime [id] · /frame [ocr] · /settings · /backend [id] · /model [name] · /effort · /handle [name] · /update · /open · /qr · /new [thread] · /clear · /quit ctrl-c interrupts a running turn", ); return redraw(); } @@ -2441,6 +2441,14 @@ async function submitInput(submittedText, submittedMessages = null) { } if (command === "/settings" || command === "/effort") return openSettings(command === "/effort" ? 2 : 0); if (command === "/model" || command === "/models") return commandModel(rest); + // Look at any piece on the rock's card without making it this session's: + // `/preview notepat`, `/preview @handle/slug`, `/preview $cow`. Bare + // `/preview` hands the card back to the piece being written here. + if (command === "/preview") { + const shown = slabSession.preview(rest); + addEntry("notice", shown ? `Previewing ${shown} · /preview to come back` : "Previewing this session's own piece"); + return redraw(); + } if (command === "/piece") { if (rest) { try { diff --git a/aesel/test/slab-session.test.mjs b/aesel/test/slab-session.test.mjs index 355c838036..ae6194bb2d 100644 --- a/aesel/test/slab-session.test.mjs +++ b/aesel/test/slab-session.test.mjs @@ -3,7 +3,7 @@ import { mkdtemp, readFile, rm, stat } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import test from "node:test"; -import { SlabSession } from "../src/slab-session.mjs"; +import { SlabSession, previewAddress } from "../src/slab-session.mjs"; const readJson = async (path) => JSON.parse(await readFile(path, "utf8")); const exists = async (path) => stat(path).then(() => true, () => false); @@ -127,3 +127,40 @@ test("a pointer with nobody listening stays quiet", async (context) => { session.pointer("arrow"); session.close(); }); + +test("previews any piece without taking it as the session's own", async (context) => { + const root = await mkdtemp(join(tmpdir(), "easel-slab-")); + context.after(() => rm(root, { recursive: true, force: true })); + const session = new SlabSession({ cwd: "/project", pid: process.pid, tty: "ttys099", sessionId: "ac-preview", slabHome: root }); + const active = join(root, "state", "active-prompts", "ac-preview"); + session.start(); + session.live("butterfly.mjs", "prompt.ac/@jeffrey/butterfly", "jeffrey/butterfly"); + session.flow("ahead"); + + assert.equal(session.preview("https://aesthetic.computer/notepat"), "prompt.ac/notepat"); + let marker = await readJson(active); + assert.equal(marker.scan_url, "prompt.ac/notepat"); + assert.equal(marker.piece, ""); + assert.equal(marker.flow, "live"); + + // The session keeps writing its own piece underneath; the card stays put. + session.live("moth.mjs", "prompt.ac/@jeffrey/moth", "jeffrey/moth"); + session.flow("pushing"); + marker = await readJson(active); + assert.equal(marker.scan_url, "prompt.ac/notepat"); + + assert.equal(session.preview(""), ""); + marker = await readJson(active); + assert.equal(marker.scan_url, "prompt.ac/@jeffrey/moth"); + assert.equal(marker.piece, "moth.mjs"); + assert.equal(marker.flow, "pushing"); +}); + +test("names a piece any way it is written", () => { + assert.equal(previewAddress("notepat"), "prompt.ac/notepat"); + assert.equal(previewAddress("/notepat:c"), "prompt.ac/notepat:c"); + assert.equal(previewAddress("@jeffrey/butterfly"), "prompt.ac/@jeffrey/butterfly"); + assert.equal(previewAddress("$cow"), "prompt.ac/$cow"); + assert.equal(previewAddress("prompt.ac/@jeffrey/butterfly"), "prompt.ac/@jeffrey/butterfly"); + assert.equal(previewAddress(" "), ""); +}); diff --git a/slab/menubar-swift/Sources/SlabMenubar/ClaudeSession.swift b/slab/menubar-swift/Sources/SlabMenubar/ClaudeSession.swift index 6b2572bb6e..68ab49bf36 100644 --- a/slab/menubar-swift/Sources/SlabMenubar/ClaudeSession.swift +++ b/slab/menubar-swift/Sources/SlabMenubar/ClaudeSession.swift @@ -121,9 +121,8 @@ struct ClaudeSession { var artifactPreview: LocalArtifactPreview? /// The piece this session is writing, with its extension — `balozo.mjs`. - /// 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. + /// Only Easel sets it. It titles the piece's card, not the rock: the + /// rock keeps its session name (see `SigilRenderer.name(for:)`). var piece: String = "" var piecePublishedAt: String = "" var pieceVersion: Int = 0 diff --git a/slab/menubar-swift/Sources/SlabMenubar/Ledger.swift b/slab/menubar-swift/Sources/SlabMenubar/Ledger.swift index 6854eb8f02..9753d5dc0f 100644 --- a/slab/menubar-swift/Sources/SlabMenubar/Ledger.swift +++ b/slab/menubar-swift/Sources/SlabMenubar/Ledger.swift @@ -55,7 +55,6 @@ struct LedgerEntry: Codable, Equatable { var proxNamespace: String? var proxName: String? var proxIdentity: String? - var proxPieceName: String? } struct Ledger: Codable { @@ -416,8 +415,10 @@ final class LedgerStore { loopboyContact: s.loopboyContact.isEmpty ? nil : s.loopboyContact, scanURL: s.scanURL.isEmpty ? nil : s.scanURL) } - // Easel namespace follows the current artifact; provider identity stays - // stable for resuming. Retain collision choices only for this piece. + // The Easel address is the rock's own name, never the piece's, so + // `prox:aesel:` keeps pointing at the same session however many + // pieces it writes or previews. Provider identity stays stable for + // resuming, and a collision choice is kept for as long as the session. let previous = (try? JSONDecoder().decode(Ledger.self, from: Data(contentsOf: URL(fileURLWithPath: Self.localFile))))?.entries ?? [] let peerFiles = (try? FileManager.default.contentsOfDirectory(atPath: Self.peersDir)) ?? [] @@ -432,17 +433,12 @@ final class LedgerStore { for index in easelIndices { let session = sessions[index] let identity = session.providerSessionId.isEmpty ? session.sessionId : session.providerSessionId - let filename = (session.piece as NSString).lastPathComponent - let stem = (filename as NSString).deletingPathExtension - let clean = stem.lowercased().filter { $0.isLetter || $0.isNumber || $0 == "-" || $0 == "_" || $0 == "." } - let base = clean.isEmpty ? "untitled" : String(clean.prefix(100)) - let prior = previous.first { $0.proxIdentity == identity && $0.proxPieceName == base }?.proxName + let base = entries[index].name + // Only a collision variant of this same name carries over; a name + // left from when the address followed the piece does not. + let prior = previous.first { $0.proxIdentity == identity }?.proxName + .flatMap { $0 == base || $0.hasPrefix(base + "-") ? $0 : nil } var name = prior ?? base - if used.contains(name) { - let owner = String(session.pieceChannel.split(separator: "/").first ?? "") - .lowercased().filter { $0.isLetter || $0.isNumber || $0 == "-" || $0 == "_" } - name = owner.isEmpty ? base : owner + "/" + base - } if used.contains(name) { let collisionBase = name let suffix = String(format: "%016llx", SigilRenderer.seed(for: identity + "\u{1}" + selfHost)) @@ -460,7 +456,6 @@ final class LedgerStore { entries[index].proxNamespace = "easel" entries[index].proxName = name entries[index].proxIdentity = identity - entries[index].proxPieceName = base } entries.append(contentsOf: advertisedAgents()) diff --git a/slab/menubar-swift/Sources/SlabMenubar/SigilRenderer.swift b/slab/menubar-swift/Sources/SlabMenubar/SigilRenderer.swift index fa85f4d0d3..283ed71baa 100644 --- a/slab/menubar-swift/Sources/SlabMenubar/SigilRenderer.swift +++ b/slab/menubar-swift/Sources/SlabMenubar/SigilRenderer.swift @@ -70,17 +70,11 @@ enum SigilRenderer { return routeName } } - // 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 == "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)) } - } + // An Easel session used to take its piece's name here, so the rock + // renamed itself the moment a piece was published (`bunido` became + // `butterfly`) and again on every swap. A session is not its piece: + // it can write several, and preview any. The rock keeps the one name + // it was born with; the piece is named on its card and its address. return name(forSessionId: session.sessionId) } -- 2.51.2