@vierratale/ai 0.1.0-beta.1 → 0.1.0-beta.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.
@@ -0,0 +1,501 @@
1
+ import { createInterface } from 'readline';
2
+ import { Branding } from './branding.js';
3
+ import { columnWidth, centerPad } from './chatbox.js';
4
+
5
+ const C = Branding.colors;
6
+ const MAX_POPUP = 9;
7
+
8
+ const visible = (s) => String(s ?? '').replace(/\x1b\[[0-9;]*m/g, '').length;
9
+
10
+ function padRight(text, width) {
11
+ const t = String(text ?? '');
12
+ return t + ' '.repeat(Math.max(0, width - visible(t)));
13
+ }
14
+
15
+ /**
16
+ * Raw-mode bottom-anchored line editor. Draws a fixed input bar on the last
17
+ * terminal row and, when the draft starts with "/", a popup box listing
18
+ * matching slash commands above it. Falls back to plain line reading when
19
+ * stdin is not a TTY (piped input, tests).
20
+ */
21
+ export class LineEditor {
22
+ constructor({
23
+ prompt = '',
24
+ commands = [],
25
+ onLine = () => {},
26
+ onExit = () => {},
27
+ stream = process.stdin,
28
+ write = (s) => process.stdout.write(s),
29
+ dims = () => ({ columns: process.stdout.columns || 96, rows: process.stdout.rows || 24 }),
30
+ } = {}) {
31
+ this.prompt = prompt;
32
+ this.commands = commands;
33
+ this.onLine = onLine;
34
+ this.onExit = onExit;
35
+ this.stream = stream;
36
+ this.write = write;
37
+ this.dims = dims;
38
+
39
+ this.buffer = [];
40
+ this.cursor = 0;
41
+ this.viewOffset = 0;
42
+ this.history = [];
43
+ this.histIndex = -1;
44
+ this.popup = null; // { matches, selected } | null
45
+ this.questionText = null;
46
+ this.busy = false;
47
+ this._pending = null;
48
+ this._pendingResolve = null;
49
+ this.pendingLines = [];
50
+ this.started = false;
51
+ this._fallback = null;
52
+ this._lastLine = '';
53
+ }
54
+
55
+ _cols() {
56
+ return Math.max(20, this.dims().columns || 96);
57
+ }
58
+
59
+ _rows() {
60
+ return Math.max(8, this.dims().rows || 24);
61
+ }
62
+
63
+ get isTTY() {
64
+ return !!(this.stream && this.stream.isTTY);
65
+ }
66
+
67
+ // Rows the editor reserves at the bottom of the terminal.
68
+ reservedRows() {
69
+ if (!this.isTTY) return 0;
70
+ return this.popup ? this._popupH() : 1;
71
+ }
72
+
73
+ _popupH() {
74
+ const n = this.popup ? Math.min(this.popup.matches.length, MAX_POPUP) : 0;
75
+ const h = Math.max(1, n) + 2;
76
+ return Math.min(h, Math.max(2, this._rows() - 1));
77
+ }
78
+
79
+ _text() {
80
+ return this.buffer.join('');
81
+ }
82
+
83
+ start() {
84
+ if (this.started) return this;
85
+ this.started = true;
86
+ if (this.isTTY) {
87
+ this._raw = true;
88
+ this.stream.setRawMode(true);
89
+ this.stream.resume();
90
+ this.stream.on('data', (chunk) => this._feed(String(chunk)));
91
+ this._onResize = () => this.draw();
92
+ process.stdout.on('resize', this._onResize);
93
+ } else {
94
+ this._fallback = createInterface({ input: this.stream, output: process.stdout, terminal: false });
95
+ this._fallback.on('line', (line) => {
96
+ if (this._pendingResolve) {
97
+ const r = this._pendingResolve;
98
+ this._pendingResolve = null;
99
+ this._pending = null;
100
+ r(line);
101
+ } else if (line !== undefined) {
102
+ this.pendingLines.push(line);
103
+ }
104
+ });
105
+ this._fallback.on('close', () => {
106
+ this._pendingResolve?.(null);
107
+ this._pendingResolve = null;
108
+ this._pending = null;
109
+ });
110
+ process.stdout.write(this.prompt);
111
+ }
112
+ this.draw();
113
+ return this;
114
+ }
115
+
116
+ stop() {
117
+ if (this._raw) {
118
+ try {
119
+ this.stream.setRawMode(false);
120
+ } catch { /* noop */ }
121
+ this.stream.removeAllListeners('data');
122
+ this._raw = false;
123
+ }
124
+ if (this._onResize) {
125
+ process.stdout.removeListener('resize', this._onResize);
126
+ this._onResize = null;
127
+ }
128
+ this._fallback?.close();
129
+ this._fallback = null;
130
+ }
131
+
132
+ // Resolves any pending read with null and restores the terminal.
133
+ exit() {
134
+ if (this._pendingResolve) {
135
+ const r = this._pendingResolve;
136
+ this._pendingResolve = null;
137
+ this._pending = null;
138
+ r(null);
139
+ }
140
+ this.stop();
141
+ }
142
+
143
+ readLine() {
144
+ if (this.pendingLines.length) {
145
+ const line = this.pendingLines.shift();
146
+ this._recordHistory(line);
147
+ return Promise.resolve(line);
148
+ }
149
+ if (this._pending) return this._pending;
150
+ this._pending = new Promise((resolve) => {
151
+ this._pendingResolve = resolve;
152
+ });
153
+ return this._pending;
154
+ }
155
+
156
+ question(msg) {
157
+ this.questionText = String(msg ?? '');
158
+ this.buffer = [];
159
+ this.cursor = 0;
160
+ this.popup = null;
161
+ this.draw();
162
+ return this.readLine();
163
+ }
164
+
165
+ setBusy(flag) {
166
+ this.busy = !!flag;
167
+ }
168
+
169
+ // ----- internal state manipulation -----
170
+
171
+ _recordHistory(line) {
172
+ if (line && line !== this.history[this.history.length - 1]) {
173
+ this.history.push(line);
174
+ if (this.history.length > 200) this.history.shift();
175
+ }
176
+ this.histIndex = -1;
177
+ }
178
+
179
+ _resolve(line) {
180
+ this._recordHistory(line);
181
+ this.questionText = null;
182
+ this.buffer = [];
183
+ this.cursor = 0;
184
+ this.viewOffset = 0;
185
+ this.popup = null;
186
+ if (this._pendingResolve) {
187
+ const r = this._pendingResolve;
188
+ this._pendingResolve = null;
189
+ this._pending = null;
190
+ r(line);
191
+ } else if (line !== undefined) {
192
+ this.pendingLines.push(line);
193
+ }
194
+ }
195
+
196
+ _matches(text) {
197
+ const token = text.startsWith('/') ? text.slice(1) : '';
198
+ const base = token.trim().split(/ +/)[0] || '';
199
+ return this.commands.filter((c) => c.name.startsWith(base));
200
+ }
201
+
202
+ _syncPopup() {
203
+ const s = this._text();
204
+ if (!s.startsWith('/')) {
205
+ this.popup = null;
206
+ return;
207
+ }
208
+ const words = s.slice(1).split(/ +/);
209
+ const base = words[0] || '';
210
+ if (words.length >= 2 && this.commands.some((c) => c.name === base)) {
211
+ this.popup = null;
212
+ return;
213
+ }
214
+ const matches = this._matches(s);
215
+ const sel = this.popup ? Math.min(this.popup.selected, Math.max(0, matches.length - 1)) : -1;
216
+ this.popup = { matches, selected: matches.length ? sel : -1 };
217
+ }
218
+
219
+ _acceptPopup(submit) {
220
+ const { matches } = this.popup;
221
+ const m = matches.length ? matches[this.popup.selected >= 0 ? this.popup.selected : 0] : null;
222
+ if (!m) {
223
+ this.popup = null;
224
+ if (submit) this._resolve(this._text());
225
+ return;
226
+ }
227
+ if (m.args && submit) {
228
+ // Needs arguments: fill the command and keep editing.
229
+ this.buffer = `/${m.name} `.split('');
230
+ this.cursor = this.buffer.length;
231
+ this.viewOffset = 0;
232
+ this.popup = null;
233
+ } else {
234
+ this.popup = null;
235
+ this._resolve(`/${m.name}`);
236
+ }
237
+ }
238
+
239
+ _submit() {
240
+ if (this.busy) {
241
+ // Streaming: stash the line, deliver it right after the busy phase.
242
+ const line = this._text();
243
+ this._recordHistory(line);
244
+ this.buffer = [];
245
+ this.cursor = 0;
246
+ this.popup = null;
247
+ this.pendingLines.push(line);
248
+ return;
249
+ }
250
+ if (this.popup) {
251
+ this._acceptPopup(true);
252
+ return;
253
+ }
254
+ this._resolve(this._text());
255
+ }
256
+
257
+ // ----- key handling -----
258
+
259
+ _feed(chunk) {
260
+ const code = [...String(chunk)];
261
+ let i = 0;
262
+ while (i < code.length) {
263
+ const c = code[i].charCodeAt(0);
264
+ if (c === 27) {
265
+ const esc = this._escape(code, i);
266
+ i = esc.next;
267
+ this._key(esc.key);
268
+ } else if (c === 13 || c === 10) {
269
+ this._submit();
270
+ i++;
271
+ } else if (c === 3) {
272
+ this._ctrlC();
273
+ i++;
274
+ } else if (c === 4) {
275
+ this._ctrlD();
276
+ i++;
277
+ } else if (c === 9) {
278
+ this._tab();
279
+ i++;
280
+ } else if (c === 127) {
281
+ this._backspace();
282
+ i++;
283
+ } else if (c < 32) {
284
+ i++;
285
+ } else {
286
+ this._insert(code[i]);
287
+ i++;
288
+ }
289
+ }
290
+ this.draw();
291
+ }
292
+
293
+ _escape(code, i) {
294
+ if (code[i + 1] !== '[') return { next: i + 1, key: 'esc' };
295
+ let j = i + 2;
296
+ let param = '';
297
+ while (j < code.length && /[0-9;]/.test(code[j])) {
298
+ param += code[j];
299
+ j++;
300
+ }
301
+ const fin = code[j] || '';
302
+ const next = j + 1;
303
+ if (fin === 'A') return { next, key: 'up' };
304
+ if (fin === 'B') return { next, key: 'down' };
305
+ if (fin === 'C') return { next, key: 'right' };
306
+ if (fin === 'D') return { next, key: 'left' };
307
+ if (fin === 'H' || param === '1' || param === '7') return { next, key: 'home' };
308
+ if (fin === 'F' || param === '4' || param === '8') return { next, key: 'end' };
309
+ if (param === '3') return { next, key: 'delete' };
310
+ if (param === 'Z') return { next, key: 'shift-tab' };
311
+ if (param === '1;5' && fin === 'C') return { next, key: 'ctrl-right' };
312
+ if (param === '1;5' && fin === 'D') return { next, key: 'ctrl-left' };
313
+ return { next, key: 'ignore' };
314
+ }
315
+
316
+ _insert(ch) {
317
+ this.buffer.splice(this.cursor, 0, ch);
318
+ this.cursor++;
319
+ this._syncPopup();
320
+ }
321
+
322
+ _backspace() {
323
+ if (this.cursor > 0) {
324
+ this.buffer.splice(this.cursor - 1, 1);
325
+ this.cursor--;
326
+ this._syncPopup();
327
+ }
328
+ }
329
+
330
+ _tab() {
331
+ if (this.popup) {
332
+ const { matches } = this.popup;
333
+ if (matches.length) {
334
+ this.popup.selected = Math.min(matches.length - 1, this.popup.selected + 1);
335
+ }
336
+ return;
337
+ }
338
+ if (this._text().startsWith('/')) {
339
+ this._syncPopup();
340
+ if (this.popup && this.popup.matches.length) this.popup.selected = Math.min(1, this.popup.matches.length - 1);
341
+ return;
342
+ }
343
+ this.buffer.splice(this.cursor, 0, ' ', ' ');
344
+ this.cursor += 2;
345
+ }
346
+
347
+ _ctrlC() {
348
+ if (this.popup) {
349
+ this.popup = null;
350
+ } else {
351
+ this.onExit();
352
+ }
353
+ }
354
+
355
+ _ctrlD() {
356
+ if (!this._text()) this.onExit();
357
+ }
358
+
359
+ _key(key) {
360
+ switch (key) {
361
+ case 'up':
362
+ if (this.popup && this.popup.matches.length) {
363
+ this.popup.selected = Math.max(0, this.popup.selected - 1);
364
+ } else {
365
+ this._hist(-1);
366
+ }
367
+ break;
368
+ case 'down':
369
+ if (this.popup && this.popup.matches.length) {
370
+ this.popup.selected = Math.min(this.popup.matches.length - 1, this.popup.selected + 1);
371
+ } else {
372
+ this._hist(1);
373
+ }
374
+ break;
375
+ case 'shift-tab':
376
+ if (this.popup && this.popup.matches.length) {
377
+ this.popup.selected = Math.max(0, this.popup.selected - 1);
378
+ }
379
+ break;
380
+ case 'left':
381
+ this.cursor = Math.max(0, this.cursor - 1);
382
+ break;
383
+ case 'right':
384
+ this.cursor = Math.min(this.buffer.length, this.cursor + 1);
385
+ break;
386
+ case 'ctrl-left':
387
+ this._wordJump(-1);
388
+ break;
389
+ case 'ctrl-right':
390
+ this._wordJump(1);
391
+ break;
392
+ case 'home':
393
+ this.cursor = 0;
394
+ break;
395
+ case 'end':
396
+ this.cursor = this.buffer.length;
397
+ break;
398
+ case 'delete':
399
+ if (this.cursor < this.buffer.length) this.buffer.splice(this.cursor, 1);
400
+ this._syncPopup();
401
+ break;
402
+ case 'esc':
403
+ this.popup = null;
404
+ break;
405
+ default:
406
+ break;
407
+ }
408
+ }
409
+
410
+ _wordJump(dir) {
411
+ if (dir < 0) {
412
+ while (this.cursor > 0 && this.buffer[this.cursor - 1] === ' ') this.cursor--;
413
+ while (this.cursor > 0 && this.buffer[this.cursor - 1] !== ' ') this.cursor--;
414
+ } else {
415
+ while (this.cursor < this.buffer.length && this.buffer[this.cursor] === ' ') this.cursor++;
416
+ while (this.cursor < this.buffer.length && this.buffer[this.cursor] !== ' ') this.cursor++;
417
+ }
418
+ }
419
+
420
+ _hist(dir) {
421
+ if (!this.history.length) return;
422
+ if (this.histIndex === -1) {
423
+ this._lastLine = this._text();
424
+ this.histIndex = this.history.length;
425
+ }
426
+ this.histIndex = Math.min(this.history.length, Math.max(0, this.histIndex + dir));
427
+ if (this.histIndex === this.history.length) {
428
+ this.buffer = this._lastLine.split('');
429
+ } else {
430
+ this.buffer = this.history[this.histIndex].split('');
431
+ }
432
+ this.cursor = this.buffer.length;
433
+ }
434
+
435
+ // ----- drawing -----
436
+
437
+ _inputPrefix() {
438
+ if (this.questionText !== null) {
439
+ return ` ${C.warning}${this.questionText} (y/N)${C.reset} ${C.dim}${C.bold}${C.primary}${Branding.USER_PROMPT}${C.reset} ${C.dim}›${C.reset} `;
440
+ }
441
+ return `${this.prompt} `;
442
+ }
443
+
444
+ draw() {
445
+ if (!this.isTTY) return;
446
+ const R = this._rows();
447
+ const cols = this._cols();
448
+ const col = columnWidth(cols);
449
+ const pad = centerPad(col, cols);
450
+ const prevH = this._lastPopupH || 1;
451
+ const curH = this.popup ? this._popupH() : 1;
452
+ const clearTop = Math.max(1, R - Math.max(prevH, curH) + 1);
453
+ let out = '';
454
+ for (let r = clearTop; r <= R; r++) out += `\x1b[${r};1H\x1b[2K`;
455
+ this._lastPopupH = curH;
456
+
457
+ if (this.popup) {
458
+ const boxH = this._popupH();
459
+ const listRows = Math.min(this.popup.matches.length, MAX_POPUP);
460
+ const box = Math.max(8, col);
461
+ const inner = Math.max(1, box - 8);
462
+ const top = R - boxH + 1;
463
+
464
+ const head = ` ${C.primary}╭─${C.reset} ${C.bold}${C.accent}Commands${C.reset}${C.primary} `;
465
+ const fill = Math.max(0, box - visible(head) - 1);
466
+ out += `\x1b[${top};1H\x1b[2K${pad}${head}${'─'.repeat(fill)}╮${C.reset}`;
467
+
468
+ let li = top + 1;
469
+ for (let i = 0; i < listRows; i++) {
470
+ const m = this.popup.matches[i];
471
+ const selected = i === this.popup.selected;
472
+ const name = `${C.bold}${selected ? C.accent : C.primary}/${m.name}${C.reset}`;
473
+ const args = m.args ? ` ${C.dim}${m.args}${C.reset}` : '';
474
+ const desc = m.desc ? ` ${C.dim}${m.desc}${C.reset}` : '';
475
+ const body = ` ${' '.repeat(selected ? 0 : 3)}${selected ? '›' : '·'} ${name}${args}${desc}`;
476
+ out += `\x1b[${li};1H\x1b[2K${pad} ${C.primary}│${C.reset} ${selected ? C.accent + C.bold : ''}${padRight(body, inner)}${selected ? C.reset : ''} ${C.primary}│${C.reset}`;
477
+ li++;
478
+ }
479
+ if (listRows === 0) {
480
+ out += `\x1b[${li};1H\x1b[2K${pad} ${C.primary}│${C.reset} ${C.dim}${padRight(' (no matching commands)', inner)}${C.reset} ${C.primary}│${C.reset}`;
481
+ li++;
482
+ }
483
+ const hint = listRows ? '↑↓ Tab pick Enter run Esc close' : 'Esc close';
484
+ const foot = ` ${C.primary}╰${'─'.repeat(Math.max(0, box - 6))}╯${C.reset}`;
485
+ out += `\x1b[${li};1H\x1b[2K${pad}${foot}${C.dim} ${hint}${C.reset}`;
486
+ }
487
+
488
+ // Input bar on the last row.
489
+ const max = Math.max(1, cols - visible(this._inputPrefix()) - 1);
490
+ let off = this.viewOffset;
491
+ if (this.cursor < off) off = this.cursor;
492
+ if (this.cursor > off + max - 1) off = this.cursor - max + 1;
493
+ this.viewOffset = off;
494
+
495
+ const shown = this._text().slice(off, off + max);
496
+ out += `\x1b[${R};1H\x1b[2K${pad}${this._inputPrefix()}${shown}`;
497
+ const cursorCol = visible(pad + this._inputPrefix()) + (this.cursor - off) + 1;
498
+ out += `\x1b[${R};${Math.min(cursorCol, cols)}H`;
499
+ this.write(out);
500
+ }
501
+ }
@@ -3,37 +3,113 @@ import { Branding } from './branding.js';
3
3
  const C = Branding.colors;
4
4
 
5
5
  export const Terminal = {
6
+ divider(char = '─', length = 50) {
7
+ return `${C.dim}${char.repeat(length)}${C.reset}`;
8
+ },
9
+
6
10
  printUser(text) {
7
- console.log(`${C.bold}${C.primary}${Branding.USER_PROMPT} >${C.reset} ${text}`);
11
+ const time = new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
12
+ process.stdout.write('\r\x1b[K');
13
+ console.log();
14
+ console.log(` ${C.bold}${C.primary}You${C.reset} ${C.dim}${time}${C.reset}`);
15
+ console.log(` ${text}`);
16
+ console.log();
8
17
  },
9
18
 
10
19
  printAIStart() {
11
- process.stdout.write(`${C.bold}${C.accent}${Branding.AI_PROMPT} >${C.reset} `);
20
+ const time = new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
21
+ console.log();
22
+ console.log(` ${C.bold}${C.accent}${Branding.AI_PROMPT}${C.reset} ${C.dim}${time}${C.reset}`);
23
+ this.curLine = '';
24
+ this._printPrefix();
25
+ },
26
+
27
+ _printPrefix() {
28
+ process.stdout.write(` ${C.accent}│${C.reset} `);
29
+ },
30
+
31
+ beginThinking() {
32
+ process.stdout.write(`${C.dim}· · ·${C.reset}`);
33
+ },
34
+
35
+ endThinking() {
36
+ process.stdout.write('\r\x1b[2K');
37
+ this._printPrefix();
12
38
  },
13
39
 
14
40
  printAIChunk(text) {
41
+ this._thinking = this._thinking || false;
42
+ if (this._thinking) {
43
+ this.endThinking();
44
+ this._thinking = false;
45
+ }
46
+ this.curLine = (this.curLine || '') + text;
15
47
  process.stdout.write(text);
48
+ this._maybeWrap();
49
+ },
50
+
51
+ _maybeWrap() {
52
+ if (this.curLine && this.curLine.length > 100) {
53
+ const words = this.curLine.split(' ');
54
+ if (words.length > 1) {
55
+ const lastWord = words[words.length - 1];
56
+ const rest = this.curLine.slice(0, this.curLine.length - lastWord.length).trimEnd();
57
+ process.stdout.write(`\n${C.accent}│${C.reset} `);
58
+ this.curLine = lastWord;
59
+ }
60
+ }
16
61
  },
17
62
 
18
63
  printAIEnd() {
19
- console.log();
64
+ process.stdout.write('\n');
20
65
  console.log();
21
66
  },
22
67
 
23
68
  printError(msg) {
24
- console.error(`${C.error}Error: ${msg}${C.reset}`);
69
+ console.log(` ${C.error}✖${C.reset} ${msg}`);
25
70
  },
26
71
 
27
72
  printInfo(msg) {
28
- console.log(`${C.dim}${msg}${C.reset}`);
73
+ console.log(` ${C.dim}${msg}${C.reset}`);
29
74
  },
30
75
 
31
76
  printSuccess(msg) {
32
- console.log(`${C.success}${msg}${C.reset}`);
77
+ console.log(` ${C.success}✔${C.reset} ${msg}`);
33
78
  },
34
79
 
35
80
  printWarning(msg) {
36
- console.log(`${C.warning}${msg}${C.reset}`);
81
+ console.log(` ${C.warning}⚠${C.reset} ${msg}`);
82
+ },
83
+
84
+ printSearchResults(results) {
85
+ console.log(` ${C.warning}Web Search Results${C.reset}`);
86
+ results.forEach((r, i) => {
87
+ console.log(` ${C.warning}${i + 1}.${C.reset} ${C.bold}${r.title}${C.reset}`);
88
+ console.log(` ${C.dim}${r.url}${C.reset}`);
89
+ if (r.snippet) console.log(` ${r.snippet.slice(0, 120)}`);
90
+ });
91
+ },
92
+
93
+ async showProgress(steps, work) {
94
+ // Hidden real progress: the work() task runs silently while we paint a
95
+ // fake animated progress line so the install feels alive.
96
+ const frames = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
97
+ let i = 0;
98
+ const timer = setInterval(() => {
99
+ i++;
100
+ const stage = steps[Math.floor((i / 8) % steps.length)];
101
+ const bar = frames[i % frames.length];
102
+ const fill = '='.repeat(1 + (i % 18)) + '>' + ' '.repeat(18 - (i % 18));
103
+ process.stdout.write(`\r ${C.accent}${bar}${C.reset} ${C.dim}${stage}${C.reset} [${C.accent}${fill}${C.reset}]`);
104
+ }, 100);
105
+ try {
106
+ await work();
107
+ } finally {
108
+ clearInterval(timer);
109
+ process.stdout.write('\r\x1b[2K');
110
+ console.log(` ${C.success}✔${C.reset} ${C.dim}Ready${C.reset}`);
111
+ console.log();
112
+ }
37
113
  },
38
114
 
39
115
  clear() {
@@ -44,6 +120,6 @@ export const Terminal = {
44
120
  return text
45
121
  .replace(/\*\*(.*?)\*\*/g, `${C.bold}$1${C.reset}`)
46
122
  .replace(/`([^`]+)`/g, `${C.accent}\`$1\`${C.reset}`)
47
- .replace(/```(\w*)\n([\s\S]*?)```/g, `${C.dim}┌─$1─┐${C.reset}\n$2${C.dim}└─────┘${C.reset}`);
123
+ .replace(/```(\w*)\n([\s\S]*?)```/g, `${C.dim}┌─${C.reset}\n$2${C.dim}└─────${C.reset}`);
48
124
  },
49
125
  };