From 15ef768ee43bd29de89b48a0b5bb0659fcb5a544 Mon Sep 17 00:00:00 2001 From: "prompt.ac/@jeffrey" Date: Wed, 23 Sep 2026 18:49:52 -0700 Subject: [PATCH] aesel pro: the hood and what is under it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The notebook lines are off by default; the cloud stays. The tray — the air, the bar, the status line — sits on black, or on white in light mode, whatever ground the window has: the transcript above is the hood, this is the machinery, and it always looks like itself. A pro session on Claude opens on the newest model the provider's list knows unless one was named, and a remembered family moves up to its newest release. EASEL_NO_REMEMBER=1 keeps a scripted run from touching the preferences. --- easel/package.json | 2 +- easel/src/layout.mjs | 2 +- easel/src/render.mjs | 15 ++++++++++++--- easel/src/tui.mjs | 23 +++++++++++++++++++---- 4 files changed, 33 insertions(+), 9 deletions(-) diff --git a/easel/package.json b/easel/package.json index 7739cda412..d27d4445db 100644 --- a/easel/package.json +++ b/easel/package.json @@ -1,6 +1,6 @@ { "name": "aesel", - "version": "0.7.34", + "version": "0.7.35", "private": true, "type": "module", "scripts": { diff --git a/easel/src/layout.mjs b/easel/src/layout.mjs index 19f0a10a05..fa459fba47 100644 --- a/easel/src/layout.mjs +++ b/easel/src/layout.mjs @@ -80,7 +80,7 @@ export class Layout extends EventEmitter { const base = normalize(readJson(this.baked) || {}); const over = normalize(readJson(this.file) || {}); this.override = over; - this.spec = { bottom: ["gap", "bar", "gap", "status"], status: ["handle", "workspace", "model", "activity"], bar: [95, 70, 135], prompt: "", separator: " · ", mouse: true, ...base, ...over }; + this.spec = { bottom: ["gap", "bar", "gap", "status"], status: ["handle", "workspace", "model", "activity"], bar: [95, 70, 135], prompt: "", separator: " · ", mouse: true, lines: false, ...base, ...over }; return this.spec; } diff --git a/easel/src/render.mjs b/easel/src/render.mjs index ff13408918..f379ac95cf 100644 --- a/easel/src/render.mjs +++ b/easel/src/render.mjs @@ -732,7 +732,7 @@ export function renderFrame(state, columns = 80, rows = 24, useColor = true) { // Codex's shape: the transcript, a bar to type in with a blank line on // either side, and one muted line under it saying who, where, which model. // Nothing sits between the words and the typing. - const barBg = useColor ? bg(shape.bar) : ""; + const barBg = useColor ? bg(state.tray === "light" ? [232, 232, 236] : state.tray === "dark" ? [28, 28, 32] : shape.bar) : ""; const ink = (rgb) => (useColor ? fg(rgb) : ""); let inner; state.cursorCell = null; @@ -774,7 +774,7 @@ export function renderFrame(state, columns = 80, rows = 24, useColor = true) { // Notebook lines: a faint rule under the rest of every transcript row, // the way the GUI's page is ruled. Only the blank part of a row is // underlined, so the words sit on the line rather than under a bar. - const ruled = shape.lines !== false && useColor ? body.map((row, index) => (unruled.has(visible[index]) ? row : ruleRow(row, width))) : body; + const ruled = shape.lines === true && useColor ? body.map((row, index) => (unruled.has(visible[index]) ? row : ruleRow(row, width))) : body; // The transcript as plain rows, for the selection to read and copy from. state.pageRows = body.map((row) => plainRow(row)); // A question takes the page: what is asked, and three answers, nothing @@ -791,7 +791,16 @@ export function renderFrame(state, columns = 80, rows = 24, useColor = true) { return span ? selectRow(state.pageRows[index], span, width, useColor) : row; }) : ruled; - return paintDropdown([...shown, ...shape.bottom.map((name) => rows[name]?.() ?? "")].slice(0, height), state, width, height, useColor, shape) + // Under the hood: the tray — the air, the bar, the status line — sits on + // black, or on white in light mode, whatever ground the window has. The + // transcript above is the hood; this is the machinery, and it always + // looks like itself. + const light = state.tray === "light"; + const trayGround = useColor ? `${bg(light ? [255, 255, 255] : [0, 0, 0])}${fg(light ? [40, 40, 40] : [230, 230, 230])}` : ""; + const trayMuted = useColor ? fg(light ? [110, 110, 110] : [150, 150, 150]) : ""; + const onTray = (row) => (useColor ? `${trayGround}${String(row).replace(/\x1b\[0m/g, `\x1b[0m${trayGround}`).replace(new RegExp(color.ground.replace(/[[\]\\]/g, "\\$&"), "g"), trayGround).replace(new RegExp(color.muted.replace(/[[\]\\]/g, "\\$&"), "g"), trayMuted)}${trayGround}` : row); + const trayRows = shape.bottom.map((name) => onTray(name === "bar" ? bar : name === "gap" ? fit("", width) : rows[name]?.() ?? "")); + return paintDropdown([...shown, ...trayRows].slice(0, height), state, width, height, useColor, shape) .map((line) => `${ground}${fit(line, width)}${reset}`) .join("\n"); } diff --git a/easel/src/tui.mjs b/easel/src/tui.mjs index 1395a028cf..a5bf7f917b 100755 --- a/easel/src/tui.mjs +++ b/easel/src/tui.mjs @@ -16,7 +16,7 @@ import {captureFrame} from "./preview-frame.mjs"; import {inputPixels} from './input-pixels.mjs'; import {API_WORKFLOW} from "./api-context.mjs"; -import { spawn } from "node:child_process"; +import { execFileSync, spawn } from "node:child_process"; import { homedir } from "node:os"; import { writeFile as writeExport } from "node:fs/promises"; import { TranscriptJournal } from "./transcript-journal.mjs"; @@ -40,7 +40,7 @@ import { Diagnostics } from "./diagnostics.mjs"; import { EASEL_HEIGHT, aeselFrame, aeselNextFrame, aeselWidth } from "./easel.mjs"; import { Energy, energyReport } from "./energy.mjs"; import {pickerModels,drawerKey,drawerIndex} from "./provider-picker.mjs"; -import { loadCatalog } from "./model-catalog.mjs"; +import { cachedCatalog, loadCatalog, newestModel, preferNewer } from "./model-catalog.mjs"; import { providerLabel } from "./render.mjs"; import { backendFor, backendMenu, DEFAULT_BACKEND } from "./backends.mjs"; import { GENRES, genreFor } from "./genres.mjs"; @@ -204,8 +204,15 @@ const providerChoice=chooseProviderPreferences({restored:desktopRestored, saved: explicit:{backend:option('--backend')||process.env.EASEL_BACKEND||undefined,model:option('--model')||undefined,effort:option('--effort')||undefined},fallback:process.env.EASEL_DESKTOP?'ac':DEFAULT_BACKEND}); let backend=backendFor(providerChoice.backend); let model=backend.id==='ac'?backend.defaultModel:providerChoice.model??backend.defaultModel; +// Newer is preferred. Unless a model was named on the way in, a pro session on +// Claude opens on the newest model the provider's cached list knows — and a +// remembered family moves up to its newest release the day one appears. +if (pro && backend.id === "claude" && !option("--model")) { + const known = cachedCatalog("claude"); + model = providerChoice.model ? preferNewer(providerChoice.model, known) : newestModel(known) || model; +} let effort=providerChoice.effort; -async function rememberProvider(){try{await saveProviderPreferences({backend:backend.id,model,effort});}catch(error){addEntry('error',`Could not remember provider: ${error.message}`);}} +async function rememberProvider(){if(process.env.EASEL_NO_REMEMBER==='1')return;try{await saveProviderPreferences({backend:backend.id,model,effort});}catch(error){addEntry('error',`Could not remember provider: ${error.message}`);}} let handoff = desktopRestored?.handoff || ""; let archivedConversation = desktopRestored?.archivedConversation || []; // Pro leaves the mouse to the terminal, so a drag selects text the way it @@ -213,6 +220,13 @@ let archivedConversation = desktopRestored?.archivedConversation || []; // EASEL_MOUSE=1 or 0 overrides either default. // The pro frame's shape, read early: the mouse default is part of it. const shape = new Layout(); +// Which appearance the Mac is in, for the tray's ground: black in dark mode, +// white in light. Asked once at start and again on every resize, which is +// also when the Slab menubar redresses a window. +function appearance() { + if (process.platform !== "darwin") return "dark"; + try { return /dark/i.test(execFileSync("defaults", ["read", "-g", "AppleInterfaceStyle"], { encoding: "utf8", timeout: 1500, stdio: ["ignore", "pipe", "ignore"] })) ? "dark" : "light"; } catch { return "light"; } +} let mouseEnabled = process.env.EASEL_MOUSE === "0" ? false : process.env.EASEL_MOUSE === "1" ? true : pro ? (shape.spec.mouse ?? true) : (desktopRestored?.options?.mouseEnabled ?? true); // Every session opens on a new blank piece with a random name. It is a real @@ -291,6 +305,7 @@ const state = { // Not in pro. There the engine carries the user's own settings and servers, // so the prompt is the whole boundary again and it starts closed. autoAllow: !pro, + tray: pro ? appearance() : "", entries: [ // Pro opens onto nothing but the bar: the mode and the model sit under // it, and the agreement was the disclosure. @@ -2704,7 +2719,7 @@ process.stdin.setRawMode(true); process.stdin.resume(); startNativeGamepad(); process.stdin.on("data", handleKeys); -process.stdout.on("resize", redraw); +process.stdout.on("resize", () => { if (pro) state.tray = appearance(); redraw(); }); process.on("SIGWINCH", () => { frameDiff.reset(); lastLayout = ""; lastProvider = ""; lastConversation = ""; lastPrompt = ""; redraw(); }); if (desktopSessionPath) process.on("SIGUSR2", () => { readDesktopIntent(process.env.EASEL_DESKTOP_INTENT).then(requestDesktop).catch((error) => { addEntry("error", errorText(error)); redraw(); }); -- 2.51.2