acuvo-code 0.6.9 → 0.6.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/banner.mjs CHANGED
@@ -1,90 +1,128 @@
1
1
  /**
2
2
  * ── ⭐⭐⭐ WHAT YOU SEE WHEN YOU TYPE `acuvo` ────────────────────────────────
3
3
  *
4
- * Roman, 2026-08-22, having typed it: *"it's not opening as a typable terminal,
5
- * with acuvo logo top left and details up top etc, like how claude code opens.
6
- * have you actually designed the page?"* — and then: *"no we want OUR logo"*.
4
+ * ── ⚠️⚠️ REWRITTEN AGAINST A SCREENSHOT OF THE REFERENCE, NOT A DESCRIPTION ──
7
5
  *
8
- * So this is the real mark, not letters spelling the name. It is
9
- * `console/public/brand/acuvo-mark.png` the angular A resampled to
10
- * half-blocks at 14x14 and embedded as text.
6
+ * Roman, 2026-08-22, having put the two side by side: *"look at the sizing of
7
+ * logo and text near it, and what it says, the structure, the wording like
8
+ * theirs, the model version, the repo folder."*
11
9
  *
12
- * ── ⚠️ WHY IT IS EMBEDDED RATHER THAN DECODED AT RUNTIME ────────────────────
10
+ * The reference, read off the image rather than remembered:
13
11
  *
14
- * This package has ZERO dependencies, deliberately, and Node cannot decode a
15
- * PNG without one. Converting at build time and pasting the result keeps that
16
- * promise and costs nothing at startup. If the mark ever changes, re-run the
17
- * conversion — the logo is DERIVED from the brand asset, not drawn by hand, so
18
- * it stays honest to it.
12
+ * [mark] Claude Code v2.1.219
13
+ * Opus 5 (1M context) with high effort · Claude Max
14
+ * C:\Projects\claude-build
15
+ *
16
+ * Three things that were wrong here, each a decision made without looking:
17
+ *
18
+ * 1. **The mark was SEVEN rows.** Theirs is three. A logo taller than the
19
+ * information beside it turns the opening screen into a title card for
20
+ * something you run forty times a day.
21
+ * 2. **The facts were a LABELLED TABLE** — `workspace / model / billing /
22
+ * can run` down a column. Theirs is three prose lines with no labels,
23
+ * because the values are self-describing: a version looks like a version,
24
+ * a path looks like a path. Labels are what you add when the reader cannot
25
+ * tell what they are looking at.
26
+ * 3. **The model line said only the model.** Theirs carries model, effort AND
27
+ * plan on one line separated by `·` — everything about "what am I talking
28
+ * to and who is paying" in one glance.
19
29
  *
20
30
  * ── ⚠️ THE CONSTRAINTS, WHICH ARE NOT PREFERENCES ───────────────────────────
21
31
  *
22
- * · **Half-block glyphs only** (▀ ▄ █). Braille and box-drawing render as tofu
23
- * in cmd.exe and in many CI log viewers, and a logo that renders as question
24
- * marks is worse than no logo — on the exact platform this is developed on.
32
+ * · **Half-block glyphs only** (▀ ▄ █) for the art, and only when the terminal
33
+ * measures them at ONE cell see `lib/glyph-width.mjs`. They are East Asian
34
+ * Ambiguous, so where they render double-width the padding spaces stay narrow
35
+ * and the mark tears.
25
36
  * · **No colour escapes here.** The caller owns colour and honours NO_COLOR; a
26
37
  * module that hard-codes them emits garbage the moment output is piped.
27
- * · **Every line under 80 columns.** The narrowest terminal in real use is 80,
28
- * and a wrapped banner does not read as dense, it reads as broken.
29
- *
30
- * ⭐ A NOTE ON DOING BETTER: `lib/terminal-graphics.mjs` can send a real PNG
31
- * inline on Kitty and iTerm2. It is deliberately NOT used here — Windows
32
- * Terminal speaks neither protocol, so the block art is what most users would
33
- * see anyway, and one rendering that is the same everywhere beats two that
34
- * disagree.
38
+ * · **Never wider than the terminal.** Not 80 the actual width, which a split
39
+ * pane makes much smaller.
35
40
  */
36
41
 
37
42
  /**
38
- * The Acuvo mark, resampled from the brand PNG. Seven rows: tall enough to be
39
- * the mark rather than a smudge, short enough for something run forty times a
40
- * day rather than opened once.
43
+ * The Acuvo mark, in half-blocks. FOUR rows, down from seven.
44
+ *
45
+ * ⚠️ THE HEIGHT IS THE POINT. It has to sit beside three lines of text without
46
+ * dominating them — the proportion the reference uses, and the reason their
47
+ * opening screen reads as a status line rather than a splash.
41
48
  */
42
49
  const MARK = [
43
- ' ▄█',
44
- ' ▄███',
45
- ' ▄█▀▀██',
46
- ' ▄█▀ ▀██',
47
- ' ▄█▀ ██ ▀██',
48
- ' ▄████▀▀█████',
49
- '█▀ █▀ ▀█',
50
+ ' ▄█▄',
51
+ ' ▄█▀█▄',
52
+ ' ▄█▄▄▄█▄',
53
+ '█▀ ▀█',
50
54
  ];
51
55
 
52
56
  /**
53
- * ── ⭐⭐⭐ THE SAME MARK, IN CHARACTERS NOTHING CAN STRETCH ─────────────────
54
- *
55
- * Every glyph here is ASCII, so it is width-1 by definition in every terminal,
56
- * every font, every locale. No ambiguity class, nothing to measure, nothing to
57
- * get wrong.
57
+ * The same mark in characters nothing can stretch.
58
58
  *
59
- * ⚠️ THIS IS NOT "NO LOGO" — that distinction matters. Roman asked for OUR
60
- * mark and then asked again when he got letters spelling the name. The fallback
61
- * for a terminal that cannot draw half-blocks must therefore still BE the
62
- * angular A, drawn a different way, rather than a wordmark standing in for it.
63
- * A degraded logo is a logo; a text substitute is a missing one.
59
+ * ⚠️ THIS IS NOT "NO LOGO" — the distinction matters. Roman asked for OUR mark
60
+ * and asked again when he got letters spelling the name. A terminal that cannot
61
+ * draw half-blocks must still get the angular A, drawn another way. A degraded
62
+ * logo is a logo; a wordmark standing in for one is a missing logo.
64
63
  */
65
64
  const MARK_ASCII = [
66
- ' /\\',
67
- ' / \\',
68
- ' / /\\ \\',
69
- ' / / \\ \\',
70
- ' / /____\\ \\',
71
- '/_/ \\_\\',
65
+ ' /\\',
66
+ ' / \\',
67
+ ' /----\\',
68
+ '/ \\',
72
69
  ];
73
70
 
74
71
  const GUTTER = 2;
75
72
 
76
73
  /**
77
- * The widest the banner may ever be when the terminal will not say how wide it
78
- * is. Kept at 80 because that is the narrowest terminal in real use, so a
79
- * banner built to it fits everywhere.
74
+ * The widest the banner may be when the terminal will not say how wide it is.
75
+ * 80, because that is the narrowest terminal in real use.
80
76
  */
81
77
  export const MAX_BANNER_COLUMNS = 80;
82
78
 
83
- /** Below this the two-column layout stops being a layout and starts being a mess. */
79
+ /** Below this the two-column layout stops being a layout. */
84
80
  const MIN_TEXT_COLUMNS = 30;
85
81
 
82
+ /** Visible length, ignoring ANSI escapes — they occupy no cells. */
83
+ function visibleLength(s) {
84
+ return String(s).replace(/\x1b\[[0-9;]*m/g, '').length;
85
+ }
86
+
86
87
  /**
87
- * Build the opening screen: mark on the left, facts on the right.
88
+ * Cut a possibly-coloured string to `width` visible cells, keeping escapes
89
+ * balanced so a truncation cannot leak colour into the rest of the screen.
90
+ */
91
+ function clampToWidth(s, width) {
92
+ if (visibleLength(s) <= width) return s;
93
+ let out = '';
94
+ let seen = 0;
95
+ const re = /(\x1b\[[0-9;]*m)|([\s\S])/g;
96
+ let m;
97
+ while ((m = re.exec(s)) !== null) {
98
+ if (m[1]) { out += m[1]; continue; }
99
+ if (seen >= width) break;
100
+ out += m[2];
101
+ seen += 1;
102
+ }
103
+ /**
104
+ * ⚠️ ONLY RE-CLOSE A STRING THAT WAS ACTUALLY COLOURED. Appending a reset
105
+ * unconditionally puts four bytes on the end of every truncated PLAIN line —
106
+ * harmless on a terminal, garbage the moment output is piped to a file.
107
+ */
108
+ return out.includes('\x1b') ? `${out}\x1b[0m` : out;
109
+ }
110
+
111
+ /**
112
+ * Elide in the MIDDLE, never the end.
113
+ *
114
+ * The informative parts of a path are the drive and the leaf; chopping the tail
115
+ * leaves `C:\Users\somebody\Projects\a-`, which identifies nothing.
116
+ */
117
+ function fit(value, room) {
118
+ const s = String(value ?? '');
119
+ if (s.length <= room) return s;
120
+ const head = Math.ceil((room - 1) / 2);
121
+ return `${s.slice(0, head)}…${s.slice(s.length - (room - 1 - head))}`;
122
+ }
123
+
124
+ /**
125
+ * Build the opening screen.
88
126
  *
89
127
  * @param {object} o
90
128
  * @param {string} o.version
@@ -104,63 +142,39 @@ export function openingScreen({
104
142
  const brand = paint?.brand ?? ((s) => s);
105
143
 
106
144
  /**
107
- * ── ⚠️⚠️ THE WIDTH IS THE TERMINAL'S, NOT A CONSTANT ──────────────────
108
- *
109
- * This used to build to a fixed 80 columns and never ask. Every line it
110
- * produced was under 80 and it looked immaculate — in an 80-column terminal.
111
- * In a narrower one, which is what a side panel or a split pane is, every
112
- * single row wraps, and a wrapped banner does not read as dense. It reads as
113
- * broken software, which is the first thing a new user sees.
114
- *
115
- * `- 1` because a line that exactly fills the width wraps on some terminals
116
- * and not others, and the difference is not worth one column.
145
+ * ⚠️ THE WIDTH IS THE TERMINAL'S, NOT A CONSTANT. This built to a fixed 80 and
146
+ * never asked; every line was under 80 and it looked immaculate — in an
147
+ * 80-column terminal. In a split pane every row wrapped.
117
148
  */
118
149
  const width = Math.max(20, Math.min(MAX_BANNER_COLUMNS, (columns ?? MAX_BANNER_COLUMNS) - 1));
119
150
 
120
151
  const mark = style === 'text' ? MARK_ASCII : MARK;
121
152
  const markWidth = Math.max(...mark.map((l) => l.length));
122
153
 
123
- /**
124
- * ⭐ THREE LAYOUTS, CHOSEN BY WHAT ACTUALLY FITS — never by a platform guess.
125
- * Side by side when there is room for both; mark above the facts when there
126
- * is room for the mark alone; facts only when there is not. The last one is
127
- * rare and it still has to be right, because a 24-column terminal is somebody
128
- * on a phone over SSH and they deserve a legible screen, not a torn one.
129
- */
130
154
  const sideBySide = width >= markWidth + GUTTER + MIN_TEXT_COLUMNS;
131
155
  const textColumns = sideBySide ? width - markWidth - GUTTER : width;
156
+ const room = Math.max(12, textColumns);
132
157
 
133
158
  /**
134
- * ⚠️ ELIDED IN THE MIDDLE, NOT THE END. The informative parts of a path are
135
- * the drive and the leaf; chopping the tail leaves `C:\Users\somebody\Projects\a-`,
136
- * which identifies nothing. Same for a model id, where the family is at the
137
- * front and the variant at the back.
159
+ * ── THREE LINES, NO LABELS, IN THE REFERENCE'S ORDER ────────────────────
160
+ *
161
+ * Name and version · what you are talking to and who pays · where you are.
162
+ *
163
+ * ⚠️ `billing` JOINS THE MODEL LINE rather than taking a row of its own, which
164
+ * is how the reference does it ("… with high effort · Claude Max"). It matters
165
+ * more here than it does for them: our billing line is the one that says
166
+ * **"YOUR OWN OpenRouter key (not your Acuvo plan)"**, and somebody needs to
167
+ * see that beside the model, not three rows away from it.
138
168
  */
139
- const room = Math.max(8, textColumns - 11);
140
- const fit = (v) => {
141
- const s = String(v ?? '');
142
- if (s.length <= room) return s;
143
- const head = Math.ceil((room - 1) / 2);
144
- return `${s.slice(0, head)}…${s.slice(s.length - (room - 1 - head))}`;
145
- };
146
-
147
169
  const right = [
148
- `ACUVO CODE${version ? ` ${version}` : ''}`,
149
- '',
150
- `workspace ${fit(workspace)}`,
151
- `model ${fit(model)}`,
152
- `billing ${fit(billing)}`,
153
- `can run ${fit(canRun)}`,
154
- '',
170
+ `Acuvo Code${version ? ` v${version}` : ''}`,
171
+ fit([model, billing].filter(Boolean).join(' · '), room),
172
+ fit(workspace, room),
155
173
  ];
156
174
 
157
175
  const lines = [''];
158
176
 
159
177
  if (sideBySide) {
160
- /**
161
- * ⚠️ THE TWO COLUMNS ARE ZIPPED, NOT CONCATENATED, and the row counts are
162
- * allowed to differ — whichever is shorter simply runs out.
163
- */
164
178
  const rows = Math.max(mark.length, right.length);
165
179
  for (let i = 0; i < rows; i += 1) {
166
180
  const text = right[i] ?? '';
@@ -170,39 +184,42 @@ export function openingScreen({
170
184
  * right-hand column drifts.
171
185
  *
172
186
  * ⚠️⚠️ AND PADDED ONLY WHEN SOMETHING FOLLOWS. On a mark-only row the
173
- * padding sits INSIDE the colour, before the reset, where `trimEnd`
174
- * cannot reach it — so the coloured banner carried trailing whitespace the
175
- * plain one did not. Invisible, but it means colour changed the layout.
187
+ * padding sits INSIDE the colour, before the reset, where `trimEnd` cannot
188
+ * reach it — so the coloured banner carried trailing whitespace the plain
189
+ * one did not, which means colour changed the layout.
176
190
  */
177
191
  const raw = mark[i] ?? '';
178
192
  const padded = text ? raw.padEnd(markWidth + GUTTER) : raw;
179
193
  lines.push(`${mark[i] ? brand(padded) : padded}${text}`.trimEnd());
180
194
  }
181
195
  } else {
182
- const roomForMark = width >= markWidth;
183
- if (roomForMark) {
196
+ if (width >= markWidth) {
184
197
  for (const row of mark) lines.push(brand(row));
185
198
  lines.push('');
186
199
  }
187
200
  for (const text of right) lines.push(text.trimEnd());
188
201
  }
189
202
 
190
- lines.push('');
191
-
192
203
  /**
193
- * ⚠️ ONLY WHEN A PROMPT ACTUALLY FOLLOWS. Printing "type what you want done"
194
- * above a one-shot run that has already been given its task is an instruction
195
- * for something the user cannot do.
204
+ * ── ⚠️ WHAT IT MAY RUN IS A SEPARATE, QUIETER LINE ─────────────────────────
196
205
  *
197
- * And it is SHORTENED rather than wrapped when the terminal is narrow. A
198
- * hint that wraps onto a second line looks like an error message.
206
+ * The reference puts this at the very bottom of the screen ("bypass
207
+ * permissions on (shift+tab to cycle)"), away from the identity block, because
208
+ * it is a MODE rather than a fact about the session. Keeping it out of the
209
+ * three-line lockup is what stops that block growing back into the table it
210
+ * used to be.
199
211
  */
212
+ if (canRun) {
213
+ lines.push('', clampToWidth(fit(canRun, width), width));
214
+ }
215
+
216
+ lines.push('');
217
+
200
218
  if (interactive) {
201
219
  /**
202
220
  * ⭐ SHORTENED IN STAGES, NEVER TRUNCATED. A clamp would cut "leave" to
203
- * "leav", and a hint with a word chopped in half does not read as a
204
- * compact hint it reads as a rendering bug, which is precisely the
205
- * impression this screen keeps making.
221
+ * "leav", and a hint with a word chopped in half reads as a rendering bug —
222
+ * precisely the impression this screen keeps making.
206
223
  */
207
224
  const hints = [
208
225
  ' Type what you want done. /help for commands · exit to leave',
@@ -213,43 +230,11 @@ export function openingScreen({
213
230
  }
214
231
 
215
232
  /**
216
- * ⚠️⚠️ THE LAST WORD ON WIDTH, AND IT IS NOT A BELT-AND-BRACES CHECK.
217
- * Everything above reasons about `.length`, which counts CODE UNITS, while a
218
- * terminal counts CELLS and the whole reason this file was rewritten is that
219
- * those two disagree. This clamp is measured on the painted string with the
220
- * escapes discounted, so a mark that turns out wider than advertised is
221
- * truncated rather than allowed to wrap and tear the layout.
233
+ * ⚠️⚠️ THE LAST WORD ON WIDTH. Everything above reasons about `.length`, which
234
+ * counts CODE UNITS, while a terminal counts CELLS and the whole reason this
235
+ * file was rewritten is that those two disagree. Measured on the painted
236
+ * string with escapes discounted, so a mark wider than advertised is truncated
237
+ * rather than allowed to wrap and tear the layout.
222
238
  */
223
239
  return lines.map((l) => clampToWidth(l, width)).join('\n');
224
240
  }
225
-
226
- /** Visible length, ignoring ANSI escapes — they occupy no cells. */
227
- function visibleLength(s) {
228
- return String(s).replace(/\[[0-9;]*m/g, '').length;
229
- }
230
-
231
- /**
232
- * Cut a possibly-coloured string to `width` visible cells, keeping the escapes
233
- * balanced so a truncation cannot leak colour into the rest of the screen.
234
- */
235
- function clampToWidth(s, width) {
236
- if (visibleLength(s) <= width) return s;
237
- let out = '';
238
- let seen = 0;
239
- const re = /(\[[0-9;]*m)|([\s\S])/g;
240
- let m;
241
- while ((m = re.exec(s)) !== null) {
242
- if (m[1]) { out += m[1]; continue; }
243
- if (seen >= width) break;
244
- out += m[2];
245
- seen += 1;
246
- }
247
- /**
248
- * ⚠️ ONLY RE-CLOSE A STRING THAT WAS ACTUALLY COLOURED. Appending a reset
249
- * unconditionally puts four bytes on the end of every truncated PLAIN line —
250
- * harmless on a terminal, and garbage the moment output is piped to a file or
251
- * a CI log, which is the one place this module has already been told never to
252
- * write escapes.
253
- */
254
- return out.includes('') ? `${out}` : out;
255
- }
package/lib/input-box.mjs CHANGED
@@ -557,7 +557,32 @@ export function pinRegion(output, { rows = 2, env = process.env } = {}) {
557
557
  * is a quieter version of the same defect this rewrite removes.
558
558
  */
559
559
  const makeRoom = '\n'.repeat(rows);
560
- output.write(`${makeRoom}${CSI}${rows}A${CSI}1;${bottom}r${CSI}${bottom};1H`);
560
+ /**
561
+ * ── ⚠️⚠️ THE CURSOR GOES BACK WHERE THE CONTENT ENDED, NOT TO THE BOTTOM ──
562
+ *
563
+ * Roman, from a screenshot: the banner sat at the top, then forty blank
564
+ * rows, then the invitation alone near the bottom — "the prompt box is too
565
+ * low and you have to scroll down to see everything."
566
+ *
567
+ * The cause was the last move in the old sequence, `ESC[{bottom};1H`. It
568
+ * parked the cursor on the LAST row of the scrolling region, so the first
569
+ * thing printed after pinning landed at the bottom of the screen and the
570
+ * whole gap opened up behind it. A terminal fills DOWNWARD: after reserving
571
+ * rows, output has to carry on from exactly where the banner stopped.
572
+ *
573
+ * ⭐ DECSC/DECRC (`ESC7`/`ESC8`) rather than an absolute move, because the
574
+ * right row is "wherever the content happens to end" and only the terminal
575
+ * knows that. Setting a scroll region homes the cursor to (1,1) as a side
576
+ * effect — that is the reason a restore is needed at all, and the reason the
577
+ * old code moved the cursor somewhere explicit in the first place. It just
578
+ * moved it to the wrong somewhere.
579
+ *
580
+ * ⚠️ The save happens AFTER the newlines and the step back up, so the
581
+ * position being restored is already inside the region — a restore to a row
582
+ * below the region would put the cursor outside it, which is undefined
583
+ * across terminals.
584
+ */
585
+ output.write(`${makeRoom}${CSI}${rows}A\x1b7${CSI}1;${bottom}r\x1b8`);
561
586
 
562
587
  const release = () => {
563
588
  if (released) return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "acuvo-code",
3
- "version": "0.6.9",
3
+ "version": "0.6.10",
4
4
  "description": "Acuvo Code — the terminal client for the Acuvo capability registry. Zero dependencies, by design.",
5
5
  "type": "module",
6
6
  "bin": {