From f471ac236836d17f9655c8b7f3ab5946c75c59ee Mon Sep 17 00:00:00 2001 From: "prompt.ac/@jeffrey" Date: Sun, 13 Sep 2026 08:03:03 -0400 Subject: [PATCH] easel: a vendor CLI talking to a person is not a protocol error MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `prox:ruzomo` was reporting a JSON parse error in a session that had not run a turn yet. Both bridges read the child's stdout a line at a time and JSON.parse every one of them, and anything that failed became `invalid engine message: Unexpected token …` in the transcript. But a vendor CLI writes to stdout for two audiences. The protocol goes there, and so do the notices meant for whoever is watching — the connector warning, the model-catalog complaint. Neither was ever addressed to us, and both were being shown to the user as errors in their own session. A line that opens with a brace or a bracket was meant to be JSON, and failing to parse that is a real fault worth surfacing. Anything else is the CLI talking, and now goes to the log beside the stderr it would have used had it picked the other pipe. slab-web: previews open small, and two AC defaults are tuned for a full screen. The system paints its corner label at (6, 6) so a visitor can get back; in a tile a few hundred pixels wide that is a sizeable share of the frame and there is nowhere to go back to. `nolabel` reclaims it — verified on a real piece, not assumed: the label is gone and the corner buttons with it. And density is an upscale factor rather than a resolution, so a higher number means fewer, larger pixels. The name reads backwards and I read it backwards: "lower density" wants a *higher* value. Measured at 420x300 — 0.5 illegible, 1 readable, 2 comfortable — so previews get 2, overridable with SLAB_WEB_DENSITY. Only aesthetic.computer and prompt.ac get the parameters; a file:// study or any other URL has no idea what they mean, and an existing query string is extended rather than replaced. Co-Authored-By: Claude Opus 5 (1M context) --- easel/src/app-server.mjs | 14 +++++++++++--- easel/src/claude-server.mjs | 21 ++++++++++++++++++--- slab/bin/slab-web | 23 +++++++++++++++++++++++ 3 files changed, 52 insertions(+), 6 deletions(-) diff --git a/easel/src/app-server.mjs b/easel/src/app-server.mjs index 687d472f10..2e5b90a259 100644 --- a/easel/src/app-server.mjs +++ b/easel/src/app-server.mjs @@ -53,11 +53,19 @@ export class AppServer extends EventEmitter { }); createInterface({ input: this.child.stdout }).on("line", (line) => { - if (!line.trim()) return; + const trimmed = line.trim(); + if (!trimmed) return; try { - this.#receive(JSON.parse(line)); + this.#receive(JSON.parse(trimmed)); } catch (error) { - this.emit("protocolError", new Error(`invalid engine message: ${error.message}`)); + // See claude-server.mjs: a line that was meant to be JSON is a protocol + // fault; anything else is the CLI addressing a person and belongs in the + // log rather than in someone's transcript as an error. + if (trimmed.startsWith("{") || trimmed.startsWith("[")) { + this.emit("protocolError", new Error(`invalid engine message: ${error.message}`)); + } else { + this.emit("log", trimmed); + } } }); diff --git a/easel/src/claude-server.mjs b/easel/src/claude-server.mjs index 9dc66b3337..c30db50849 100644 --- a/easel/src/claude-server.mjs +++ b/easel/src/claude-server.mjs @@ -304,11 +304,26 @@ export class ClaudeServer extends EventEmitter { }); createInterface({ input: child.stdout }).on("line", (line) => { - if (!line.trim()) return; + const trimmed = line.trim(); + if (!trimmed) return; try { - this.#receive(JSON.parse(line)); + this.#receive(JSON.parse(trimmed)); } catch (error) { - this.emit("protocolError", new Error(`invalid engine message: ${error.message}`)); + // A vendor CLI writes to stdout for two audiences: this protocol, and + // the person running it. Connector notices and model-catalog warnings + // arrive on the same pipe as the messages, and parsing them as protocol + // put "invalid engine message: Unexpected token" in the transcript for + // something that was never addressed to us. + // + // A line that opens with a brace or a bracket was meant to be JSON, and + // failing to parse that is a real protocol fault worth surfacing. + // Anything else is the CLI talking, and belongs in the log beside the + // stderr it would have used if it had chosen the other pipe. + if (trimmed.startsWith("{") || trimmed.startsWith("[")) { + this.emit("protocolError", new Error(`invalid engine message: ${error.message}`)); + } else { + this.emit("log", trimmed); + } } }); createInterface({ input: child.stderr }).on("line", (line) => { diff --git a/slab/bin/slab-web b/slab/bin/slab-web index f3a150a928..fcc4dd5a73 100755 --- a/slab/bin/slab-web +++ b/slab/bin/slab-web @@ -32,6 +32,29 @@ case "$url" in *) url="https://aesthetic.computer/$url" ;; # bare token → AC piece/$code esac +# A preview window is small, and two of AC's defaults are tuned for a full +# screen rather than a tile. +# +# The system paints its corner label at (6, 6) so a visitor can get back. In a +# window a few hundred pixels wide that label is a sizeable fraction of the +# frame and there is nowhere to go back to, so `nolabel` reclaims it. +# +# Density is an upscale factor, not a resolution: a higher number draws each AC +# pixel across more screen pixels, so the canvas holds fewer, larger ones. The +# name reads backwards and I had it backwards — measured at 420x300, density=0.5 +# was illegible, 1 was readable, 2 was comfortable. 2 it is: chunkier pixels, +# which is what a piece being watched out of the corner of an eye wants. +# +# Only for aesthetic.computer: a file:// study or an arbitrary URL has no idea +# what these mean. +case "$url" in + https://aesthetic.computer/*|https://prompt.ac/*) + sep="?" + case "$url" in *\?*) sep="&" ;; esac + url="${url}${sep}nolabel&density=${SLAB_WEB_DENSITY:-2}" + ;; +esac + if [ "$CHROME" = "1" ]; then # Real Chrome, --app (frameless) mode, main profile (authed). Invoke the # binary directly so a running Chrome forwards the new app window into your -- 2.51.2