aegiscode 5.2.33 → 6.1.0

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/src/screen.js ADDED
@@ -0,0 +1,352 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Terminal plumbing: cell-accurate width maths, the span/line render model,
5
+ * wrapping, and the ephemeral "live" region the CLI redraws while a call is in
6
+ * flight.
7
+ *
8
+ * The render model is aegiscodex-dev's: a line is an array of spans
9
+ * `{ t: text, s: style-prefix, w: cell-width }`, built with `span()`, measured
10
+ * with `lineWidth()`, padded/truncated with `padLine()`, and painted with
11
+ * `paint()`. Two width functions exist because they have different callers:
12
+ * `w()` returns the display width of a *plain string* (the linear transcript
13
+ * and the string renderers in render.js use it), while `padLine()` does the
14
+ * per-codepoint maths for a span line (the overlays use it).
15
+ *
16
+ * Design note — this CLI is a LINEAR transcript, not a full-screen alt-buffer
17
+ * TUI (which is what Claude Code and aegiscodex-dev are). Finished turns are
18
+ * written once to scrollback, so output stays selectable, pipeable and
19
+ * scrollback-searchable; only the one live status/spinner line is redrawn. The
20
+ * main session loop deliberately does NOT enter the alternate screen — `--print`
21
+ * and piped runs must keep working. The alt-screen / mouse / bracketed-paste
22
+ * helpers below exist only so the overlay renderers (overlays.js) and any future
23
+ * modal can paint and restore cleanly; they are never called on the transcript
24
+ * path. Keeping the two concerns in one module (rather than two) is the same
25
+ * split the reference uses.
26
+ */
27
+
28
+ const ANSI_RE = /\x1b\[[0-9;]*[A-Za-z]/g;
29
+
30
+ // Real terminal cell width (wcwidth-style). Combining marks and variation
31
+ // selectors occupy no cell; CJK fullwidth forms and emoji-presentation glyphs
32
+ // occupy two; everything else one. A plain code-point count misaligns every
33
+ // width decision — padding, wrapping, cursor placement — the moment a line
34
+ // contains CJK or emoji, because the terminal draws those two cells wide.
35
+ // (Regexes copied verbatim from aegiscodex-dev/src/screen.js.)
36
+ const RE_ZERO = /[\u0300-\u036F\u1AB0-\u1AFF\u1DC0-\u1DFF\u20D0-\u20FF\uFE00-\uFE0F\uFE20-\uFE2F]/u;
37
+ const RE_WIDE = /[\u1100-\u115F\u2E80-\u303E\u3041-\u33FF\u3400-\u4DBF\u4E00-\u9FFF\uA000-\uA4CF\uA960-\uA97F\uAC00-\uD7A3\uF900-\uFAFF\uFE10-\uFE19\uFE30-\uFE4F\uFF00-\uFF60\uFFE0-\uFFE6\u{1F1E6}-\u{1F1FF}\u{1F300}-\u{1F64F}\u{1F680}-\u{1F6FF}\u{1F900}-\u{1F9FF}\u{1FA70}-\u{1FAFF}\u{20000}-\u{3FFFD}]/u;
38
+ /** Cell width of one code point. */
39
+ const cwidth = (ch) => (RE_ZERO.test(ch) ? 0 : RE_WIDE.test(ch) ? 2 : 1);
40
+
41
+ function stripAnsi(s) {
42
+ return String(s).replace(ANSI_RE, '');
43
+ }
44
+
45
+ /** Display width of a string in terminal cells. ANSI escapes are transparent
46
+ * (so a styled line measures the same as its visible text) — kept from the
47
+ * previous revision so callers measuring render output stay correct. */
48
+ function w(s) {
49
+ let n = 0;
50
+ for (const ch of stripAnsi(s)) {
51
+ if (RE_ZERO.test(ch)) continue;
52
+ n += RE_WIDE.test(ch) ? 2 : 1;
53
+ }
54
+ return n;
55
+ }
56
+
57
+ /** Truncate a plain string to `width` cells without cutting a wide/combining
58
+ * codepoint. */
59
+ function clip(s, width) {
60
+ if (w(s) <= width) return s;
61
+ let out = '';
62
+ let used = 0;
63
+ for (const ch of String(s)) {
64
+ const cw = cwidth(ch);
65
+ if (used + cw > width) break;
66
+ out += ch;
67
+ used += cw;
68
+ }
69
+ return out;
70
+ }
71
+
72
+ /** Pad (or clip) a plain string to exactly `width` cells. */
73
+ function pad(s, width) {
74
+ const t = clip(s, width);
75
+ return t + ' '.repeat(Math.max(0, width - w(t)));
76
+ }
77
+
78
+ /** Right-align a plain string inside `width`. */
79
+ function padStart(s, width) {
80
+ const t = clip(s, width);
81
+ return ' '.repeat(Math.max(0, width - w(t))) + t;
82
+ }
83
+
84
+ // ── the span/line render model ──────────────────────────────────────────────
85
+
86
+ /** A styled text run. Shape { t, s, w } is the contract with overlays.js and
87
+ * markdown.js. */
88
+ function span(style, t) {
89
+ return { t, s: style, w: w(t) };
90
+ }
91
+
92
+ /** Total display width of a span line. */
93
+ const lineWidth = (line) => line.reduce((a, sp) => a + sp.w, 0);
94
+
95
+ /**
96
+ * Pad / truncate a span line to an exact cell width. Truncation is visual only
97
+ * (a trailing style reset may be dropped) — the terminal clips whole glyphs, and
98
+ * a 2-cell glyph is never split mid-width.
99
+ */
100
+ function padLine(line, width) {
101
+ let cur = 0;
102
+ const out = [];
103
+ for (const sp of line) {
104
+ if (cur >= width) break;
105
+ let t = sp.t;
106
+ let tw = sp.w;
107
+ if (cur + tw > width) {
108
+ // Truncate by whole code points that fit in the remaining cells — a
109
+ // codepoint slice (keep chars) would split a 2-cell glyph mid-width.
110
+ const room = width - cur;
111
+ let piece = '';
112
+ let used = 0;
113
+ for (const ch of [...t]) {
114
+ const cw = w(ch);
115
+ if (used + cw > room) break;
116
+ piece += ch;
117
+ used += cw;
118
+ }
119
+ t = piece;
120
+ tw = used;
121
+ }
122
+ out.push({ t, s: sp.s, w: tw });
123
+ cur += tw;
124
+ }
125
+ if (cur < width) out.push(span('', ' '.repeat(width - cur)));
126
+ return out;
127
+ }
128
+
129
+ /** Term size, clamped so a zero-column report (CI, redirected stdout) still
130
+ * yields a paintable frame. */
131
+ const getSize = () => ({
132
+ cols: Math.max(20, (process.stdout && process.stdout.columns) || 80),
133
+ rows: Math.max(5, (process.stdout && process.stdout.rows) || 24),
134
+ });
135
+
136
+ // ── wrapping (plain strings; the string renderers use these) ─────────────────
137
+
138
+ /**
139
+ * Word-wrap one logical plain line to `width` cells, preserving words and never
140
+ * breaking mid-word unless the word alone exceeds the width.
141
+ */
142
+ function wrapLine(line, width, indent = '') {
143
+ const limit = Math.max(8, width);
144
+ if (w(line) <= limit) return [line];
145
+ const words = line.split(' ');
146
+ const out = [];
147
+ let cur = '';
148
+ for (const word of words) {
149
+ const piece = cur ? `${cur} ${word}` : word;
150
+ if (w(piece) <= limit) {
151
+ cur = piece;
152
+ continue;
153
+ }
154
+ if (cur) out.push(cur);
155
+ if (w(word) <= limit) {
156
+ cur = word;
157
+ continue;
158
+ }
159
+ // A single token longer than the line: hard-split on cells.
160
+ let rest = word;
161
+ while (w(rest) > limit) {
162
+ const head = clip(rest, limit);
163
+ out.push(head);
164
+ rest = rest.slice(head.length);
165
+ }
166
+ cur = rest;
167
+ }
168
+ if (cur || !out.length) out.push(cur);
169
+ return out;
170
+ }
171
+
172
+ /** Wrap a block of plain text (honours existing newlines), applying `indent`. */
173
+ function wrapBlock(text, width, indent = '', hang = false) {
174
+ const lines = [];
175
+ for (const raw of String(text).split('\n')) {
176
+ if (raw === '') {
177
+ lines.push('');
178
+ continue;
179
+ }
180
+ const parts = wrapLine(raw, width - w(indent));
181
+ parts.forEach((p) => lines.push(indent + p));
182
+ }
183
+ return lines;
184
+ }
185
+
186
+ // ── cursor / screen control ─────────────────────────────────────────────────
187
+
188
+ const ESC = {
189
+ hideCursor: '\x1b[?25l',
190
+ showCursor: '\x1b[?25h',
191
+ clearToEnd: '\x1b[0K',
192
+ clearLine: '\x1b[2K',
193
+ clearScreen: '\x1b[2J\x1b[H',
194
+ up: (n) => (n > 0 ? `\x1b[${n}A` : ''),
195
+ down: (n) => (n > 0 ? `\x1b[${n}B` : ''),
196
+ col: (n) => `\x1b[${n}G`,
197
+ };
198
+
199
+ const hideCursor = () => process.stdout.write('\x1b[?25l');
200
+ const showCursor = () => process.stdout.write('\x1b[?25h');
201
+ const moveTo = (row, col) => process.stdout.write(`\x1b[${row};${col}H`);
202
+
203
+ function clearScreen() {
204
+ process.stdout.write('\x1b[2J\x1b[H');
205
+ }
206
+
207
+ // Alternate screen buffer. Not used by the transcript (see the module note);
208
+ // available to overlays that need to paint and then restore the pre-launch
209
+ // terminal exactly. Guarded so paired enter/leave stay balanced.
210
+ let inAlt = false;
211
+ function enterAltScreen() {
212
+ if (inAlt) return;
213
+ inAlt = true;
214
+ process.stdout.write('\x1b[?1049h');
215
+ }
216
+ function leaveAltScreen() {
217
+ if (!inAlt) return;
218
+ inAlt = false;
219
+ process.stdout.write('\x1b[?1049l');
220
+ }
221
+ const isAltScreen = () => inAlt;
222
+
223
+ // Bracketed paste (DEC 2004): a paste arrives as one \x1b[200~ … \x1b[201~
224
+ // chunk instead of a burst of keystrokes.
225
+ const enableBracketedPaste = () => process.stdout.write('\x1b[?2004h');
226
+ const disableBracketedPaste = () => process.stdout.write('\x1b[?2004l');
227
+
228
+ // Mouse button-event tracking (DECSET 1000) + SGR coordinates (1006). Only the
229
+ // alt-screen/overlay path turns these on; disabled on exit so the shell we
230
+ // return to isn't left forwarding raw wheel bytes as input.
231
+ const enableMouseTracking = () => process.stdout.write('\x1b[?1000h\x1b[?1006h');
232
+ const disableMouseTracking = () => process.stdout.write('\x1b[?1000l\x1b[?1006l');
233
+
234
+ /**
235
+ * Paint span lines top-aligned and clear the rest of the frame. Used by the
236
+ * overlay renderers; the transcript path never calls it (it stays linear).
237
+ */
238
+ function paint(lines) {
239
+ const { rows, cols } = getSize();
240
+ let out = '\x1b[H';
241
+ for (let i = 0; i < rows; i++) {
242
+ // Reset at the top of every row: span styles are colour codes, and a colour
243
+ // code does NOT clear bold/italic/dim. Without this a row that ends inside a
244
+ // BOLD segment whose "off" span got dropped by padding would leak the
245
+ // attribute into every later row of the frame.
246
+ out += '\x1b[0m';
247
+ const line = lines[i];
248
+ if (line) {
249
+ const pl = padLine(line, cols);
250
+ for (const sp of pl) out += sp.s + sp.t;
251
+ out += '\x1b[0K';
252
+ } else {
253
+ out += '\x1b[0K';
254
+ }
255
+ if (i < rows - 1) out += '\n';
256
+ }
257
+ out += '\x1b[0m';
258
+ process.stdout.write(out);
259
+ }
260
+
261
+ /** Repaint from `startRow` down, leaving rows above it untouched. */
262
+ function paintFrom(lines, startRow) {
263
+ const { rows, cols } = getSize();
264
+ let out = '';
265
+ for (let i = 0; i < rows - startRow; i++) {
266
+ const line = lines[startRow + i];
267
+ if (i > 0) out += '\n';
268
+ out += '\x1b[0K';
269
+ out += '\x1b[0m';
270
+ if (line) {
271
+ const pl = padLine(line, cols);
272
+ for (const sp of pl) out += sp.s + sp.t;
273
+ }
274
+ }
275
+ out += '\x1b[0m';
276
+ process.stdout.write(out);
277
+ }
278
+
279
+ function termWidth(fallback = 80) {
280
+ return Math.max(20, (process.stdout && process.stdout.columns) || fallback);
281
+ }
282
+
283
+ /**
284
+ * A block of lines at the bottom of the transcript that can be redrawn in
285
+ * place. Every write goes through here, so the escape arithmetic lives in one
286
+ * place: `update()` erases the previous block, prints the new one, and keeps
287
+ * the cursor below it.
288
+ */
289
+ class LiveRegion {
290
+ constructor(out = process.stdout) {
291
+ this.out = out;
292
+ this.rows = 0;
293
+ this.active = false;
294
+ }
295
+
296
+ update(lines) {
297
+ const body = lines.map((l) => l + ESC.clearToEnd).join('\n');
298
+ if (!this.active) {
299
+ this.out.write(body);
300
+ this.active = true;
301
+ } else {
302
+ // Back to the first row of the old block, rewrite, clear the tail.
303
+ this.out.write(ESC.col(1) + ESC.up(this.rows) + body + '\n' + ESC.col(1) + ESC.up(1));
304
+ }
305
+ this.rows = lines.length;
306
+ }
307
+
308
+ /** Erase the block and return the cursor to its start, so the caller can
309
+ * print the durable version of the same content. */
310
+ clear() {
311
+ if (!this.active) return;
312
+ this.out.write(ESC.col(1) + ESC.clearLine);
313
+ for (let i = 1; i < this.rows; i++) this.out.write(`\n${ESC.clearLine}`);
314
+ this.out.write(ESC.up(this.rows - 1) + ESC.col(1));
315
+ this.rows = 0;
316
+ this.active = false;
317
+ }
318
+ }
319
+
320
+ module.exports = {
321
+ // plain-string width helpers (kept for the string renderers + app/bin)
322
+ w,
323
+ clip,
324
+ pad,
325
+ padStart,
326
+ stripAnsi,
327
+ wrapLine,
328
+ wrapBlock,
329
+ // span/line render model (from aegiscodex-dev)
330
+ span,
331
+ lineWidth,
332
+ padLine,
333
+ paint,
334
+ paintFrom,
335
+ getSize,
336
+ // cursor / screen / input helpers (overlay path)
337
+ EC: ESC,
338
+ hideCursor,
339
+ showCursor,
340
+ moveTo,
341
+ clearScreen,
342
+ enterAltScreen,
343
+ leaveAltScreen,
344
+ isAltScreen,
345
+ enableBracketedPaste,
346
+ disableBracketedPaste,
347
+ enableMouseTracking,
348
+ disableMouseTracking,
349
+ // the live region + sizing (app/bin)
350
+ LiveRegion,
351
+ termWidth,
352
+ };
package/src/theme.js ADDED
@@ -0,0 +1,156 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Design tokens — the aegiscodex-dev design system, adopted wholesale.
5
+ *
6
+ * Every value below is copied verbatim from `aegiscodex-dev/src/theme.js`, which
7
+ * extracted them from the live ANSI stream of Claude Code: gold 255,193,7 ·
8
+ * coral 215,119,87 · lavender 177,185,249 · blue 120,160,250 · green 78,186,101
9
+ * · red 220,90,90 · gray 153,153,153 · dim 80,80,80.
10
+ *
11
+ * `test/cli-conformance.test.mjs` pins these to aegiscodex-dev's source, so the
12
+ * CLI cannot drift away from the design it is meant to match. (The previous
13
+ * revision of this file was the inverse: a violet/cyan "Signal" palette with a
14
+ * test asserting divergence. That theme is gone.)
15
+ *
16
+ * Zero dependencies: SGR escapes are hand-built, like the rest of this repo.
17
+ */
18
+
19
+ const RESET = '\x1b[0m';
20
+ const BOLD = '\x1b[1m';
21
+ const DIM = '\x1b[2m';
22
+ const ITALIC = '\x1b[3m';
23
+ const UNDER = '\x1b[4m';
24
+ const BOLD_OFF = '\x1b[22m';
25
+ const ITALIC_OFF = '\x1b[23m';
26
+ const UNDER_OFF = '\x1b[24m';
27
+ const RESET_FG = '\x1b[39m';
28
+ const RESET_BG = '\x1b[49m';
29
+
30
+ const RGB = (r, g, b) => `\x1b[38;2;${r};${g};${b}m`;
31
+ const BG = (r, g, b) => `\x1b[48;2;${r};${g};${b}m`;
32
+
33
+ /** The dark palette. Role names match aegiscodex-dev exactly. */
34
+ const C = {
35
+ gold: RGB(255, 193, 7), // header line, accents, welcome frame
36
+ coral: RGB(215, 119, 87), // "Welcome to ..." title
37
+ lavender: RGB(177, 185, 249), // selection cursor ❯, links, focused option
38
+ blue: RGB(120, 160, 250), // selected palette row
39
+ green: RGB(78, 186, 101), // checkmarks, success, diff additions
40
+ red: RGB(220, 90, 90), // diff removals, errors
41
+ gray: RGB(153, 153, 153), // secondary text, menu numbers, hints
42
+ dim: RGB(80, 80, 80), // ╌ divider lines
43
+ white: RGB(255, 255, 255), // primary text, spinner glyphs
44
+ black: RGB(30, 30, 30), // code-block background
45
+ darkBg: RGB(20, 20, 20), // theme card background
46
+ };
47
+
48
+ /** Light-theme approximations, true to the dark palette's hue mapping. */
49
+ const LIGHT = {
50
+ gold: RGB(180, 130, 0),
51
+ coral: RGB(190, 100, 70),
52
+ lavender: RGB(120, 130, 220),
53
+ blue: RGB(70, 110, 220),
54
+ green: RGB(60, 150, 80),
55
+ red: RGB(200, 60, 60),
56
+ gray: RGB(110, 110, 110),
57
+ dim: RGB(170, 170, 170),
58
+ white: RGB(40, 40, 40),
59
+ black: RGB(245, 245, 245),
60
+ darkBg: RGB(255, 255, 255),
61
+ };
62
+
63
+ /**
64
+ * Glyphs, copied from aegiscodex-dev's `GLYPH` including its platform passes.
65
+ * A few of these are missing from non-Linux terminal fonts (⎿ U+23BF, ⏸ U+23F8)
66
+ * or render as double-width emoji (✦ U+2726, ✻/✢/✽/✶), which breaks row
67
+ * alignment exactly like the welcome art — those platforms get single-width
68
+ * stand-ins instead.
69
+ */
70
+ const GLYPH = (() => {
71
+ const base = {
72
+ cursor: '❯', // menu selection marker, prompt prefix
73
+ check: '✔', // selected / completed
74
+ divider: '╌', // menu separators
75
+ bullet: '·', // inline separators
76
+ hint: '❯', // "Try ..." suggestion marker
77
+ pause: '⏸', // bottom status line (manual mode)
78
+ star: '✦', // own-session marker, decorations
79
+ leftarrow: '←', // hints on the status line
80
+ pointer: '▸', // tips list bullets
81
+ hook: '⎿', // inline command / tip rows (tool commands, usage hints)
82
+ block: '●', // streaming cursor / assistant answer marker
83
+ bloom: '✻', // "done" spinner glyph (✻ Brewed for 2s)
84
+ spin: ['✢', '·', '✻', '*', '✽', '✶'], // working spinner
85
+ ellipse: '…',
86
+ };
87
+ if (process.platform === 'win32') {
88
+ return {
89
+ ...base,
90
+ pause: '❚❚', // ⏸ U+23F8 missing from many Windows fonts
91
+ hook: '_|', // ⎿ U+23BF almost never present in Windows fonts
92
+ star: '*', // ✦ U+2726 renders wide/emoji
93
+ bloom: '*', // ✻ U+273B
94
+ spin: ['|', '/', '-', '\\', '*', '-'],
95
+ };
96
+ }
97
+ if (process.platform === 'darwin') {
98
+ return {
99
+ ...base,
100
+ pause: '❚❚', // ⏸ has an emoji presentation on Apple fonts
101
+ star: '*', // ✦ renders as a double-width emoji in Terminal.app
102
+ hook: '_|', // ⎿ U+23BF not in Apple monospace fonts
103
+ };
104
+ }
105
+ return base;
106
+ })();
107
+
108
+ /** Working-line verbs, verbatim from the capture-backed table. */
109
+ const VERBS = [
110
+ 'Incubating',
111
+ 'Tempering',
112
+ 'Determining',
113
+ 'Beboppin\'',
114
+ 'Julienning',
115
+ 'Inferring',
116
+ 'Simmering',
117
+ 'Twisting',
118
+ 'Fermenting',
119
+ 'Fiddle-faddling',
120
+ 'Orbiting',
121
+ 'Prestidigitating',
122
+ ];
123
+
124
+ /** Completion verbs: "✻ Churned for 3s" after a chat turn, "✻ Worked for 3s"
125
+ * after one that used tools. */
126
+ const DONE_VERBS = ['Churned', 'Worked'];
127
+
128
+ const THEMES = { dark: C, light: LIGHT };
129
+
130
+ /** The palette object for a context. Colours are always derived from one of the
131
+ * two theme objects — never typed inline. */
132
+ function themeOf(ctx) {
133
+ return ctx && ctx.light ? LIGHT : C;
134
+ }
135
+
136
+ module.exports = {
137
+ RGB,
138
+ BG,
139
+ RESET,
140
+ BOLD,
141
+ DIM,
142
+ ITALIC,
143
+ UNDER,
144
+ BOLD_OFF,
145
+ ITALIC_OFF,
146
+ UNDER_OFF,
147
+ RESET_FG,
148
+ RESET_BG,
149
+ C,
150
+ LIGHT,
151
+ THEMES,
152
+ GLYPH,
153
+ VERBS,
154
+ DONE_VERBS,
155
+ themeOf,
156
+ };