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.
@@ -1,446 +1,263 @@
1
1
  // ─── Terminal UI for autonomous loop ───
2
- // Renders: fixed header (progress), scrolling log, fixed footer (stats + input)
3
- // Falls back to plain Formatter when no TTY.
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 ESC = '\x1b[';
8
- const ansi = {
9
- clear: `${ESC}2J`,
10
- home: `${ESC}H`,
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
- black: `${ESC}30m`, red: `${ESC}31m`, green: `${ESC}32m`, yellow: `${ESC}33m`,
23
- blue: `${ESC}34m`, magenta: `${ESC}35m`, cyan: `${ESC}36m`, white: `${ESC}37m`,
24
- gray: `${ESC}90m`, brightRed: `${ESC}91m`, brightGreen: `${ESC}92m`,
25
- brightYellow: `${ESC}93m`, brightBlue: `${ESC}94m`, brightMagenta: `${ESC}95m`,
26
- brightCyan: `${ESC}96m`,
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
- // ─── Shared constants ───
34
- const TOOL_ICONS = {
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
- const AGENT_COLORS = [ansi.fg.brightCyan, ansi.fg.brightMagenta, ansi.fg.brightGreen, ansi.fg.brightYellow, ansi.fg.brightBlue, ansi.fg.brightRed];
40
- const agentColorMap = new Map();
41
- let colorIdx = 0;
42
- function agentColor(name) {
43
- if (!agentColorMap.has(name)) {
44
- agentColorMap.set(name, AGENT_COLORS[colorIdx++ % AGENT_COLORS.length]);
45
- }
46
- return agentColorMap.get(name);
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
- paused = false;
85
- // Layout
86
- rows = 0;
87
- cols = 0;
88
- headerRows = 3;
89
- footerRows = 3;
90
- logLines = [];
91
- maxLogLines = 500;
92
- // State
93
- iteration = 0;
94
- maxIterations = 0;
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.stdout.isTTY === true;
106
- if (this.interactive) {
107
- this.initTUI();
108
- }
61
+ this.interactive = interactive && process.stdin.isTTY === true;
62
+ if (this.interactive)
63
+ this.setupInput();
109
64
  }
110
- // ─── TUI init/teardown ───
111
- initTUI() {
112
- const { rows, columns } = process.stdout;
113
- this.rows = rows || 40;
114
- this.cols = columns || 120;
115
- process.stdout.write(ansi.hideCursor);
116
- process.stdout.write(ansi.clear + ansi.home);
117
- this.renderHeader();
118
- this.renderFooter();
119
- this.setScrollRegion();
120
- // Handle resize
121
- process.stdout.on('resize', () => {
122
- this.rows = process.stdout.rows || 40;
123
- this.cols = process.stdout.columns || 120;
124
- this.renderHeader();
125
- this.renderFooter();
126
- this.setScrollRegion();
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
- // Handle raw keypress for input display
136
- if (process.stdin.isTTY) {
137
- process.stdin.setRawMode(true);
138
- process.stdin.resume();
139
- process.stdin.on('data', (data) => {
140
- const key = data.toString();
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
- // Ctrl+S → stop (graceful, finish current iteration)
156
- if (key === '\x13') {
157
- this.onHotkey?.('stop');
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
- setInputHandler(handler) {
196
- this.onInput = handler;
197
- }
198
- setHotkeyHandler(handler) {
199
- this.onHotkey = handler;
200
- }
201
- isPaused() {
202
- return this.paused;
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
- renderHeader() {
213
- process.stdout.write(ansi.saveCursor);
214
- const pct = this.maxIterations > 0 ? Math.round((this.iteration / this.maxIterations) * 100) : 0;
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
- renderFooter() {
233
- process.stdout.write(ansi.saveCursor);
234
- const elapsed = this.iterStart > 0 ? fmtDuration(Date.now() - this.iterStart) : '—';
235
- const totalTok = this.tokens.input + this.tokens.output;
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
- renderInputLine() {
260
- process.stdout.write(ansi.saveCursor);
261
- const row = this.rows;
262
- process.stdout.write(ansi.moveTo(row, 1) + ansi.clearLine);
263
- process.stdout.write(`${ansi.bg.medGray}${ansi.fg.white} › ${this.inputBuffer}${' '.repeat(Math.max(0, this.cols - this.inputBuffer.length - 3))}${ansi.reset}`);
264
- process.stdout.write(ansi.restoreCursor);
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
- // Write to scroll region
270
- const scrollBottom = this.rows - this.footerRows;
271
- process.stdout.write(ansi.saveCursor);
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
- this.logLines.push(raw);
282
- if (this.logLines.length > this.maxLogLines) {
283
- this.logLines = this.logLines.slice(-this.maxLogLines);
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
- // File log
137
+ }
138
+ fileOnly(msg) {
286
139
  if (this.logFile) {
287
140
  try {
288
- appendFileSync(this.logFile, `[${new Date().toISOString()}] ${raw}\n`);
141
+ appendFileSync(this.logFile, `[${new Date().toISOString()}] ${msg}\n`);
289
142
  }
290
143
  catch { /* */ }
291
144
  }
292
145
  }
293
- // ─── Public API ───
146
+ // ─── Public methods ───
294
147
  banner() {
295
- if (!this.interactive) {
296
- console.log('');
297
- console.log(`${ansi.bold}${ansi.fg.cyan} ╔══════════════════════════════════════════════╗${ansi.reset}`);
298
- console.log(`${ansi.bold}${ansi.fg.cyan}${ansi.fg.white}Claude Starter Kit — Autonomous Loop${ansi.fg.cyan} ║${ansi.reset}`);
299
- console.log(`${ansi.bold}${ansi.fg.cyan} ╚══════════════════════════════════════════════╝${ansi.reset}`);
300
- console.log('');
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
- // TUI mode: header is already rendered
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.iteration = i;
318
- this.maxIterations = max;
319
- this.iterStart = Date.now();
320
- this.toolCalls = 0;
321
- this.tokens = { input: 0, output: 0 };
322
- this.agentStack = [];
323
- if (this.interactive) {
324
- this.renderHeader();
325
- this.renderFooter();
326
- }
327
- else {
328
- const pct = Math.round((i / max) * 100);
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.taskName = name;
334
- this.tasksDone = done;
335
- this.tasksTotal = total;
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.iterStart;
342
- const totalTok = this.tokens.input + this.tokens.output;
343
- if (!this.interactive) {
344
- this.appendLog(`\n ${ansi.fg.gray}──── ${fmtDuration(elapsed)} │ ${this.toolCalls} tools │ ${fmtTokens(totalTok)} tokens ────${ansi.reset}`, `--- ${fmtDuration(elapsed)} | ${this.toolCalls} tools | ${fmtTokens(totalTok)} tokens ---`);
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.handleAssistant(message);
192
+ this.onAssistant(message);
352
193
  break;
353
194
  case 'user':
354
- this.handleToolResult(message);
195
+ this.onToolResult(message);
355
196
  break;
356
197
  case 'system':
357
- this.handleSystem(message);
198
+ this.onSystem(message);
358
199
  break;
359
200
  case 'result':
360
- this.handleResult(message);
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
- handleAssistant(msg) {
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.tokens.input += msg.message.usage.input_tokens || 0;
371
- this.tokens.output += msg.message.usage.output_tokens || 0;
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
- const text = truncate(block.text, 300);
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
- formatToolUse(block) {
387
- this.toolCalls++;
388
- if (this.interactive)
389
- this.renderFooter();
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 indent = this.indent();
394
- const time = `${ansi.fg.gray}${ts()}${ansi.reset}`;
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 agentType = input.subagent_type || input.type || 'agent';
398
- const desc = truncate(input.description || input.prompt || '', 50);
399
- const col = agentColor(agentType);
400
- const model = input.model ? `${ansi.fg.gray}(${input.model})${ansi.reset}` : '';
401
- this.agentStack.push(agentType);
402
- this.appendLog(`${indent}${time} ${icon} ${col}${ansi.bold}${agentType}${ansi.reset} ${model} ${ansi.fg.gray}${desc}${ansi.reset}`, `AGENT: ${agentType} ${input.model || ''} — ${desc}`);
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
- const cmd = truncate(input.command || '', 70);
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
- case 'Read': {
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
- const path = (input.file_path || '').replace(/^\/project\//, '');
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 = `${ansi.fg.cyan}${input.pattern || ''}${ansi.reset}`;
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 'TodoRead':
436
- detail = `${ansi.fg.gray}reading tasks${ansi.reset}`;
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.appendLog(`${indent}${time} ${icon} ${ansi.bold}${name}${ansi.reset} ${detail}`, `TOOL: ${name} ${JSON.stringify(input).slice(0, 200)}`);
258
+ this.out(`${pre}${time} ${icon} ${a.bold}${name}${a.reset} ${detail}`, `TOOL: ${name} ${JSON.stringify(input).slice(0, 200)}`);
442
259
  }
443
- handleToolResult(msg) {
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 isError = block.is_error === true;
454
- const indent = this.indent();
455
- if (isError) {
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
- const short = truncate(output, 100);
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 lines = output.split('\n').length;
465
- this.appendLog(`${indent} ${ansi.fg.green}✓${ansi.reset} ${ansi.fg.gray}${lines} lines${ansi.reset}`, `OK: (${lines} lines)`);
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
- handleSystem(msg) {
470
- if (msg.subtype === 'task_progress') {
471
- const desc = msg.description || '';
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
- handleResult(msg) {
480
- if (msg.session_id) {
481
- this.fileLog(`SESSION: ${msg.session_id}`);
482
- }
288
+ onResult(msg) {
289
+ if (msg.session_id)
290
+ this.fileOnly(`SESSION: ${msg.session_id}`);
483
291
  }
484
- // ─── Helpers ───
292
+ // ─── Indent ───
485
293
  indent() {
486
- const depth = this.agentStack.length;
487
- if (depth === 0)
294
+ if (this.agents.length === 0)
488
295
  return ' ';
489
- const pipes = Array.from({ length: depth }, (_, i) => {
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('WARN: be careful');
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('ERROR: something broke');
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('OK: all good');
56
+ expect(readLog()).toContain('all good');
57
57
  });
58
58
  it('setIteration renders iteration header', () => {
59
59
  const tui = new TUI(LOG_FILE, false);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-claude-workspace",
3
- "version": "1.1.123",
3
+ "version": "1.1.124",
4
4
  "author": "",
5
5
  "repository": {
6
6
  "type": "git",