aegiscode 6.0.0 → 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 CHANGED
@@ -1,30 +1,50 @@
1
1
  'use strict';
2
2
 
3
3
  /**
4
- * Terminal plumbing: cell-accurate width maths, wrapping, and the ephemeral
5
- * "live" region the CLI redraws while a call is in flight.
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).
6
15
  *
7
16
  * Design note — this CLI is a LINEAR transcript, not a full-screen alt-buffer
8
17
  * TUI (which is what Claude Code and aegiscodex-dev are). Finished turns are
9
18
  * written once to scrollback, so output stays selectable, pipeable and
10
- * scrollback-searchable; only the one live status/spinner line is redrawn. That
11
- * is a deliberate product difference, and it is also why there is no alt-screen
12
- * or mouse handling here.
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.
13
26
  */
14
27
 
15
28
  const ANSI_RE = /\x1b\[[0-9;]*[A-Za-z]/g;
16
29
 
17
- /** Combining marks and variation selectors occupy no cell. */
18
- const RE_ZERO = /[\u0300-\u036f\ufe00-\ufe0f\u200b-\u200f]/;
19
- /** Wide (CJK/fullwidth/emoji) ranges occupy two cells. */
20
- const RE_WIDE =
21
- /[\u1100-\u115f\u2e80-\u303e\u3041-\u33ff\u3400-\u4dbf\u4e00-\u9fff\ua000-\ua4cf\uac00-\ud7a3\uf900-\ufaff\ufe30-\ufe6f\uff00-\uff60\uffe0-\uffe6]|[\u{1f300}-\u{1faff}]|[\u{1f000}-\u{1f2ff}]/u;
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);
22
40
 
23
41
  function stripAnsi(s) {
24
42
  return String(s).replace(ANSI_RE, '');
25
43
  }
26
44
 
27
- /** Display width of a string in terminal cells. */
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. */
28
48
  function w(s) {
29
49
  let n = 0;
30
50
  for (const ch of stripAnsi(s)) {
@@ -34,13 +54,14 @@ function w(s) {
34
54
  return n;
35
55
  }
36
56
 
37
- /** Truncate to `width` cells without cutting a wide/combining codepoint. */
57
+ /** Truncate a plain string to `width` cells without cutting a wide/combining
58
+ * codepoint. */
38
59
  function clip(s, width) {
39
60
  if (w(s) <= width) return s;
40
61
  let out = '';
41
62
  let used = 0;
42
- for (const ch of s) {
43
- const cw = RE_ZERO.test(ch) ? 0 : RE_WIDE.test(ch) ? 2 : 1;
63
+ for (const ch of String(s)) {
64
+ const cw = cwidth(ch);
44
65
  if (used + cw > width) break;
45
66
  out += ch;
46
67
  used += cw;
@@ -48,20 +69,74 @@ function clip(s, width) {
48
69
  return out;
49
70
  }
50
71
 
51
- /** Pad (or clip) to exactly `width` cells. */
72
+ /** Pad (or clip) a plain string to exactly `width` cells. */
52
73
  function pad(s, width) {
53
74
  const t = clip(s, width);
54
75
  return t + ' '.repeat(Math.max(0, width - w(t)));
55
76
  }
56
77
 
57
- /** Right-align inside `width`. */
78
+ /** Right-align a plain string inside `width`. */
58
79
  function padStart(s, width) {
59
80
  const t = clip(s, width);
60
81
  return ' '.repeat(Math.max(0, width - w(t))) + t;
61
82
  }
62
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
+
63
138
  /**
64
- * Word-wrap one logical line to `width` cells, preserving words and never
139
+ * Word-wrap one logical plain line to `width` cells, preserving words and never
65
140
  * breaking mid-word unless the word alone exceeds the width.
66
141
  */
67
142
  function wrapLine(line, width, indent = '') {
@@ -94,8 +169,7 @@ function wrapLine(line, width, indent = '') {
94
169
  return out;
95
170
  }
96
171
 
97
- /** Wrap a block of text (honours existing newlines), applying `indent` to
98
- * continuation lines only when `hang` is true. */
172
+ /** Wrap a block of plain text (honours existing newlines), applying `indent`. */
99
173
  function wrapBlock(text, width, indent = '', hang = false) {
100
174
  const lines = [];
101
175
  for (const raw of String(text).split('\n')) {
@@ -104,12 +178,12 @@ function wrapBlock(text, width, indent = '', hang = false) {
104
178
  continue;
105
179
  }
106
180
  const parts = wrapLine(raw, width - w(indent));
107
- parts.forEach((p, i) => lines.push(i === 0 || !hang ? indent + p : indent + p));
181
+ parts.forEach((p) => lines.push(indent + p));
108
182
  }
109
183
  return lines;
110
184
  }
111
185
 
112
- // --- cursor / screen control ------------------------------------------------
186
+ // ── cursor / screen control ─────────────────────────────────────────────────
113
187
 
114
188
  const ESC = {
115
189
  hideCursor: '\x1b[?25l',
@@ -122,6 +196,86 @@ const ESC = {
122
196
  col: (n) => `\x1b[${n}G`,
123
197
  };
124
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
+
125
279
  function termWidth(fallback = 80) {
126
280
  return Math.max(20, (process.stdout && process.stdout.columns) || fallback);
127
281
  }
@@ -164,6 +318,7 @@ class LiveRegion {
164
318
  }
165
319
 
166
320
  module.exports = {
321
+ // plain-string width helpers (kept for the string renderers + app/bin)
167
322
  w,
168
323
  clip,
169
324
  pad,
@@ -171,7 +326,27 @@ module.exports = {
171
326
  stripAnsi,
172
327
  wrapLine,
173
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)
174
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)
175
350
  LiveRegion,
176
351
  termWidth,
177
352
  };
package/src/theme.js CHANGED
@@ -1,13 +1,17 @@
1
1
  'use strict';
2
2
 
3
3
  /**
4
- * AEGIS terminal theme "Signal".
4
+ * Design tokensthe aegiscodex-dev design system, adopted wholesale.
5
5
  *
6
- * This CLI is deliberately NOT a Claude Code look-alike. The palette below is
7
- * the whole point of the file: violet/cyan on ink, not the warm gold/coral
8
- * scheme every Claude Code derivative ships. `test/cli-identity.test.mjs` pins
9
- * that divergence it fails if any of Claude Code's exact RGB triples, or its
10
- * prompt/spinner glyphs, reappear here.
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.)
11
15
  *
12
16
  * Zero dependencies: SGR escapes are hand-built, like the rest of this repo.
13
17
  */
@@ -26,76 +30,107 @@ const RESET_BG = '\x1b[49m';
26
30
  const RGB = (r, g, b) => `\x1b[38;2;${r};${g};${b}m`;
27
31
  const BG = (r, g, b) => `\x1b[48;2;${r};${g};${b}m`;
28
32
 
29
- /**
30
- * Dark theme (default). Every value is a truecolor triple; the palette leans
31
- * cool, and `ink`/`panel` are blue-black rather than neutral grey so the
32
- * transcript reads as a different product at a glance.
33
- */
34
- const SIGNAL = {
35
- plasma: [124, 92, 255], // brand accent — sigil, prompts, active states
36
- beam: [34, 211, 238], // secondary — structure, model ids, code
37
- pulse: [52, 211, 153], // success
38
- alert: [251, 113, 133], // warning
39
- fault: [255, 99, 99], // error
40
- muted: [148, 163, 184], // secondary text
41
- dim: [71, 85, 105], // rails, rules, disabled
42
- text: [226, 232, 240], // body
43
- ink: [10, 12, 18], // deepest background (status bar, inverse)
44
- panel: [22, 25, 34], // raised surface (banner box)
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
45
46
  };
46
47
 
47
- /** Light theme — same roles, daylight values. Never a Claude Code palette. */
48
- const SIGNAL_LIGHT = {
49
- plasma: [79, 55, 200],
50
- beam: [8, 122, 145],
51
- pulse: [5, 122, 85],
52
- alert: [190, 55, 90],
53
- fault: [185, 28, 28],
54
- muted: [71, 85, 105],
55
- dim: [148, 163, 184],
56
- text: [15, 23, 42],
57
- ink: [248, 250, 252],
58
- panel: [241, 245, 249],
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),
59
61
  };
60
62
 
61
63
  /**
62
- * Glyphs. Chosen to share no character with Claude Code's prompt (`❯`),
63
- * spinner (`✢ · * ✶`), block cursor (`●`), hook (`⎿`) or quote bar (`▎`).
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.
64
69
  */
65
- const GLYPH = {
66
- prompt: '»', // input prompt
67
- sigil: '', // AEGIS mark / assistant turns
68
- rail: '', // heavy vertical gutter, both transcript roles
69
- railEnd: '', // gutter corner where a turn's meta line attaches
70
- spend: '', // money readout
71
- ok: '',
72
- err: '',
73
- warn: '!',
74
- bullet: '', // U+2219, not Claude's U+00B7
75
- pointer: '', // selected row
76
- divider: '',
77
- rule: '',
78
- spin: ['', '▂', '▃', '▄', '▅', '▆', '▇', '█', '▇', '▆', '▅', '▄', '▃', '▂'],
79
- box: { tl: '', tr: '', bl: '', br: '', h: '', v: '' }, // heavy, not rounded
80
- };
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
+ })();
81
107
 
82
- /** Verbs for the working line — plain, no Claude Code whimsy. */
108
+ /** Working-line verbs, verbatim from the capture-backed table. */
83
109
  const VERBS = [
84
- 'Consulting',
85
- 'Routing',
86
- 'Pooling',
87
- 'Reasoning',
88
- 'Drafting',
89
- 'Checking',
90
- 'Settling',
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',
91
122
  ];
92
123
 
93
- const THEMES = { dark: SIGNAL, light: SIGNAL_LIGHT };
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 };
94
129
 
95
- /** The palette object for a context, per the rule that colours are always
96
- * derived from one of the two theme objects — never typed inline. */
130
+ /** The palette object for a context. Colours are always derived from one of the
131
+ * two theme objects — never typed inline. */
97
132
  function themeOf(ctx) {
98
- return ctx && ctx.light ? SIGNAL_LIGHT : SIGNAL;
133
+ return ctx && ctx.light ? LIGHT : C;
99
134
  }
100
135
 
101
136
  module.exports = {
@@ -111,10 +146,11 @@ module.exports = {
111
146
  UNDER_OFF,
112
147
  RESET_FG,
113
148
  RESET_BG,
114
- SIGNAL,
115
- SIGNAL_LIGHT,
149
+ C,
150
+ LIGHT,
116
151
  THEMES,
117
152
  GLYPH,
118
153
  VERBS,
154
+ DONE_VERBS,
119
155
  themeOf,
120
156
  };