@teammates/cli 0.1.0 → 0.2.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.
Files changed (76) hide show
  1. package/README.md +31 -22
  2. package/dist/adapter.d.ts +1 -1
  3. package/dist/adapter.js +68 -56
  4. package/dist/adapter.test.js +34 -21
  5. package/dist/adapters/cli-proxy.d.ts +11 -4
  6. package/dist/adapters/cli-proxy.js +176 -162
  7. package/dist/adapters/copilot.d.ts +50 -0
  8. package/dist/adapters/copilot.js +210 -0
  9. package/dist/adapters/echo.d.ts +2 -2
  10. package/dist/adapters/echo.js +2 -1
  11. package/dist/adapters/echo.test.js +4 -2
  12. package/dist/cli-utils.d.ts +21 -0
  13. package/dist/cli-utils.js +74 -0
  14. package/dist/cli-utils.test.d.ts +1 -0
  15. package/dist/cli-utils.test.js +179 -0
  16. package/dist/cli.js +3160 -961
  17. package/dist/compact.d.ts +39 -0
  18. package/dist/compact.js +269 -0
  19. package/dist/compact.test.d.ts +1 -0
  20. package/dist/compact.test.js +198 -0
  21. package/dist/console/ansi.d.ts +18 -0
  22. package/dist/console/ansi.js +20 -0
  23. package/dist/console/ansi.test.d.ts +1 -0
  24. package/dist/console/ansi.test.js +50 -0
  25. package/dist/console/dropdown.d.ts +23 -0
  26. package/dist/console/dropdown.js +63 -0
  27. package/dist/console/file-drop.d.ts +59 -0
  28. package/dist/console/file-drop.js +186 -0
  29. package/dist/console/file-drop.test.d.ts +1 -0
  30. package/dist/console/file-drop.test.js +145 -0
  31. package/dist/console/index.d.ts +22 -0
  32. package/dist/console/index.js +23 -0
  33. package/dist/console/interactive-readline.d.ts +65 -0
  34. package/dist/console/interactive-readline.js +132 -0
  35. package/dist/console/markdown-table.d.ts +17 -0
  36. package/dist/console/markdown-table.js +270 -0
  37. package/dist/console/markdown-table.test.d.ts +1 -0
  38. package/dist/console/markdown-table.test.js +130 -0
  39. package/dist/console/mutable-output.d.ts +21 -0
  40. package/dist/console/mutable-output.js +51 -0
  41. package/dist/console/paste-handler.d.ts +63 -0
  42. package/dist/console/paste-handler.js +177 -0
  43. package/dist/console/prompt-box.d.ts +55 -0
  44. package/dist/console/prompt-box.js +120 -0
  45. package/dist/console/prompt-input.d.ts +136 -0
  46. package/dist/console/prompt-input.js +618 -0
  47. package/dist/console/startup.d.ts +20 -0
  48. package/dist/console/startup.js +138 -0
  49. package/dist/console/startup.test.d.ts +1 -0
  50. package/dist/console/startup.test.js +41 -0
  51. package/dist/console/wordwheel.d.ts +75 -0
  52. package/dist/console/wordwheel.js +123 -0
  53. package/dist/dropdown.js +4 -21
  54. package/dist/index.d.ts +5 -5
  55. package/dist/index.js +3 -3
  56. package/dist/onboard.d.ts +24 -0
  57. package/dist/onboard.js +174 -11
  58. package/dist/orchestrator.d.ts +8 -11
  59. package/dist/orchestrator.js +33 -81
  60. package/dist/orchestrator.test.js +59 -79
  61. package/dist/registry.d.ts +1 -1
  62. package/dist/registry.js +56 -12
  63. package/dist/registry.test.js +57 -13
  64. package/dist/theme.d.ts +56 -0
  65. package/dist/theme.js +54 -0
  66. package/dist/types.d.ts +18 -13
  67. package/package.json +8 -3
  68. package/template/CROSS-TEAM.md +2 -2
  69. package/template/PROTOCOL.md +72 -15
  70. package/template/README.md +2 -2
  71. package/template/TEMPLATE.md +118 -15
  72. package/template/example/SOUL.md +2 -1
  73. package/template/example/WISDOM.md +9 -0
  74. package/dist/adapters/codex.d.ts +0 -50
  75. package/dist/adapters/codex.js +0 -213
  76. package/template/example/MEMORIES.md +0 -26
@@ -0,0 +1,618 @@
1
+ /**
2
+ * PromptInput — consolonia-based replacement for node:readline.
3
+ *
4
+ * Uses consolonia's InputProcessor for raw terminal input parsing
5
+ * (escape sequences, bracketed paste, mouse events) and renders
6
+ * the prompt line + fenced borders directly via ANSI escape codes.
7
+ *
8
+ * This is a scrolling REPL input component, NOT a full-screen TUI.
9
+ * Agent output scrolls normally above; only the input area is managed.
10
+ *
11
+ * Layout:
12
+ * ────────────────────────────────────────
13
+ * ❯ user input here|
14
+ * ────────────────────────────────────────
15
+ * (optional dropdown lines)
16
+ */
17
+ import { EventEmitter } from "node:events";
18
+ import { createInputProcessor, esc, visibleLength, } from "@teammates/consolonia";
19
+ // ── PromptInput ────────────────────────────────────────────────────
20
+ export class PromptInput extends EventEmitter {
21
+ _prompt;
22
+ _promptLen;
23
+ _borderChar;
24
+ _borderStyle;
25
+ _value = "";
26
+ _cursor = 0;
27
+ _history;
28
+ _historyIndex = -1;
29
+ _savedInput = "";
30
+ _active = false;
31
+ _dropdownLines = [];
32
+ _linesBelow = 0; // how many lines we drew below prompt
33
+ _processor;
34
+ _events;
35
+ _dataHandler = null;
36
+ _resizeHandler = null;
37
+ _wasRawMode = false;
38
+ _onUpDown;
39
+ _beforeSubmit;
40
+ _colorize;
41
+ _hint;
42
+ _resizeTimer = null;
43
+ _promptRows = 1; // screen rows occupied by prompt + value
44
+ _cursorRow = 0; // cursor's row within prompt (0-based)
45
+ _drawnCols = 0; // terminal width when we last drew the prompt area
46
+ _statusLine = null; // optional line above top border
47
+ constructor(options = {}) {
48
+ super();
49
+ this._prompt = options.prompt ?? "> ";
50
+ this._promptLen = visibleLength(this._prompt);
51
+ this._borderChar = options.borderChar ?? "─";
52
+ this._borderStyle = options.borderStyle ?? ((s) => `\x1b[2m${s}\x1b[0m`);
53
+ this._history = options.history ?? [];
54
+ this._onUpDown = options.onUpDown ?? null;
55
+ this._beforeSubmit = options.beforeSubmit ?? null;
56
+ this._colorize = options.colorize ?? null;
57
+ this._hint = options.hint ?? null;
58
+ const { processor, events } = createInputProcessor();
59
+ this._processor = processor;
60
+ this._events = events;
61
+ this._events.on("input", (ev) => this._handleInput(ev));
62
+ }
63
+ // ── Public API ─────────────────────────────────────────────────
64
+ /** Current line text. */
65
+ get line() {
66
+ return this._value;
67
+ }
68
+ /** Current cursor position. */
69
+ get cursor() {
70
+ return this._cursor;
71
+ }
72
+ /** Whether the input is active (accepting keystrokes). */
73
+ get active() {
74
+ return this._active;
75
+ }
76
+ /** Command history. */
77
+ get history() {
78
+ return this._history;
79
+ }
80
+ /** Set prompt text. */
81
+ set prompt(text) {
82
+ this._prompt = text;
83
+ this._promptLen = visibleLength(text);
84
+ }
85
+ get prompt() {
86
+ return this._prompt;
87
+ }
88
+ /** Set the line text and move cursor to end. */
89
+ setLine(text) {
90
+ this._value = text;
91
+ this._cursor = text.length;
92
+ if (this._active)
93
+ this._refresh();
94
+ }
95
+ /** Show dropdown content below the bottom border. */
96
+ setDropdown(lines) {
97
+ this._dropdownLines = lines;
98
+ if (this._active)
99
+ this._refresh();
100
+ }
101
+ /** Clear dropdown content. */
102
+ clearDropdown() {
103
+ if (this._dropdownLines.length > 0) {
104
+ this._dropdownLines = [];
105
+ if (this._active)
106
+ this._refresh();
107
+ }
108
+ }
109
+ /**
110
+ * Activate the prompt: enable raw mode, bracketed paste, draw UI.
111
+ * Call this to show the prompt and start accepting input.
112
+ */
113
+ activate() {
114
+ if (this._active)
115
+ return;
116
+ this._active = true;
117
+ // Set up raw mode + listeners on first activation
118
+ if (!this._dataHandler) {
119
+ // Enable raw mode
120
+ if (process.stdin.isTTY) {
121
+ this._wasRawMode = process.stdin.isRaw ?? false;
122
+ process.stdin.setRawMode(true);
123
+ }
124
+ // Hide system cursor — we render our own block cursor
125
+ process.stdout.write(esc.hideCursor);
126
+ // Enable bracketed paste
127
+ process.stdout.write(esc.bracketedPasteOn);
128
+ // Listen for raw data
129
+ this._dataHandler = (chunk) => {
130
+ this._processor.feed(chunk.toString("utf-8"));
131
+ };
132
+ process.stdin.on("data", this._dataHandler);
133
+ process.stdin.resume();
134
+ // Listen for resize
135
+ this._resizeHandler = () => this._onResize();
136
+ process.stdout.on("resize", this._resizeHandler);
137
+ }
138
+ // Draw the prompt area
139
+ this._drawStatusLine();
140
+ this._drawTopBorder();
141
+ this._drawPromptLine();
142
+ this._drawBelow();
143
+ }
144
+ /**
145
+ * Deactivate the prompt: stop drawing but keep raw mode and input
146
+ * handling active so Ctrl+C still works during dispatch.
147
+ */
148
+ deactivate() {
149
+ if (!this._active)
150
+ return;
151
+ this._active = false;
152
+ // Move to the last prompt row
153
+ const down = this._promptRows - 1 - this._cursorRow;
154
+ if (down > 0)
155
+ process.stdout.write(esc.moveDown(down));
156
+ // Erase bottom border + dropdown (starts on next line)
157
+ process.stdout.write(`\n\r${esc.eraseDown}`);
158
+ this._linesBelow = 0;
159
+ }
160
+ /**
161
+ * Erase the entire prompt area (top border + prompt + bottom border + dropdown)
162
+ * and deactivate. Cursor ends at the position where the top border was.
163
+ * Used when the prompt area should be replaced with other content (e.g. user message block).
164
+ */
165
+ deactivateAndErase() {
166
+ if (!this._active)
167
+ return;
168
+ this._active = false;
169
+ // Move up from cursor row to top border (+ status line if present)
170
+ const up = this._cursorRow + 1 + (this._statusLine ? 1 : 0);
171
+ process.stdout.write(`${esc.moveUp(up)}\r${esc.eraseDown}`);
172
+ this._linesBelow = 0;
173
+ }
174
+ /**
175
+ * Set a status line that renders above the top border.
176
+ * Pass null to clear it. The status line is re-rendered in place
177
+ * without redrawing the entire prompt — ideal for animation.
178
+ */
179
+ setStatus(text) {
180
+ const hadStatus = this._statusLine !== null;
181
+ this._statusLine = text;
182
+ if (!this._active)
183
+ return;
184
+ if (text === null && hadStatus) {
185
+ // Remove status line — move up to it, erase, redraw prompt area
186
+ const up = this._cursorRow + 1 + 1; // cursor → prompt start → top border → status
187
+ process.stdout.write(`${esc.moveUp(up)}\r${esc.eraseDown}`);
188
+ this._linesBelow = 0;
189
+ this._drawTopBorder();
190
+ this._drawPromptLine();
191
+ this._drawBelow();
192
+ }
193
+ else if (text !== null && !hadStatus) {
194
+ // Add status line — move up to top border, erase, draw status + prompt area
195
+ const up = this._cursorRow + 1; // cursor → prompt start → top border
196
+ process.stdout.write(`${esc.moveUp(up)}\r${esc.eraseDown}`);
197
+ this._linesBelow = 0;
198
+ this._drawStatusLine();
199
+ this._drawTopBorder();
200
+ this._drawPromptLine();
201
+ this._drawBelow();
202
+ }
203
+ else if (text !== null && hadStatus) {
204
+ // Update status line in place — move up to status line, overwrite, move back
205
+ const up = this._cursorRow + 1 + 1;
206
+ process.stdout.write(`${esc.moveUp(up)}\r${esc.eraseLine}${text}${esc.moveDown(up)}\r`);
207
+ }
208
+ }
209
+ /** Fully close the input, destroy processor, restore terminal. */
210
+ close() {
211
+ this._active = false;
212
+ // Cancel pending resize
213
+ if (this._resizeTimer) {
214
+ clearTimeout(this._resizeTimer);
215
+ this._resizeTimer = null;
216
+ }
217
+ // Remove stdin listener
218
+ if (this._dataHandler) {
219
+ process.stdin.removeListener("data", this._dataHandler);
220
+ this._dataHandler = null;
221
+ }
222
+ if (this._resizeHandler) {
223
+ process.stdout.removeListener("resize", this._resizeHandler);
224
+ this._resizeHandler = null;
225
+ }
226
+ // Restore system cursor and disable bracketed paste
227
+ process.stdout.write(esc.showCursor + esc.bracketedPasteOff);
228
+ // Restore raw mode
229
+ if (process.stdin.isTTY) {
230
+ process.stdin.setRawMode(this._wasRawMode);
231
+ }
232
+ this._processor.destroy();
233
+ this.emit("close");
234
+ }
235
+ // ── Input handling ─────────────────────────────────────────────
236
+ _handleInput(event) {
237
+ if (!this._active)
238
+ return;
239
+ if (event.type === "paste") {
240
+ this._handlePaste(event.event);
241
+ return;
242
+ }
243
+ if (event.type === "key") {
244
+ this._handleKey(event.event);
245
+ return;
246
+ }
247
+ }
248
+ _handlePaste(paste) {
249
+ const text = paste.text;
250
+ // Emit paste event so the CLI can handle multi-line paste specially
251
+ this.emit("paste", text);
252
+ }
253
+ _handleKey(key) {
254
+ // ── Ctrl+C → interrupt
255
+ if (key.key === "c" && key.ctrl) {
256
+ // Clear current line
257
+ if (this._value.length > 0) {
258
+ this._value = "";
259
+ this._cursor = 0;
260
+ this._refresh();
261
+ }
262
+ else {
263
+ // Empty line + Ctrl+C → close
264
+ this.close();
265
+ return;
266
+ }
267
+ return;
268
+ }
269
+ // ── Ctrl+D → close on empty line
270
+ if (key.key === "d" && key.ctrl) {
271
+ if (this._value.length === 0) {
272
+ this.close();
273
+ return;
274
+ }
275
+ // Non-empty: delete forward (like unix behavior)
276
+ if (this._cursor < this._value.length) {
277
+ this._value =
278
+ this._value.slice(0, this._cursor) +
279
+ this._value.slice(this._cursor + 1);
280
+ this._refresh();
281
+ }
282
+ return;
283
+ }
284
+ // ── Ctrl+L → clear screen, redraw
285
+ if (key.key === "l" && key.ctrl) {
286
+ process.stdout.write(esc.clearScreen + esc.moveTo(0, 0));
287
+ this._drawStatusLine();
288
+ this._drawTopBorder();
289
+ this._drawPromptLine();
290
+ this._drawBelow();
291
+ return;
292
+ }
293
+ // ── Enter → submit
294
+ if (key.key === "enter") {
295
+ // Allow consumer to modify the value (e.g. accept wordwheel selection)
296
+ const modified = this._beforeSubmit?.(this._value);
297
+ const val = modified ?? this._value;
298
+ // Add to history
299
+ if (val.length > 0 &&
300
+ (this._history.length === 0 ||
301
+ this._history[this._history.length - 1] !== val)) {
302
+ this._history.push(val);
303
+ }
304
+ this._historyIndex = -1;
305
+ this._savedInput = "";
306
+ this._value = "";
307
+ this._cursor = 0;
308
+ this.emit("line", val);
309
+ return;
310
+ }
311
+ // ── Escape → emit escape (for wordwheel cancellation)
312
+ if (key.key === "escape") {
313
+ this.emit("escape");
314
+ return;
315
+ }
316
+ // ── Tab → emit tab (for autocomplete)
317
+ if (key.key === "tab") {
318
+ this.emit("tab", key.shift);
319
+ return;
320
+ }
321
+ // ── Backspace
322
+ if (key.key === "backspace") {
323
+ if (key.ctrl) {
324
+ // Ctrl+Backspace: delete word backward
325
+ const newPos = this._wordBoundaryLeft(this._cursor);
326
+ this._value =
327
+ this._value.slice(0, newPos) + this._value.slice(this._cursor);
328
+ this._cursor = newPos;
329
+ }
330
+ else if (this._cursor > 0) {
331
+ this._value =
332
+ this._value.slice(0, this._cursor - 1) +
333
+ this._value.slice(this._cursor);
334
+ this._cursor--;
335
+ }
336
+ this._refresh();
337
+ this.emit("change", this._value, this._cursor);
338
+ return;
339
+ }
340
+ // ── Delete
341
+ if (key.key === "delete") {
342
+ if (this._cursor < this._value.length) {
343
+ this._value =
344
+ this._value.slice(0, this._cursor) +
345
+ this._value.slice(this._cursor + 1);
346
+ this._refresh();
347
+ this.emit("change", this._value, this._cursor);
348
+ }
349
+ return;
350
+ }
351
+ // ── Left / Right
352
+ if (key.key === "left") {
353
+ if (key.ctrl) {
354
+ this._cursor = this._wordBoundaryLeft(this._cursor);
355
+ }
356
+ else if (this._cursor > 0) {
357
+ this._cursor--;
358
+ }
359
+ this._refresh();
360
+ return;
361
+ }
362
+ if (key.key === "right") {
363
+ if (key.ctrl) {
364
+ this._cursor = this._wordBoundaryRight(this._cursor);
365
+ }
366
+ else if (this._cursor < this._value.length) {
367
+ this._cursor++;
368
+ }
369
+ this._refresh();
370
+ return;
371
+ }
372
+ // ── Home / End / Ctrl+A / Ctrl+E
373
+ if (key.key === "home" || (key.key === "a" && key.ctrl)) {
374
+ this._cursor = 0;
375
+ this._refresh();
376
+ return;
377
+ }
378
+ if (key.key === "end" || (key.key === "e" && key.ctrl)) {
379
+ this._cursor = this._value.length;
380
+ this._refresh();
381
+ return;
382
+ }
383
+ // ── Ctrl+U → kill backward
384
+ if (key.key === "u" && key.ctrl) {
385
+ this._value = this._value.slice(this._cursor);
386
+ this._cursor = 0;
387
+ this._refresh();
388
+ this.emit("change", this._value, this._cursor);
389
+ return;
390
+ }
391
+ // ── Ctrl+K → kill forward
392
+ if (key.key === "k" && key.ctrl) {
393
+ this._value = this._value.slice(0, this._cursor);
394
+ this._refresh();
395
+ this.emit("change", this._value, this._cursor);
396
+ return;
397
+ }
398
+ // ── Up → wordwheel intercept, then history back
399
+ if (key.key === "up") {
400
+ if (this._onUpDown?.("up"))
401
+ return;
402
+ this._historyBack();
403
+ return;
404
+ }
405
+ // ── Down → wordwheel intercept, then history forward
406
+ if (key.key === "down") {
407
+ if (this._onUpDown?.("down"))
408
+ return;
409
+ this._historyForward();
410
+ return;
411
+ }
412
+ // ── Printable characters
413
+ if (key.char.length > 0 && !key.ctrl && !key.alt) {
414
+ this._value =
415
+ this._value.slice(0, this._cursor) +
416
+ key.char +
417
+ this._value.slice(this._cursor);
418
+ this._cursor += key.char.length;
419
+ this._refresh();
420
+ this.emit("change", this._value, this._cursor);
421
+ return;
422
+ }
423
+ }
424
+ // ── History ────────────────────────────────────────────────────
425
+ _historyBack() {
426
+ if (this._history.length === 0)
427
+ return;
428
+ if (this._historyIndex === -1) {
429
+ this._savedInput = this._value;
430
+ this._historyIndex = this._history.length - 1;
431
+ }
432
+ else if (this._historyIndex > 0) {
433
+ this._historyIndex--;
434
+ }
435
+ this._value = this._history[this._historyIndex];
436
+ this._cursor = this._value.length;
437
+ this._refresh();
438
+ }
439
+ _historyForward() {
440
+ if (this._historyIndex < 0)
441
+ return;
442
+ if (this._historyIndex < this._history.length - 1) {
443
+ this._historyIndex++;
444
+ this._value = this._history[this._historyIndex];
445
+ }
446
+ else {
447
+ this._historyIndex = -1;
448
+ this._value = this._savedInput;
449
+ this._savedInput = "";
450
+ }
451
+ this._cursor = this._value.length;
452
+ this._refresh();
453
+ }
454
+ // ── Word boundaries ────────────────────────────────────────────
455
+ _wordBoundaryLeft(pos) {
456
+ if (pos <= 0)
457
+ return 0;
458
+ let i = pos - 1;
459
+ while (i > 0 && this._value[i] === " ")
460
+ i--;
461
+ while (i > 0 && this._value[i - 1] !== " ")
462
+ i--;
463
+ return i;
464
+ }
465
+ _wordBoundaryRight(pos) {
466
+ const len = this._value.length;
467
+ if (pos >= len)
468
+ return len;
469
+ let i = pos;
470
+ while (i < len && this._value[i] !== " ")
471
+ i++;
472
+ while (i < len && this._value[i] === " ")
473
+ i++;
474
+ return i;
475
+ }
476
+ // ── Rendering ──────────────────────────────────────────────────
477
+ _cols() {
478
+ return process.stdout.columns || 80;
479
+ }
480
+ _buildBorder() {
481
+ return this._borderStyle(this._borderChar.repeat(this._cols()));
482
+ }
483
+ _drawStatusLine() {
484
+ if (this._statusLine) {
485
+ process.stdout.write(`${this._statusLine}\n`);
486
+ }
487
+ }
488
+ _drawTopBorder() {
489
+ this._drawnCols = this._cols();
490
+ process.stdout.write(`${this._buildBorder()}\n`);
491
+ }
492
+ _drawPromptLine() {
493
+ const cols = this._cols();
494
+ // Build the display string with a block cursor inserted
495
+ const before = this._value.slice(0, this._cursor);
496
+ const charAtCursor = this._value[this._cursor] ?? " ";
497
+ const after = this._value.slice(this._cursor + 1);
498
+ // Colorize the parts separately
499
+ const colorBefore = this._colorize ? this._colorize(before) : before;
500
+ const colorAfter = this._colorize ? this._colorize(after) : after;
501
+ // Block cursor: inverted character
502
+ const blockCursor = `\x1b[7m${charAtCursor}\x1b[27m`;
503
+ // Dim hint text after the value (e.g. placeholder params)
504
+ const hintText = this._hint ? (this._hint(this._value) ?? "") : "";
505
+ const dimHint = hintText ? `\x1b[2m${hintText}\x1b[22m` : "";
506
+ const line = this._prompt + colorBefore + blockCursor + colorAfter + dimHint;
507
+ process.stdout.write(line);
508
+ // Calculate geometry — +1 for the cursor block char, + hint length
509
+ const totalChars = this._promptLen +
510
+ this._value.length +
511
+ (this._cursor >= this._value.length ? 1 : 0) +
512
+ hintText.length;
513
+ this._promptRows = totalChars <= cols ? 1 : Math.ceil(totalChars / cols);
514
+ const cursorCharPos = this._promptLen + this._cursor;
515
+ this._cursorRow = Math.floor(cursorCharPos / cols);
516
+ if (cursorCharPos > 0 && cursorCharPos % cols === 0) {
517
+ this._cursorRow = cursorCharPos / cols - 1;
518
+ }
519
+ // Terminal's actual cursor ends at the end of the written text.
520
+ // We need to move it to the end of the prompt row area for _drawBelow.
521
+ // Since system cursor is hidden, we just need _cursorRow to be correct
522
+ // for the move calculations in _drawBelow and _refresh.
523
+ // Move terminal cursor to the cursor position for _drawBelow math.
524
+ const endChars = totalChars;
525
+ let endRow;
526
+ if (endChars === 0) {
527
+ endRow = 0;
528
+ }
529
+ else if (endChars % cols === 0) {
530
+ endRow = endChars / cols;
531
+ }
532
+ else {
533
+ endRow = Math.floor(endChars / cols);
534
+ }
535
+ const rowDiff = endRow - this._cursorRow;
536
+ if (rowDiff > 0)
537
+ process.stdout.write(esc.moveUp(rowDiff));
538
+ else if (rowDiff < 0)
539
+ process.stdout.write(esc.moveDown(-rowDiff));
540
+ }
541
+ _drawBelow() {
542
+ const cols = this._cols();
543
+ // Move from cursor row to last prompt row
544
+ const moveToEnd = this._promptRows - 1 - this._cursorRow;
545
+ let buf = "";
546
+ if (moveToEnd > 0)
547
+ buf += esc.moveDown(moveToEnd);
548
+ // Bottom border
549
+ buf += `\n${this._buildBorder()}`;
550
+ let lines = 1;
551
+ // Dropdown lines
552
+ for (const line of this._dropdownLines) {
553
+ buf += `\n${line.slice(0, cols)}`;
554
+ lines++;
555
+ }
556
+ process.stdout.write(buf);
557
+ // Move back to cursor row (system cursor hidden, just need row math)
558
+ const moveBack = lines + moveToEnd;
559
+ if (moveBack > 0) {
560
+ process.stdout.write(`${esc.moveUp(moveBack)}\r`);
561
+ }
562
+ this._linesBelow = lines;
563
+ }
564
+ /** Full refresh of the prompt area. */
565
+ _refresh() {
566
+ if (!this._active)
567
+ return;
568
+ // Move to first prompt row and erase just the prompt line(s)
569
+ if (this._cursorRow > 0) {
570
+ process.stdout.write(esc.moveUp(this._cursorRow));
571
+ }
572
+ process.stdout.write(`\r${esc.eraseLine}`);
573
+ // Redraw only the prompt line — borders and dropdown are unchanged
574
+ this._drawPromptLine();
575
+ }
576
+ _onResize() {
577
+ if (!this._active)
578
+ return;
579
+ // Debounce: resize fires many times during window drag.
580
+ // Only redraw once it settles.
581
+ if (this._resizeTimer)
582
+ clearTimeout(this._resizeTimer);
583
+ this._resizeTimer = setTimeout(() => {
584
+ this._resizeTimer = null;
585
+ if (!this._active)
586
+ return;
587
+ // After resize, old content may have re-wrapped. Calculate how many
588
+ // screen rows each old element now occupies at the new terminal width.
589
+ const newCols = this._cols();
590
+ const oldCols = this._drawnCols || newCols;
591
+ // How many screen rows does a line of `len` chars occupy at `cols` width?
592
+ const rowsFor = (len, cols) => len <= 0 ? 0 : Math.ceil(len / cols);
593
+ // Old top border was oldCols chars, now wraps to:
594
+ const topBorderRows = rowsFor(oldCols, newCols);
595
+ // Old prompt was _promptLen + _value.length + hint chars:
596
+ const hintLen = this._hint ? (this._hint(this._value) ?? "").length : 0;
597
+ const promptChars = this._promptLen + this._value.length + hintLen;
598
+ const oldPromptRows = Math.max(1, rowsFor(promptChars, newCols));
599
+ // Old bottom border was also oldCols chars:
600
+ const _botBorderRows = rowsFor(oldCols, newCols);
601
+ // Cursor is currently on _cursorRow within the old prompt area.
602
+ // Total rows above cursor: topBorderRows + _cursorRow
603
+ // Total rows below cursor: (oldPromptRows - 1 - _cursorRow) + botBorderRows + dropdown
604
+ const rowsAbove = topBorderRows + this._cursorRow;
605
+ const _rowsBelowPrompt = oldPromptRows - 1 - this._cursorRow;
606
+ const _dropdownRows = Math.max(0, this._linesBelow - 1); // _linesBelow includes bottom border
607
+ // Move up to top of the entire prompt area (including status line), erase, redraw
608
+ const statusRows = this._statusLine ? 1 : 0;
609
+ process.stdout.write(`${esc.moveUp(rowsAbove + statusRows)}\r${esc.eraseDown}`);
610
+ this._linesBelow = 0;
611
+ // Redraw at new width
612
+ this._drawStatusLine();
613
+ this._drawTopBorder();
614
+ this._drawPromptLine();
615
+ this._drawBelow();
616
+ }, 80);
617
+ }
618
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Animated startup sequence for the teammates CLI.
3
+ *
4
+ * Phase 1: Reveals "teammates" letter by letter in block font, left-aligned.
5
+ * Phase 2: Replaces with compact "TM" block logo + stats panel to the right.
6
+ */
7
+ /** Build the two-line title from a word using the block font. */
8
+ export declare function buildTitle(word: string): [string, string];
9
+ export interface StartupInfo {
10
+ version: string;
11
+ adapterName: string;
12
+ teammateCount: number;
13
+ cwd: string;
14
+ recallInstalled: boolean;
15
+ teammates: {
16
+ name: string;
17
+ role: string;
18
+ }[];
19
+ }
20
+ export declare function playStartup(info: StartupInfo): Promise<void>;