create-claude-workspace 1.1.123 → 1.1.124
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/dist/scripts/lib/tui.mjs +192 -401
- package/dist/scripts/lib/tui.spec.js +3 -3
- package/package.json +1 -1
package/dist/scripts/lib/tui.mjs
CHANGED
|
@@ -1,446 +1,263 @@
|
|
|
1
1
|
// ─── Terminal UI for autonomous loop ───
|
|
2
|
-
//
|
|
3
|
-
//
|
|
2
|
+
// Two modes:
|
|
3
|
+
// Default: beautiful colored line-by-line output (works everywhere)
|
|
4
|
+
// Interactive (--interactive): adds input prompt + keyboard controls (needs capable terminal)
|
|
4
5
|
import { appendFileSync } from 'node:fs';
|
|
5
|
-
import { createInterface } from 'node:readline';
|
|
6
6
|
// ─── ANSI ───
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
saveCursor: `${ESC}s`,
|
|
12
|
-
restoreCursor: `${ESC}u`,
|
|
13
|
-
hideCursor: `${ESC}?25l`,
|
|
14
|
-
showCursor: `${ESC}?25h`,
|
|
15
|
-
scrollRegion: (top, bottom) => `${ESC}${top};${bottom}r`,
|
|
16
|
-
moveTo: (row, col) => `${ESC}${row};${col}H`,
|
|
17
|
-
clearLine: `${ESC}2K`,
|
|
18
|
-
bold: `${ESC}1m`,
|
|
19
|
-
dim: `${ESC}2m`,
|
|
20
|
-
reset: `${ESC}0m`,
|
|
7
|
+
const a = {
|
|
8
|
+
reset: '\x1b[0m',
|
|
9
|
+
bold: '\x1b[1m',
|
|
10
|
+
dim: '\x1b[2m',
|
|
21
11
|
fg: {
|
|
22
|
-
|
|
23
|
-
blue:
|
|
24
|
-
gray:
|
|
25
|
-
brightYellow:
|
|
26
|
-
brightCyan:
|
|
27
|
-
},
|
|
28
|
-
bg: {
|
|
29
|
-
blue: `${ESC}44m`, cyan: `${ESC}46m`, gray: `${ESC}100m`,
|
|
30
|
-
darkGray: `${ESC}48;5;236m`, medGray: `${ESC}48;5;238m`,
|
|
12
|
+
red: '\x1b[31m', green: '\x1b[32m', yellow: '\x1b[33m',
|
|
13
|
+
blue: '\x1b[34m', magenta: '\x1b[35m', cyan: '\x1b[36m', white: '\x1b[37m',
|
|
14
|
+
gray: '\x1b[90m', brightRed: '\x1b[91m', brightGreen: '\x1b[92m',
|
|
15
|
+
brightYellow: '\x1b[93m', brightBlue: '\x1b[94m', brightMagenta: '\x1b[95m',
|
|
16
|
+
brightCyan: '\x1b[96m',
|
|
31
17
|
},
|
|
18
|
+
bg: { gray: '\x1b[48;5;236m' },
|
|
32
19
|
};
|
|
33
|
-
// ───
|
|
34
|
-
const
|
|
20
|
+
// ─── Agent colors ───
|
|
21
|
+
const AGENT_PALETTE = [a.fg.brightCyan, a.fg.brightMagenta, a.fg.brightGreen, a.fg.brightYellow, a.fg.brightBlue, a.fg.brightRed];
|
|
22
|
+
const agentColors = new Map();
|
|
23
|
+
let nextColor = 0;
|
|
24
|
+
function agentColor(name) {
|
|
25
|
+
if (!agentColors.has(name))
|
|
26
|
+
agentColors.set(name, AGENT_PALETTE[nextColor++ % AGENT_PALETTE.length]);
|
|
27
|
+
return agentColors.get(name);
|
|
28
|
+
}
|
|
29
|
+
// ─── Tool icons ───
|
|
30
|
+
const ICONS = {
|
|
35
31
|
Bash: '⚡', Read: '📖', Write: '✏️ ', Edit: '🔧', Glob: '🔍', Grep: '🔎',
|
|
36
32
|
Agent: '🤖', TodoRead: '📋', TodoWrite: '📝', WebSearch: '🌐', WebFetch: '🌐',
|
|
37
33
|
AskUserQuestion: '❓',
|
|
38
34
|
};
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
function
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
}
|
|
48
|
-
function ts() {
|
|
49
|
-
return new Date().toLocaleTimeString('en-GB', { hour12: false });
|
|
50
|
-
}
|
|
51
|
-
function truncate(s, max) {
|
|
52
|
-
const clean = s.replace(/\n/g, ' ').trim();
|
|
53
|
-
return clean.length > max ? clean.slice(0, max) + '…' : clean;
|
|
54
|
-
}
|
|
55
|
-
function bar(pct, width) {
|
|
56
|
-
const filled = Math.round((pct / 100) * width);
|
|
57
|
-
return `${ansi.fg.green}${'█'.repeat(filled)}${ansi.fg.gray}${'░'.repeat(width - filled)}${ansi.reset}`;
|
|
58
|
-
}
|
|
59
|
-
function fmtTokens(n) {
|
|
60
|
-
if (n < 1000)
|
|
61
|
-
return `${n}`;
|
|
62
|
-
if (n < 1e6)
|
|
63
|
-
return `${(n / 1000).toFixed(1)}K`;
|
|
64
|
-
return `${(n / 1e6).toFixed(2)}M`;
|
|
65
|
-
}
|
|
66
|
-
function fmtDuration(ms) {
|
|
67
|
-
if (ms < 1000)
|
|
68
|
-
return `${ms}ms`;
|
|
69
|
-
if (ms < 60_000)
|
|
70
|
-
return `${(ms / 1000).toFixed(1)}s`;
|
|
71
|
-
if (ms < 3600_000)
|
|
72
|
-
return `${(ms / 60_000).toFixed(1)}m`;
|
|
73
|
-
return `${(ms / 3600_000).toFixed(1)}h`;
|
|
74
|
-
}
|
|
75
|
-
function stripAnsi(s) {
|
|
76
|
-
return s.replace(/\x1b\[[0-9;]*m/g, '');
|
|
77
|
-
}
|
|
35
|
+
// ─── Helpers ───
|
|
36
|
+
function ts() { return new Date().toLocaleTimeString('en-GB', { hour12: false }); }
|
|
37
|
+
function strip(s) { return s.replace(/\x1b\[[0-9;]*m/g, ''); }
|
|
38
|
+
function trunc(s, n) { const c = s.replace(/\n/g, ' ').trim(); return c.length > n ? c.slice(0, n) + '…' : c; }
|
|
39
|
+
function fmtTok(n) { return n < 1e3 ? `${n}` : n < 1e6 ? `${(n / 1e3).toFixed(1)}K` : `${(n / 1e6).toFixed(2)}M`; }
|
|
40
|
+
function fmtDur(ms) { return ms < 1e3 ? `${ms}ms` : ms < 6e4 ? `${(ms / 1e3).toFixed(1)}s` : `${(ms / 6e4).toFixed(1)}m`; }
|
|
41
|
+
function bar(pct, w) { const f = Math.round((pct / 100) * w); return `${a.fg.green}${'█'.repeat(f)}${a.fg.gray}${'░'.repeat(w - f)}${a.reset}`; }
|
|
42
|
+
// ─── TUI ───
|
|
78
43
|
export class TUI {
|
|
79
44
|
logFile;
|
|
80
45
|
interactive;
|
|
81
|
-
rl = null;
|
|
82
46
|
onInput = null;
|
|
83
47
|
onHotkey = null;
|
|
84
|
-
|
|
85
|
-
//
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
taskName = '';
|
|
96
|
-
tasksDone = 0;
|
|
97
|
-
tasksTotal = 0;
|
|
98
|
-
toolCalls = 0;
|
|
99
|
-
tokens = { input: 0, output: 0 };
|
|
100
|
-
iterStart = 0;
|
|
101
|
-
agentStack = [];
|
|
102
|
-
inputBuffer = '';
|
|
48
|
+
paused_ = false;
|
|
49
|
+
// Stats
|
|
50
|
+
iteration_ = 0;
|
|
51
|
+
maxIter = 0;
|
|
52
|
+
taskName_ = '';
|
|
53
|
+
tasksDone_ = 0;
|
|
54
|
+
tasksTotal_ = 0;
|
|
55
|
+
tools = 0;
|
|
56
|
+
tokens_ = { input: 0, output: 0 };
|
|
57
|
+
iterStart_ = 0;
|
|
58
|
+
agents = [];
|
|
103
59
|
constructor(logFile, interactive = false) {
|
|
104
60
|
this.logFile = logFile;
|
|
105
|
-
this.interactive = interactive && process.
|
|
106
|
-
if (this.interactive)
|
|
107
|
-
this.
|
|
108
|
-
}
|
|
61
|
+
this.interactive = interactive && process.stdin.isTTY === true;
|
|
62
|
+
if (this.interactive)
|
|
63
|
+
this.setupInput();
|
|
109
64
|
}
|
|
110
|
-
// ───
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
process.
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
});
|
|
128
|
-
// Readline for input
|
|
129
|
-
this.rl = createInterface({ input: process.stdin, output: process.stdout, terminal: false });
|
|
130
|
-
this.rl.on('line', (line) => {
|
|
131
|
-
if (this.onInput && line.trim()) {
|
|
132
|
-
this.onInput(line.trim());
|
|
65
|
+
// ─── Input handling (interactive only) ───
|
|
66
|
+
setupInput() {
|
|
67
|
+
if (!process.stdin.isTTY)
|
|
68
|
+
return;
|
|
69
|
+
process.stdin.setRawMode(true);
|
|
70
|
+
process.stdin.resume();
|
|
71
|
+
let buf = '';
|
|
72
|
+
process.stdin.on('data', (data) => {
|
|
73
|
+
const key = data.toString();
|
|
74
|
+
if (key === '\x03') {
|
|
75
|
+
this.onHotkey?.('quit');
|
|
76
|
+
return;
|
|
77
|
+
} // Ctrl+C
|
|
78
|
+
if (key === '\x1a') { // Ctrl+Z
|
|
79
|
+
this.paused_ = !this.paused_;
|
|
80
|
+
this.onHotkey?.(this.paused_ ? 'pause' : 'resume');
|
|
81
|
+
return;
|
|
133
82
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
// Ctrl+C → quit
|
|
142
|
-
if (key === '\x03') {
|
|
143
|
-
this.onHotkey?.('quit');
|
|
144
|
-
this.destroy();
|
|
145
|
-
process.emit('SIGINT');
|
|
146
|
-
return;
|
|
147
|
-
}
|
|
148
|
-
// Ctrl+Z → pause/resume toggle
|
|
149
|
-
if (key === '\x1a') {
|
|
150
|
-
this.paused = !this.paused;
|
|
151
|
-
this.onHotkey?.(this.paused ? 'pause' : 'resume');
|
|
152
|
-
this.renderFooter();
|
|
153
|
-
return;
|
|
83
|
+
if (key === '\x13') {
|
|
84
|
+
this.onHotkey?.('stop');
|
|
85
|
+
return;
|
|
86
|
+
} // Ctrl+S
|
|
87
|
+
if (key === '\r' || key === '\n') {
|
|
88
|
+
if (buf.trim()) {
|
|
89
|
+
this.onInput?.(buf.trim());
|
|
154
90
|
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
return;
|
|
159
|
-
}
|
|
160
|
-
// Enter
|
|
161
|
-
if (key === '\r' || key === '\n') {
|
|
162
|
-
if (this.inputBuffer.trim() && this.onInput) {
|
|
163
|
-
this.onInput(this.inputBuffer.trim());
|
|
164
|
-
}
|
|
165
|
-
this.inputBuffer = '';
|
|
166
|
-
this.renderInputLine();
|
|
167
|
-
return;
|
|
168
|
-
}
|
|
169
|
-
// Backspace
|
|
170
|
-
if (key === '\x7f' || key === '\b') {
|
|
171
|
-
this.inputBuffer = this.inputBuffer.slice(0, -1);
|
|
172
|
-
this.renderInputLine();
|
|
173
|
-
return;
|
|
174
|
-
}
|
|
175
|
-
// Regular character
|
|
176
|
-
if (key.length === 1 && key >= ' ') {
|
|
177
|
-
this.inputBuffer += key;
|
|
178
|
-
this.renderInputLine();
|
|
179
|
-
}
|
|
180
|
-
});
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
destroy() {
|
|
184
|
-
if (this.interactive) {
|
|
185
|
-
process.stdout.write(ansi.scrollRegion(1, this.rows));
|
|
186
|
-
process.stdout.write(ansi.moveTo(this.rows, 1));
|
|
187
|
-
process.stdout.write(ansi.showCursor);
|
|
188
|
-
process.stdout.write('\n');
|
|
189
|
-
this.rl?.close();
|
|
190
|
-
if (process.stdin.isTTY) {
|
|
191
|
-
process.stdin.setRawMode(false);
|
|
91
|
+
buf = '';
|
|
92
|
+
this.printInputPrompt('');
|
|
93
|
+
return;
|
|
192
94
|
}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
}
|
|
204
|
-
// ─── Layout rendering ───
|
|
205
|
-
setScrollRegion() {
|
|
206
|
-
const scrollTop = this.headerRows + 1;
|
|
207
|
-
const scrollBottom = this.rows - this.footerRows;
|
|
208
|
-
process.stdout.write(ansi.scrollRegion(scrollTop, scrollBottom));
|
|
209
|
-
// Move cursor to scroll area
|
|
210
|
-
process.stdout.write(ansi.moveTo(scrollBottom, 1));
|
|
95
|
+
if (key === '\x7f' || key === '\b') {
|
|
96
|
+
buf = buf.slice(0, -1);
|
|
97
|
+
this.printInputPrompt(buf);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (key.length === 1 && key >= ' ') {
|
|
101
|
+
buf += key;
|
|
102
|
+
this.printInputPrompt(buf);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
211
105
|
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
const taskPct = this.tasksTotal > 0 ? Math.round((this.tasksDone / this.tasksTotal) * 100) : 0;
|
|
216
|
-
// Line 1: Title bar
|
|
217
|
-
process.stdout.write(ansi.moveTo(1, 1) + ansi.clearLine);
|
|
218
|
-
const title = ` Claude Starter Kit — Autonomous Loop `;
|
|
219
|
-
const pad = Math.max(0, this.cols - stripAnsi(title).length);
|
|
220
|
-
process.stdout.write(`${ansi.bg.darkGray}${ansi.fg.white}${ansi.bold}${title}${' '.repeat(pad)}${ansi.reset}`);
|
|
221
|
-
// Line 2: Progress
|
|
222
|
-
process.stdout.write(ansi.moveTo(2, 1) + ansi.clearLine);
|
|
223
|
-
const iterInfo = ` Iter ${this.iteration}/${this.maxIterations} ${bar(pct, 15)} ${pct}%`;
|
|
224
|
-
const taskInfo = this.tasksTotal > 0 ? ` Tasks ${this.tasksDone}/${this.tasksTotal} ${bar(taskPct, 10)} ${taskPct}%` : '';
|
|
225
|
-
const taskLabel = this.taskName ? ` ${ansi.fg.cyan}${truncate(this.taskName, 40)}${ansi.reset}` : '';
|
|
226
|
-
process.stdout.write(`${ansi.bg.gray}${ansi.fg.white}${iterInfo}${taskInfo}${taskLabel}${' '.repeat(Math.max(0, this.cols - stripAnsi(iterInfo + taskInfo + taskLabel).length))}${ansi.reset}`);
|
|
227
|
-
// Line 3: Separator
|
|
228
|
-
process.stdout.write(ansi.moveTo(3, 1) + ansi.clearLine);
|
|
229
|
-
process.stdout.write(`${ansi.fg.gray}${'─'.repeat(this.cols)}${ansi.reset}`);
|
|
230
|
-
process.stdout.write(ansi.restoreCursor);
|
|
106
|
+
printInputPrompt(buf) {
|
|
107
|
+
const prompt = `${a.fg.gray} › ${a.fg.white}${buf}${a.reset}`;
|
|
108
|
+
process.stdout.write(`\r\x1b[2K${prompt}`);
|
|
231
109
|
}
|
|
232
|
-
|
|
233
|
-
process.
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
// Footer line 1: Separator
|
|
237
|
-
const footerTop = this.rows - this.footerRows + 1;
|
|
238
|
-
process.stdout.write(ansi.moveTo(footerTop, 1) + ansi.clearLine);
|
|
239
|
-
process.stdout.write(`${ansi.fg.gray}${'─'.repeat(this.cols)}${ansi.reset}`);
|
|
240
|
-
// Footer line 2: Stats + hotkeys
|
|
241
|
-
process.stdout.write(ansi.moveTo(footerTop + 1, 1) + ansi.clearLine);
|
|
242
|
-
const pauseLabel = this.paused ? `${ansi.fg.yellow}⏸ PAUSED${ansi.reset}` : '';
|
|
243
|
-
const stats = ` ${ansi.fg.gray}${elapsed}${ansi.reset} │ ${ansi.fg.cyan}${this.toolCalls} tools${ansi.reset} │ ${ansi.fg.yellow}${fmtTokens(totalTok)} tokens${ansi.reset}`;
|
|
244
|
-
const hotkeys = `${ansi.fg.gray}Ctrl+Z: ${this.paused ? 'resume' : 'pause'} │ Ctrl+S: stop │ Ctrl+C: quit${ansi.reset}`;
|
|
245
|
-
const middle = pauseLabel ? ` ${pauseLabel} │ ` : ' │ ';
|
|
246
|
-
const fullStats = stats + middle + hotkeys;
|
|
247
|
-
const pad = Math.max(0, this.cols - stripAnsi(fullStats).length);
|
|
248
|
-
process.stdout.write(`${ansi.bg.darkGray}${fullStats}${' '.repeat(pad)}${ansi.reset}`);
|
|
249
|
-
// Footer line 3: Input
|
|
250
|
-
process.stdout.write(ansi.moveTo(footerTop + 2, 1) + ansi.clearLine);
|
|
251
|
-
if (this.interactive) {
|
|
252
|
-
process.stdout.write(`${ansi.bg.medGray}${ansi.fg.white} › ${this.inputBuffer}${' '.repeat(Math.max(0, this.cols - this.inputBuffer.length - 3))}${ansi.reset}`);
|
|
253
|
-
}
|
|
254
|
-
else {
|
|
255
|
-
process.stdout.write(`${ansi.bg.darkGray}${ansi.fg.gray} Press --interactive to enable input${' '.repeat(Math.max(0, this.cols - 36))}${ansi.reset}`);
|
|
110
|
+
destroy() {
|
|
111
|
+
if (this.interactive && process.stdin.isTTY) {
|
|
112
|
+
process.stdin.setRawMode(false);
|
|
113
|
+
process.stdout.write('\n');
|
|
256
114
|
}
|
|
257
|
-
process.stdout.write(ansi.restoreCursor);
|
|
258
115
|
}
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
}
|
|
266
|
-
// ─── Log output ───
|
|
267
|
-
appendLog(formatted, raw) {
|
|
116
|
+
setInputHandler(handler) { this.onInput = handler; }
|
|
117
|
+
setHotkeyHandler(handler) { this.onHotkey = handler; }
|
|
118
|
+
isPaused() { return this.paused_; }
|
|
119
|
+
// ─── Output ───
|
|
120
|
+
out(formatted, raw) {
|
|
121
|
+
// Clear input line if interactive, then print, then restore prompt
|
|
268
122
|
if (this.interactive) {
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
process.stdout.write(ansi.moveTo(scrollBottom, 1));
|
|
273
|
-
process.stdout.write('\n' + formatted);
|
|
274
|
-
process.stdout.write(ansi.restoreCursor);
|
|
275
|
-
// Update footer (timer ticks)
|
|
276
|
-
this.renderFooter();
|
|
123
|
+
process.stdout.write('\r\x1b[2K');
|
|
124
|
+
console.log(formatted);
|
|
125
|
+
this.printInputPrompt('');
|
|
277
126
|
}
|
|
278
127
|
else {
|
|
279
128
|
console.log(formatted);
|
|
280
129
|
}
|
|
281
|
-
|
|
282
|
-
if (this.
|
|
283
|
-
|
|
130
|
+
const plain = raw || strip(formatted);
|
|
131
|
+
if (this.logFile) {
|
|
132
|
+
try {
|
|
133
|
+
appendFileSync(this.logFile, `[${new Date().toISOString()}] ${plain}\n`);
|
|
134
|
+
}
|
|
135
|
+
catch { /* */ }
|
|
284
136
|
}
|
|
285
|
-
|
|
137
|
+
}
|
|
138
|
+
fileOnly(msg) {
|
|
286
139
|
if (this.logFile) {
|
|
287
140
|
try {
|
|
288
|
-
appendFileSync(this.logFile, `[${new Date().toISOString()}] ${
|
|
141
|
+
appendFileSync(this.logFile, `[${new Date().toISOString()}] ${msg}\n`);
|
|
289
142
|
}
|
|
290
143
|
catch { /* */ }
|
|
291
144
|
}
|
|
292
145
|
}
|
|
293
|
-
// ─── Public
|
|
146
|
+
// ─── Public methods ───
|
|
294
147
|
banner() {
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
148
|
+
this.out('');
|
|
149
|
+
this.out(` ${a.fg.cyan}${a.bold}╔══════════════════════════════════════════════╗${a.reset}`);
|
|
150
|
+
this.out(` ${a.fg.cyan}${a.bold}║ ${a.fg.white}Claude Starter Kit — Autonomous Loop${a.fg.cyan} ║${a.reset}`);
|
|
151
|
+
this.out(` ${a.fg.cyan}${a.bold}╚══════════════════════════════════════════════╝${a.reset}`);
|
|
152
|
+
if (this.interactive) {
|
|
153
|
+
this.out(` ${a.fg.gray} Ctrl+Z pause/resume │ Ctrl+S stop │ Ctrl+C quit${a.reset}`);
|
|
301
154
|
}
|
|
302
|
-
|
|
303
|
-
}
|
|
304
|
-
info(msg) {
|
|
305
|
-
this.appendLog(` ${ansi.fg.gray}${ts()}${ansi.reset} ${ansi.fg.blue}ℹ${ansi.reset} ${msg}`, `INFO: ${stripAnsi(msg)}`);
|
|
306
|
-
}
|
|
307
|
-
warn(msg) {
|
|
308
|
-
this.appendLog(` ${ansi.fg.gray}${ts()}${ansi.reset} ${ansi.fg.yellow}⚠${ansi.reset} ${ansi.fg.yellow}${msg}${ansi.reset}`, `WARN: ${stripAnsi(msg)}`);
|
|
309
|
-
}
|
|
310
|
-
error(msg) {
|
|
311
|
-
this.appendLog(` ${ansi.fg.gray}${ts()}${ansi.reset} ${ansi.fg.red}✗${ansi.reset} ${ansi.fg.red}${msg}${ansi.reset}`, `ERROR: ${stripAnsi(msg)}`);
|
|
312
|
-
}
|
|
313
|
-
success(msg) {
|
|
314
|
-
this.appendLog(` ${ansi.fg.gray}${ts()}${ansi.reset} ${ansi.fg.green}✓${ansi.reset} ${ansi.fg.green}${msg}${ansi.reset}`, `OK: ${stripAnsi(msg)}`);
|
|
155
|
+
this.out('');
|
|
315
156
|
}
|
|
157
|
+
info(msg) { this.out(` ${a.fg.gray}${ts()}${a.reset} ${a.fg.blue}ℹ${a.reset} ${msg}`); }
|
|
158
|
+
warn(msg) { this.out(` ${a.fg.gray}${ts()}${a.reset} ${a.fg.yellow}⚠${a.reset} ${a.fg.yellow}${msg}${a.reset}`); }
|
|
159
|
+
error(msg) { this.out(` ${a.fg.gray}${ts()}${a.reset} ${a.fg.red}✗${a.reset} ${a.fg.red}${msg}${a.reset}`); }
|
|
160
|
+
success(msg) { this.out(` ${a.fg.gray}${ts()}${a.reset} ${a.fg.green}✓${a.reset} ${a.fg.green}${msg}${a.reset}`); }
|
|
316
161
|
setIteration(i, max) {
|
|
317
|
-
this.
|
|
318
|
-
this.
|
|
319
|
-
this.
|
|
320
|
-
this.
|
|
321
|
-
this.
|
|
322
|
-
this.
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
this.appendLog(`\n ${ansi.bold}${ansi.fg.white}─── Iteration ${i}/${max} ${bar(pct, 20)} ${pct}%${ansi.reset}\n`, `--- Iteration ${i}/${max} ---`);
|
|
162
|
+
this.iteration_ = i;
|
|
163
|
+
this.maxIter = max;
|
|
164
|
+
this.iterStart_ = Date.now();
|
|
165
|
+
this.tools = 0;
|
|
166
|
+
this.tokens_ = { input: 0, output: 0 };
|
|
167
|
+
this.agents = [];
|
|
168
|
+
const pct = Math.round((i / max) * 100);
|
|
169
|
+
this.out('');
|
|
170
|
+
this.out(` ${a.bold}${a.fg.white}━━━ Iteration ${i}/${max} ${bar(pct, 20)} ${pct}% ━━━${a.reset}`);
|
|
171
|
+
if (this.taskName_) {
|
|
172
|
+
const tPct = this.tasksTotal_ > 0 ? Math.round((this.tasksDone_ / this.tasksTotal_) * 100) : 0;
|
|
173
|
+
this.out(` ${a.fg.cyan}📋 ${this.taskName_}${a.reset} ${a.fg.gray}(${this.tasksDone_}/${this.tasksTotal_} tasks ${bar(tPct, 10)} ${tPct}%)${a.reset}`);
|
|
330
174
|
}
|
|
175
|
+
this.out('');
|
|
331
176
|
}
|
|
332
177
|
setTask(name, done, total) {
|
|
333
|
-
this.
|
|
334
|
-
this.
|
|
335
|
-
this.
|
|
336
|
-
if (this.interactive) {
|
|
337
|
-
this.renderHeader();
|
|
338
|
-
}
|
|
178
|
+
this.taskName_ = name;
|
|
179
|
+
this.tasksDone_ = done;
|
|
180
|
+
this.tasksTotal_ = total;
|
|
339
181
|
}
|
|
340
182
|
iterationEnd() {
|
|
341
|
-
const elapsed = Date.now() - this.
|
|
342
|
-
const
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
}
|
|
183
|
+
const elapsed = fmtDur(Date.now() - this.iterStart_);
|
|
184
|
+
const tok = fmtTok(this.tokens_.input + this.tokens_.output);
|
|
185
|
+
this.out('');
|
|
186
|
+
this.out(` ${a.fg.gray}━━━━ ${elapsed} │ ${this.tools} tools │ ${tok} tokens ━━━━${a.reset}`);
|
|
346
187
|
}
|
|
347
188
|
// ─── SDK message handler ───
|
|
348
189
|
handleMessage(message) {
|
|
349
190
|
switch (message.type) {
|
|
350
191
|
case 'assistant':
|
|
351
|
-
this.
|
|
192
|
+
this.onAssistant(message);
|
|
352
193
|
break;
|
|
353
194
|
case 'user':
|
|
354
|
-
this.
|
|
195
|
+
this.onToolResult(message);
|
|
355
196
|
break;
|
|
356
197
|
case 'system':
|
|
357
|
-
this.
|
|
198
|
+
this.onSystem(message);
|
|
358
199
|
break;
|
|
359
200
|
case 'result':
|
|
360
|
-
this.
|
|
201
|
+
this.onResult(message);
|
|
361
202
|
break;
|
|
362
|
-
// Skip: stream_event, tool_progress, auth_status, rate_limit_event, prompt_suggestion
|
|
363
203
|
}
|
|
364
204
|
}
|
|
365
|
-
|
|
205
|
+
onAssistant(msg) {
|
|
366
206
|
const content = msg.message?.content;
|
|
367
207
|
if (!Array.isArray(content))
|
|
368
208
|
return;
|
|
369
209
|
if (msg.message?.usage) {
|
|
370
|
-
this.
|
|
371
|
-
this.
|
|
372
|
-
if (this.interactive)
|
|
373
|
-
this.renderFooter();
|
|
210
|
+
this.tokens_.input += msg.message.usage.input_tokens || 0;
|
|
211
|
+
this.tokens_.output += msg.message.usage.output_tokens || 0;
|
|
374
212
|
}
|
|
375
213
|
for (const block of content) {
|
|
376
214
|
if (block.type === 'text' && block.text?.trim()) {
|
|
377
|
-
|
|
378
|
-
const indent = this.indent();
|
|
379
|
-
this.appendLog(`${indent}${ansi.fg.white}${text}${ansi.reset}`, `TEXT: ${stripAnsi(text)}`);
|
|
380
|
-
}
|
|
381
|
-
if (block.type === 'tool_use') {
|
|
382
|
-
this.formatToolUse(block);
|
|
215
|
+
this.out(`${this.indent()}${a.fg.white}${trunc(block.text, 300)}${a.reset}`, `TEXT: ${trunc(block.text, 300)}`);
|
|
383
216
|
}
|
|
217
|
+
if (block.type === 'tool_use')
|
|
218
|
+
this.onToolUse(block);
|
|
384
219
|
}
|
|
385
220
|
}
|
|
386
|
-
|
|
387
|
-
this.
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
const name = block.name || 'unknown';
|
|
391
|
-
const icon = TOOL_ICONS[name] || '⚙️';
|
|
221
|
+
onToolUse(block) {
|
|
222
|
+
this.tools++;
|
|
223
|
+
const name = block.name || '?';
|
|
224
|
+
const icon = ICONS[name] || '⚙️';
|
|
392
225
|
const input = block.input || {};
|
|
393
|
-
const
|
|
394
|
-
const time = `${
|
|
226
|
+
const pre = this.indent();
|
|
227
|
+
const time = `${a.fg.gray}${ts()}${a.reset}`;
|
|
395
228
|
// Agent delegation
|
|
396
229
|
if (name === 'Agent') {
|
|
397
|
-
const
|
|
398
|
-
const
|
|
399
|
-
const
|
|
400
|
-
const
|
|
401
|
-
this.
|
|
402
|
-
this.
|
|
230
|
+
const type = input.subagent_type || input.type || 'agent';
|
|
231
|
+
const model = input.model ? ` ${a.fg.gray}(${input.model})${a.reset}` : '';
|
|
232
|
+
const desc = trunc(input.description || input.prompt || '', 50);
|
|
233
|
+
const col = agentColor(type);
|
|
234
|
+
this.agents.push(type);
|
|
235
|
+
this.out(`${pre}${time} ${icon} ${col}${a.bold}${type}${a.reset}${model} ${a.fg.gray}${desc}${a.reset}`, `AGENT: ${type} ${input.model || ''} — ${desc}`);
|
|
403
236
|
return;
|
|
404
237
|
}
|
|
405
|
-
let detail
|
|
238
|
+
let detail;
|
|
406
239
|
switch (name) {
|
|
407
|
-
case 'Bash':
|
|
408
|
-
|
|
409
|
-
detail = `${ansi.fg.yellow}${cmd}${ansi.reset}`;
|
|
240
|
+
case 'Bash':
|
|
241
|
+
detail = `${a.fg.yellow}${trunc(input.command || '', 70)}${a.reset}`;
|
|
410
242
|
break;
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
const path = (input.file_path || '').replace(/^\/project\//, '');
|
|
414
|
-
detail = `${ansi.fg.cyan}${path}${ansi.reset}`;
|
|
243
|
+
case 'Read':
|
|
244
|
+
detail = `${a.fg.cyan}${(input.file_path || '').replace(/^\/project\//, '')}${a.reset}`;
|
|
415
245
|
break;
|
|
416
|
-
}
|
|
417
246
|
case 'Write':
|
|
418
|
-
case 'Edit':
|
|
419
|
-
|
|
420
|
-
detail = `${ansi.fg.cyan}${path}${ansi.reset}`;
|
|
247
|
+
case 'Edit':
|
|
248
|
+
detail = `${a.fg.cyan}${(input.file_path || '').replace(/^\/project\//, '')}${a.reset}`;
|
|
421
249
|
break;
|
|
422
|
-
}
|
|
423
250
|
case 'Glob':
|
|
424
|
-
detail = `${
|
|
425
|
-
break;
|
|
426
|
-
case 'Grep': {
|
|
427
|
-
const pat = input.pattern || '';
|
|
428
|
-
const path = input.path ? ` ${ansi.fg.gray}in ${input.path}${ansi.reset}` : '';
|
|
429
|
-
detail = `${ansi.fg.cyan}/${pat}/${ansi.reset}${path}`;
|
|
430
|
-
break;
|
|
431
|
-
}
|
|
432
|
-
case 'TodoWrite':
|
|
433
|
-
detail = `${ansi.fg.gray}updating tasks${ansi.reset}`;
|
|
251
|
+
detail = `${a.fg.cyan}${input.pattern || ''}${a.reset}`;
|
|
434
252
|
break;
|
|
435
|
-
case '
|
|
436
|
-
detail = `${
|
|
253
|
+
case 'Grep':
|
|
254
|
+
detail = `${a.fg.cyan}/${input.pattern || ''}/${a.reset}${input.path ? ` ${a.fg.gray}in ${input.path}` : ''}${a.reset}`;
|
|
437
255
|
break;
|
|
438
|
-
default:
|
|
439
|
-
detail = `${ansi.fg.gray}${truncate(JSON.stringify(input), 60)}${ansi.reset}`;
|
|
256
|
+
default: detail = `${a.fg.gray}${trunc(JSON.stringify(input), 60)}${a.reset}`;
|
|
440
257
|
}
|
|
441
|
-
this.
|
|
258
|
+
this.out(`${pre}${time} ${icon} ${a.bold}${name}${a.reset} ${detail}`, `TOOL: ${name} ${JSON.stringify(input).slice(0, 200)}`);
|
|
442
259
|
}
|
|
443
|
-
|
|
260
|
+
onToolResult(msg) {
|
|
444
261
|
const content = msg.message?.content;
|
|
445
262
|
if (!Array.isArray(content))
|
|
446
263
|
return;
|
|
@@ -450,60 +267,34 @@ export class TUI {
|
|
|
450
267
|
const output = String(block.content || '').trim();
|
|
451
268
|
if (!output)
|
|
452
269
|
continue;
|
|
453
|
-
const
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
const short = truncate(output, 100);
|
|
457
|
-
this.appendLog(`${indent} ${ansi.fg.red}✗ ${short}${ansi.reset}`, `ERROR: ${short}`);
|
|
270
|
+
const pre = this.indent();
|
|
271
|
+
if (block.is_error) {
|
|
272
|
+
this.out(`${pre} ${a.fg.red}✗ ${trunc(output, 100)}${a.reset}`, `ERROR: ${trunc(output, 100)}`);
|
|
458
273
|
}
|
|
459
274
|
else if (output.length < 150) {
|
|
460
|
-
|
|
461
|
-
this.appendLog(`${indent} ${ansi.fg.green}✓${ansi.reset} ${ansi.fg.gray}${short}${ansi.reset}`, `OK: ${short}`);
|
|
275
|
+
this.out(`${pre} ${a.fg.green}✓${a.reset} ${a.fg.gray}${trunc(output, 100)}${a.reset}`, `OK: ${trunc(output, 100)}`);
|
|
462
276
|
}
|
|
463
277
|
else {
|
|
464
|
-
const
|
|
465
|
-
this.
|
|
278
|
+
const n = output.split('\n').length;
|
|
279
|
+
this.out(`${pre} ${a.fg.green}✓${a.reset} ${a.fg.gray}${n} lines${a.reset}`, `OK: (${n} lines)`);
|
|
466
280
|
}
|
|
467
281
|
}
|
|
468
282
|
}
|
|
469
|
-
|
|
470
|
-
if (msg.subtype === 'task_progress') {
|
|
471
|
-
|
|
472
|
-
if (desc) {
|
|
473
|
-
const indent = this.indent();
|
|
474
|
-
this.appendLog(`${indent} ${ansi.dim}${truncate(desc, 70)}${ansi.reset}`, `PROGRESS: ${desc}`);
|
|
475
|
-
}
|
|
476
|
-
return;
|
|
283
|
+
onSystem(msg) {
|
|
284
|
+
if (msg.subtype === 'task_progress' && msg.description) {
|
|
285
|
+
this.out(`${this.indent()} ${a.dim}${trunc(msg.description, 70)}${a.reset}`, `PROGRESS: ${msg.description}`);
|
|
477
286
|
}
|
|
478
287
|
}
|
|
479
|
-
|
|
480
|
-
if (msg.session_id)
|
|
481
|
-
this.
|
|
482
|
-
}
|
|
288
|
+
onResult(msg) {
|
|
289
|
+
if (msg.session_id)
|
|
290
|
+
this.fileOnly(`SESSION: ${msg.session_id}`);
|
|
483
291
|
}
|
|
484
|
-
// ───
|
|
292
|
+
// ─── Indent ───
|
|
485
293
|
indent() {
|
|
486
|
-
|
|
487
|
-
if (depth === 0)
|
|
294
|
+
if (this.agents.length === 0)
|
|
488
295
|
return ' ';
|
|
489
|
-
|
|
490
|
-
const col = agentColor(this.agentStack[i]);
|
|
491
|
-
return `${col}│${ansi.reset} `;
|
|
492
|
-
}).join('');
|
|
493
|
-
return ' ' + pipes;
|
|
494
|
-
}
|
|
495
|
-
resetAgentStack() {
|
|
496
|
-
this.agentStack = [];
|
|
497
|
-
}
|
|
498
|
-
getSessionId(msg) {
|
|
499
|
-
return msg.session_id ?? null;
|
|
500
|
-
}
|
|
501
|
-
fileLog(msg) {
|
|
502
|
-
if (!this.logFile)
|
|
503
|
-
return;
|
|
504
|
-
try {
|
|
505
|
-
appendFileSync(this.logFile, `[${new Date().toISOString()}] ${msg}\n`);
|
|
506
|
-
}
|
|
507
|
-
catch { /* */ }
|
|
296
|
+
return ' ' + this.agents.map(n => `${agentColor(n)}│${a.reset} `).join('');
|
|
508
297
|
}
|
|
298
|
+
resetAgentStack() { this.agents = []; }
|
|
299
|
+
getSessionId(msg) { return msg.session_id ?? null; }
|
|
509
300
|
}
|
|
@@ -43,17 +43,17 @@ describe('TUI — non-interactive mode', () => {
|
|
|
43
43
|
it('warn writes with warning', () => {
|
|
44
44
|
const tui = new TUI(LOG_FILE, false);
|
|
45
45
|
tui.warn('be careful');
|
|
46
|
-
expect(readLog()).toContain('
|
|
46
|
+
expect(readLog()).toContain('be careful');
|
|
47
47
|
});
|
|
48
48
|
it('error writes with error', () => {
|
|
49
49
|
const tui = new TUI(LOG_FILE, false);
|
|
50
50
|
tui.error('something broke');
|
|
51
|
-
expect(readLog()).toContain('
|
|
51
|
+
expect(readLog()).toContain('something broke');
|
|
52
52
|
});
|
|
53
53
|
it('success writes ok', () => {
|
|
54
54
|
const tui = new TUI(LOG_FILE, false);
|
|
55
55
|
tui.success('all good');
|
|
56
|
-
expect(readLog()).toContain('
|
|
56
|
+
expect(readLog()).toContain('all good');
|
|
57
57
|
});
|
|
58
58
|
it('setIteration renders iteration header', () => {
|
|
59
59
|
const tui = new TUI(LOG_FILE, false);
|