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