nex-code 0.3.5 → 0.3.7

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/cli/spinner.js DELETED
@@ -1,371 +0,0 @@
1
- /**
2
- * cli/spinner.js — Terminal Spinner and Progress Components
3
- * Spinner, MultiProgress, TaskProgress classes for animated terminal output
4
- */
5
-
6
- const C = {
7
- reset: '\x1b[0m',
8
- bold: '\x1b[1m',
9
- dim: '\x1b[2m',
10
- white: '\x1b[37m',
11
- red: '\x1b[31m',
12
- green: '\x1b[32m',
13
- yellow: '\x1b[33m',
14
- blue: '\x1b[34m',
15
- magenta: '\x1b[35m',
16
- cyan: '\x1b[36m',
17
- gray: '\x1b[90m',
18
- bgRed: '\x1b[41m',
19
- bgGreen: '\x1b[42m',
20
- brightCyan: '\x1b[96m',
21
- brightMagenta: '\x1b[95m',
22
- brightBlue: '\x1b[94m',
23
- };
24
-
25
- const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
26
- const TASK_FRAMES = ['✽', '✦', '✧', '✦'];
27
-
28
- class Spinner {
29
- constructor(text = 'Thinking...') {
30
- this.text = text;
31
- this.frame = 0;
32
- this.interval = null;
33
- this.startTime = null;
34
- }
35
-
36
- _render() {
37
- if (this._stopped) return;
38
- const f = SPINNER_FRAMES[this.frame % SPINNER_FRAMES.length];
39
- let elapsed = '';
40
- if (this.startTime) {
41
- const totalSecs = Math.floor((Date.now() - this.startTime) / 1000);
42
- if (totalSecs >= 60) {
43
- const mins = Math.floor(totalSecs / 60);
44
- const secs = totalSecs % 60;
45
- elapsed = ` ${C.dim}${mins}m ${String(secs).padStart(2, '0')}s${C.reset}`;
46
- } else if (totalSecs >= 1) {
47
- elapsed = ` ${C.dim}${totalSecs}s${C.reset}`;
48
- }
49
- }
50
- process.stderr.write(`\x1b[2K\r${C.cyan}${f}${C.reset} ${C.dim}${this.text}${C.reset}${elapsed}`);
51
- this.frame++;
52
- }
53
-
54
- start() {
55
- this._stopped = false;
56
- this.startTime = Date.now();
57
- process.stderr.write('\x1b[?25l'); // hide cursor
58
- this._render(); // render first frame immediately
59
- this.interval = setInterval(() => this._render(), 80);
60
- }
61
-
62
- update(text) {
63
- this.text = text;
64
- }
65
-
66
- stop() {
67
- this._stopped = true;
68
- if (this.interval) {
69
- clearInterval(this.interval);
70
- this.interval = null;
71
- }
72
- // Single write: clear line + show cursor (avoids flicker)
73
- process.stderr.write('\x1b[2K\r\x1b[?25h');
74
- this.startTime = null;
75
- }
76
- }
77
-
78
- // ─── MultiProgress ────────────────────────────────────────────
79
- const MULTI_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
80
-
81
- class MultiProgress {
82
- /**
83
- * @param {string[]} labels - Labels for each parallel task
84
- */
85
- constructor(labels) {
86
- this.labels = labels;
87
- this.statuses = labels.map(() => 'running'); // 'running' | 'done' | 'error'
88
- this.frame = 0;
89
- this.interval = null;
90
- this.startTime = null;
91
- this.lineCount = labels.length;
92
- }
93
-
94
- _formatElapsed() {
95
- if (!this.startTime) return '';
96
- const totalSecs = Math.floor((Date.now() - this.startTime) / 1000);
97
- if (totalSecs < 1) return '';
98
- const mins = Math.floor(totalSecs / 60);
99
- const secs = totalSecs % 60;
100
- return mins > 0 ? `${mins}m ${String(secs).padStart(2, '0')}s` : `${secs}s`;
101
- }
102
-
103
- _render() {
104
- if (this._stopped) return;
105
- const f = MULTI_FRAMES[this.frame % MULTI_FRAMES.length];
106
- const elapsed = this._formatElapsed();
107
- const elapsedStr = elapsed ? ` ${C.dim}${elapsed}${C.reset}` : '';
108
- let buf = '';
109
-
110
- for (let i = 0; i < this.labels.length; i++) {
111
- let icon, color;
112
- switch (this.statuses[i]) {
113
- case 'done':
114
- icon = `${C.green}✓${C.reset}`;
115
- color = C.dim;
116
- break;
117
- case 'error':
118
- icon = `${C.red}✗${C.reset}`;
119
- color = C.dim;
120
- break;
121
- default:
122
- icon = `${C.cyan}${f}${C.reset}`;
123
- color = '';
124
- }
125
- // Show elapsed on last line only
126
- const suffix = (i === this.labels.length - 1) ? elapsedStr : '';
127
- buf += `\x1b[2K ${icon} ${color}${this.labels[i]}${C.reset}${suffix}\n`;
128
- }
129
-
130
- // Move cursor back up to start of our block
131
- if (this.lineCount > 0) {
132
- buf += `\x1b[${this.lineCount}A`;
133
- }
134
-
135
- process.stderr.write(buf);
136
- this.frame++;
137
- }
138
-
139
- start() {
140
- this._stopped = false;
141
- this.startTime = Date.now();
142
- // Single buffered write: hide cursor + reserve lines + move back up
143
- let buf = '\x1b[?25l';
144
- for (let i = 0; i < this.lineCount; i++) buf += '\n';
145
- if (this.lineCount > 0) buf += `\x1b[${this.lineCount}A`;
146
- process.stderr.write(buf);
147
- this._render();
148
- this.interval = setInterval(() => this._render(), 80);
149
- }
150
-
151
- /**
152
- * @param {number} index - Index of the task to update
153
- * @param {'running'|'done'|'error'} status
154
- */
155
- update(index, status) {
156
- if (index >= 0 && index < this.statuses.length) {
157
- this.statuses[index] = status;
158
- }
159
- }
160
-
161
- stop() {
162
- this._stopped = true;
163
- if (this.interval) {
164
- clearInterval(this.interval);
165
- this.interval = null;
166
- }
167
- // Final render to show final states
168
- this._renderFinal();
169
- process.stderr.write('\x1b[?25h'); // show cursor
170
- }
171
-
172
- _renderFinal() {
173
- const elapsed = this._formatElapsed();
174
- const elapsedStr = elapsed ? ` ${C.dim}${elapsed}${C.reset}` : '';
175
- let buf = '';
176
- for (let i = 0; i < this.labels.length; i++) {
177
- let icon;
178
- switch (this.statuses[i]) {
179
- case 'done':
180
- icon = `${C.green}✓${C.reset}`;
181
- break;
182
- case 'error':
183
- icon = `${C.red}✗${C.reset}`;
184
- break;
185
- default:
186
- icon = `${C.yellow}○${C.reset}`;
187
- }
188
- const suffix = (i === this.labels.length - 1) ? elapsedStr : '';
189
- buf += `\x1b[2K ${icon} ${C.dim}${this.labels[i]}${C.reset}${suffix}\n`;
190
- }
191
- process.stderr.write(buf);
192
- }
193
- }
194
-
195
- // ─── TaskProgress ────────────────────────────────────────────
196
- const TASK_ICONS = { done: '✔', in_progress: '◼', pending: '◻', failed: '✗' };
197
- const TASK_COLORS = { done: C.green, in_progress: C.cyan, pending: C.dim, failed: C.red };
198
-
199
- let _activeTaskProgress = null;
200
-
201
- class TaskProgress {
202
- /**
203
- * @param {string} name - Header label for the task list
204
- * @param {Array<{id: string, description: string, status: string}>} tasks
205
- */
206
- constructor(name, tasks) {
207
- this.name = name;
208
- this.tasks = tasks.map(t => ({ id: t.id, description: t.description, status: t.status || 'pending' }));
209
- this.frame = 0;
210
- this.interval = null;
211
- this.startTime = null;
212
- this.tokens = 0;
213
- this.lineCount = 1 + this.tasks.length; // header + task lines
214
- this._paused = false;
215
- }
216
-
217
- _formatElapsed() {
218
- if (!this.startTime) return '';
219
- const totalSecs = Math.floor((Date.now() - this.startTime) / 1000);
220
- if (totalSecs < 1) return '';
221
- const mins = Math.floor(totalSecs / 60);
222
- const secs = totalSecs % 60;
223
- return mins > 0 ? `${mins}m ${String(secs).padStart(2, '0')}s` : `${secs}s`;
224
- }
225
-
226
- _formatTokens() {
227
- if (this.tokens <= 0) return '';
228
- if (this.tokens >= 1000) return `${(this.tokens / 1000).toFixed(1)}k`;
229
- return String(this.tokens);
230
- }
231
-
232
- _render() {
233
- if (this._stopped) return;
234
- const f = TASK_FRAMES[this.frame % TASK_FRAMES.length];
235
- const elapsed = this._formatElapsed();
236
- const tokStr = this._formatTokens();
237
- const stats = [elapsed, tokStr ? `↓ ${tokStr} tokens` : ''].filter(Boolean).join(' · ');
238
- const statsStr = stats ? ` ${C.dim}(${stats})${C.reset}` : '';
239
-
240
- let buf = `\x1b[2K${C.cyan}${f}${C.reset} ${this.name}…${statsStr}\n`;
241
-
242
- for (let i = 0; i < this.tasks.length; i++) {
243
- const t = this.tasks[i];
244
- const connector = i === 0 ? '⎿' : ' ';
245
- const icon = TASK_ICONS[t.status] || TASK_ICONS.pending;
246
- const color = TASK_COLORS[t.status] || TASK_COLORS.pending;
247
- const desc = t.description.length > 55 ? t.description.substring(0, 52) + '...' : t.description;
248
- buf += `\x1b[2K ${C.dim}${connector}${C.reset} ${color}${icon}${C.reset} ${desc}\n`;
249
- }
250
-
251
- // Move cursor back up
252
- buf += `\x1b[${this.lineCount}A`;
253
- process.stderr.write(buf);
254
- this.frame++;
255
- }
256
-
257
- start() {
258
- this._stopped = false;
259
- this.startTime = Date.now();
260
- this._paused = false;
261
- // Single buffered write: hide cursor + reserve lines + move back up
262
- let buf = '\x1b[?25l';
263
- for (let i = 0; i < this.lineCount; i++) buf += '\n';
264
- buf += `\x1b[${this.lineCount}A`;
265
- process.stderr.write(buf);
266
- this._render();
267
- this.interval = setInterval(() => this._render(), 120);
268
- _activeTaskProgress = this;
269
- }
270
-
271
- stop() {
272
- this._stopped = true;
273
- if (this.interval) {
274
- clearInterval(this.interval);
275
- this.interval = null;
276
- }
277
- if (!this._paused) {
278
- this._renderFinal();
279
- }
280
- process.stderr.write('\x1b[?25h');
281
- this._paused = false;
282
- if (_activeTaskProgress === this) _activeTaskProgress = null;
283
- }
284
-
285
- /** Erase the block from stderr so text streaming can happen cleanly */
286
- pause() {
287
- if (this._paused) return;
288
- if (this.interval) {
289
- clearInterval(this.interval);
290
- this.interval = null;
291
- }
292
- // Single buffered write: clear all occupied lines + move back up
293
- let buf = '';
294
- for (let i = 0; i < this.lineCount; i++) buf += '\x1b[2K\n';
295
- buf += `\x1b[${this.lineCount}A`;
296
- process.stderr.write(buf);
297
- this._paused = true;
298
- }
299
-
300
- /** Re-reserve lines and restart animation after a pause */
301
- resume() {
302
- if (!this._paused) return;
303
- this._paused = false;
304
- // Single buffered write: hide cursor + reserve lines + move back up
305
- let buf = '\x1b[?25l';
306
- for (let i = 0; i < this.lineCount; i++) buf += '\n';
307
- buf += `\x1b[${this.lineCount}A`;
308
- process.stderr.write(buf);
309
- this._render();
310
- this.interval = setInterval(() => this._render(), 120);
311
- }
312
-
313
- /**
314
- * @param {string} id - Task ID
315
- * @param {string} status - 'pending' | 'in_progress' | 'done' | 'failed'
316
- */
317
- updateTask(id, status) {
318
- const t = this.tasks.find(task => task.id === id);
319
- if (t) t.status = status;
320
- }
321
-
322
- setStats({ tokens }) {
323
- if (tokens !== undefined) this.tokens = tokens;
324
- }
325
-
326
- isActive() {
327
- return this.interval !== null || this._paused;
328
- }
329
-
330
- _renderFinal() {
331
- const elapsed = this._formatElapsed();
332
- const done = this.tasks.filter(t => t.status === 'done').length;
333
- const failed = this.tasks.filter(t => t.status === 'failed').length;
334
- const total = this.tasks.length;
335
- const summary = failed > 0 ? `${done}/${total} done, ${failed} failed` : `${done}/${total} done`;
336
-
337
- let buf = `\x1b[2K${C.green}✔${C.reset} ${this.name} ${C.dim}(${elapsed} · ${summary})${C.reset}\n`;
338
- for (let i = 0; i < this.tasks.length; i++) {
339
- const t = this.tasks[i];
340
- const connector = i === 0 ? '⎿' : ' ';
341
- const icon = TASK_ICONS[t.status] || TASK_ICONS.pending;
342
- const color = TASK_COLORS[t.status] || TASK_COLORS.pending;
343
- const desc = t.description.length > 55 ? t.description.substring(0, 52) + '...' : t.description;
344
- buf += `\x1b[2K ${C.dim}${connector}${C.reset} ${color}${icon}${C.reset} ${C.dim}${desc}${C.reset}\n`;
345
- }
346
- process.stderr.write(buf);
347
- }
348
- }
349
-
350
- function setActiveTaskProgress(tp) {
351
- _activeTaskProgress = tp;
352
- }
353
-
354
- function getActiveTaskProgress() {
355
- return _activeTaskProgress;
356
- }
357
-
358
- /**
359
- * Restore terminal to a clean state (show cursor, clear spinner line).
360
- * Call this on SIGINT or unexpected exit to avoid broken terminal.
361
- */
362
- function cleanupTerminal() {
363
- if (_activeTaskProgress) {
364
- _activeTaskProgress.stop();
365
- _activeTaskProgress = null;
366
- }
367
- // Single write: show cursor + clear line (avoids flicker)
368
- process.stderr.write('\x1b[?25h\x1b[2K\r');
369
- }
370
-
371
- module.exports = { C, Spinner, MultiProgress, TaskProgress, setActiveTaskProgress, getActiveTaskProgress, cleanupTerminal };