@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.
- package/README.md +31 -22
- package/dist/adapter.d.ts +1 -1
- package/dist/adapter.js +68 -56
- package/dist/adapter.test.js +34 -21
- package/dist/adapters/cli-proxy.d.ts +11 -4
- package/dist/adapters/cli-proxy.js +176 -162
- package/dist/adapters/copilot.d.ts +50 -0
- package/dist/adapters/copilot.js +210 -0
- package/dist/adapters/echo.d.ts +2 -2
- package/dist/adapters/echo.js +2 -1
- package/dist/adapters/echo.test.js +4 -2
- package/dist/cli-utils.d.ts +21 -0
- package/dist/cli-utils.js +74 -0
- package/dist/cli-utils.test.d.ts +1 -0
- package/dist/cli-utils.test.js +179 -0
- package/dist/cli.js +3160 -961
- package/dist/compact.d.ts +39 -0
- package/dist/compact.js +269 -0
- package/dist/compact.test.d.ts +1 -0
- package/dist/compact.test.js +198 -0
- package/dist/console/ansi.d.ts +18 -0
- package/dist/console/ansi.js +20 -0
- package/dist/console/ansi.test.d.ts +1 -0
- package/dist/console/ansi.test.js +50 -0
- package/dist/console/dropdown.d.ts +23 -0
- package/dist/console/dropdown.js +63 -0
- package/dist/console/file-drop.d.ts +59 -0
- package/dist/console/file-drop.js +186 -0
- package/dist/console/file-drop.test.d.ts +1 -0
- package/dist/console/file-drop.test.js +145 -0
- package/dist/console/index.d.ts +22 -0
- package/dist/console/index.js +23 -0
- package/dist/console/interactive-readline.d.ts +65 -0
- package/dist/console/interactive-readline.js +132 -0
- package/dist/console/markdown-table.d.ts +17 -0
- package/dist/console/markdown-table.js +270 -0
- package/dist/console/markdown-table.test.d.ts +1 -0
- package/dist/console/markdown-table.test.js +130 -0
- package/dist/console/mutable-output.d.ts +21 -0
- package/dist/console/mutable-output.js +51 -0
- package/dist/console/paste-handler.d.ts +63 -0
- package/dist/console/paste-handler.js +177 -0
- package/dist/console/prompt-box.d.ts +55 -0
- package/dist/console/prompt-box.js +120 -0
- package/dist/console/prompt-input.d.ts +136 -0
- package/dist/console/prompt-input.js +618 -0
- package/dist/console/startup.d.ts +20 -0
- package/dist/console/startup.js +138 -0
- package/dist/console/startup.test.d.ts +1 -0
- package/dist/console/startup.test.js +41 -0
- package/dist/console/wordwheel.d.ts +75 -0
- package/dist/console/wordwheel.js +123 -0
- package/dist/dropdown.js +4 -21
- package/dist/index.d.ts +5 -5
- package/dist/index.js +3 -3
- package/dist/onboard.d.ts +24 -0
- package/dist/onboard.js +174 -11
- package/dist/orchestrator.d.ts +8 -11
- package/dist/orchestrator.js +33 -81
- package/dist/orchestrator.test.js +59 -79
- package/dist/registry.d.ts +1 -1
- package/dist/registry.js +56 -12
- package/dist/registry.test.js +57 -13
- package/dist/theme.d.ts +56 -0
- package/dist/theme.js +54 -0
- package/dist/types.d.ts +18 -13
- package/package.json +8 -3
- package/template/CROSS-TEAM.md +2 -2
- package/template/PROTOCOL.md +72 -15
- package/template/README.md +2 -2
- package/template/TEMPLATE.md +118 -15
- package/template/example/SOUL.md +2 -1
- package/template/example/WISDOM.md +9 -0
- package/dist/adapters/codex.d.ts +0 -50
- package/dist/adapters/codex.js +0 -213
- 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>;
|