create-claude-workspace 1.1.142 → 1.1.144

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.
@@ -7,7 +7,7 @@ import { execSync } from 'node:child_process';
7
7
  import { DEFAULTS } from './lib/types.mjs';
8
8
  import { emptyCheckpoint, readCheckpoint, writeCheckpoint } from './lib/state.mjs';
9
9
  import { pollForNewWork } from './lib/idle-poll.mjs';
10
- import { TUI } from './lib/tui.js';
10
+ import { TUI } from './lib/tui.mjs';
11
11
  import { query } from '@anthropic-ai/claude-agent-sdk';
12
12
  import { config as dotenvConfig } from '@dotenvx/dotenvx';
13
13
  // Disable SDK built-in agents (general-purpose, Explore, Plan, statusline-setup)
@@ -301,6 +301,7 @@ async function main() {
301
301
  dotenvConfig({ path: resolve(opts.projectDir, '.env'), override: false, quiet: true });
302
302
  const logPath = resolve(opts.projectDir, opts.logFile);
303
303
  const tui = new TUI(logPath, opts.interactive);
304
+ tui.setTopAgent('orchestrator');
304
305
  tui.banner();
305
306
  tui.info(`Project: ${opts.projectDir}`);
306
307
  tui.info(`Max iterations: ${opts.maxIterations} │ Max turns: ${opts.maxTurns}`);
@@ -1,185 +1,231 @@
1
1
  // ─── Terminal UI for autonomous loop ───
2
- // Events push lines to a buffer. Render loop (4 fps) draws pending lines
3
- // + status bar. No cursor tricks just append lines and \r overwrite status.
2
+ // Log lines: normal stdout (scrollable, persistent history)
3
+ // Status bar: overwrites last line using \r (carriage return)
4
+ // No ink render() — all rendering is raw ANSI to preserve scroll history.
4
5
  import { appendFileSync } from 'node:fs';
5
- // ─── ANSI ───
6
- const a = {
7
- reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m',
8
- fg: {
9
- red: '\x1b[31m', green: '\x1b[32m', yellow: '\x1b[33m',
10
- blue: '\x1b[34m', magenta: '\x1b[35m', cyan: '\x1b[36m', white: '\x1b[37m',
11
- gray: '\x1b[90m', brightRed: '\x1b[91m', brightGreen: '\x1b[92m',
12
- brightYellow: '\x1b[93m', brightBlue: '\x1b[94m', brightMagenta: '\x1b[95m',
13
- brightCyan: '\x1b[96m',
14
- },
15
- bg: { gray: '\x1b[48;5;236m' },
16
- };
17
- // ─── Shared utilities ───
18
- const AGENT_PALETTE = [a.fg.brightCyan, a.fg.brightMagenta, a.fg.brightGreen, a.fg.brightYellow, a.fg.brightBlue, a.fg.brightRed];
19
- const agentColors = new Map();
6
+ // ─── Agent colors ───
7
+ const PALETTE = ['cyan', 'magenta', 'green', 'yellow', 'blue', 'red'];
8
+ const agentColorMap = new Map();
20
9
  let nextColor = 0;
21
10
  function agentColor(name) {
22
- if (!agentColors.has(name))
23
- agentColors.set(name, AGENT_PALETTE[nextColor++ % AGENT_PALETTE.length]);
24
- return agentColors.get(name);
11
+ if (!agentColorMap.has(name))
12
+ agentColorMap.set(name, PALETTE[nextColor++ % PALETTE.length]);
13
+ return agentColorMap.get(name);
25
14
  }
15
+ const ANSI_COLORS = {
16
+ red: '\x1b[31m', green: '\x1b[32m', yellow: '\x1b[33m', blue: '\x1b[34m',
17
+ magenta: '\x1b[35m', cyan: '\x1b[36m', white: '\x1b[37m', gray: '\x1b[90m',
18
+ };
19
+ const RESET = '\x1b[0m';
20
+ const BOLD = '\x1b[1m';
21
+ const DIM = '\x1b[2m';
22
+ const HIDE_CURSOR = '\x1b[?25l';
23
+ const SHOW_CURSOR = '\x1b[?25h';
24
+ const CLEAR_LINE = '\r\x1b[2K';
25
+ // ─── Tool icons ───
26
26
  const ICONS = {
27
27
  Bash: '⚡', Read: '📖', Write: '✏️ ', Edit: '🔧', Glob: '🔍', Grep: '🔎',
28
28
  Agent: '🤖', TodoRead: '📋', TodoWrite: '📝', WebSearch: '🌐', WebFetch: '🌐', AskUserQuestion: '❓',
29
29
  };
30
+ // ─── Helpers ───
30
31
  function ts() { return new Date().toLocaleTimeString('en-GB', { hour12: false }); }
31
- function strip(s) { return s.replace(/\x1b\[[0-9;]*m/g, ''); }
32
32
  function trunc(s, n) { const c = s.replace(/\n/g, ' ').trim(); return c.length > n ? c.slice(0, n) + '…' : c; }
33
33
  function fmtTok(n) { return n < 1e3 ? `${n}` : n < 1e6 ? `${(n / 1e3).toFixed(1)}K` : `${(n / 1e6).toFixed(2)}M`; }
34
34
  function fmtDur(ms) { return ms < 1e3 ? `${ms}ms` : ms < 6e4 ? `${(ms / 1e3).toFixed(1)}s` : `${(ms / 6e4).toFixed(1)}m`; }
35
- 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}`; }
36
- // ─── TUI ───
35
+ // ─── TUI class ───
37
36
  export class TUI {
38
37
  logFile;
39
38
  interactive;
40
39
  onInput = null;
41
40
  onHotkey = null;
42
- paused_ = false;
43
- // Render state
44
- pendingLines = [];
45
- renderTimer = null;
46
- lastStatusText = '';
47
- dirty = false;
48
- // Stats
49
- loopStart = Date.now();
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 = [];
59
- inputBuf = '';
41
+ state;
42
+ statusBarVisible = false;
43
+ statusTimer = null;
44
+ stdinHandler = null;
45
+ topAgent = null;
60
46
  constructor(logFile, interactive = false) {
61
47
  this.logFile = logFile;
62
48
  this.interactive = interactive && process.stdin.isTTY === true;
49
+ this.state = {
50
+ iteration: 0, maxIter: 0, loopStart: Date.now(), iterStart: 0,
51
+ tools: 0, tokensIn: 0, tokensOut: 0, agents: [], taskName: '',
52
+ tasksDone: 0, tasksTotal: 0, paused: false, inputBuf: '',
53
+ };
63
54
  if (this.interactive) {
64
- this.setupInput();
65
- // Hide cursor — prevents flicker during status bar rewrites
66
- process.stdout.write('\x1b[?25l');
67
- // Render loop — 100ms poll, but only redraws when something changed
68
- this.renderTimer = setInterval(() => this.render(), 100);
69
- }
70
- }
71
- // ─── Render loop ───
72
- render() {
73
- const out = process.stdout;
74
- const hasPending = this.pendingLines.length > 0;
75
- const newStatus = this.buildStatus();
76
- const statusChanged = newStatus !== this.lastStatusText;
77
- if (!hasPending && !statusChanged && !this.dirty)
78
- return;
79
- // Build entire frame as single string — one write() call = no flicker
80
- let frame = '\r\x1b[2K'; // clear status line
81
- if (hasPending) {
82
- for (const line of this.pendingLines) {
83
- frame += line + '\n';
55
+ // Hide cursor to prevent flickering
56
+ process.stdout.write(HIDE_CURSOR);
57
+ // Set up raw stdin for keyboard input
58
+ if (process.stdin.setRawMode) {
59
+ process.stdin.setRawMode(true);
60
+ process.stdin.resume();
61
+ process.stdin.setEncoding('utf8');
62
+ this.stdinHandler = (data) => {
63
+ const str = String(data);
64
+ for (let i = 0; i < str.length; i++) {
65
+ const ch = str[i];
66
+ const code = ch.charCodeAt(0);
67
+ // Ctrl+C (0x03)
68
+ if (code === 0x03) {
69
+ this.onHotkey?.('quit');
70
+ this.destroy();
71
+ process.exit(0);
72
+ return;
73
+ }
74
+ // Ctrl+Z (0x1A)
75
+ if (code === 0x1A) {
76
+ this.state.paused = !this.state.paused;
77
+ this.onHotkey?.(this.state.paused ? 'pause' : 'resume');
78
+ this.renderStatusBar();
79
+ return;
80
+ }
81
+ // Ctrl+S (0x13)
82
+ if (code === 0x13) {
83
+ this.onHotkey?.('stop');
84
+ return;
85
+ }
86
+ // Enter (0x0D or 0x0A)
87
+ if (code === 0x0D || code === 0x0A) {
88
+ if (this.state.inputBuf.trim()) {
89
+ this.onInput?.(this.state.inputBuf.trim());
90
+ }
91
+ this.state.inputBuf = '';
92
+ this.renderStatusBar();
93
+ return;
94
+ }
95
+ // Backspace (0x08 or 0x7F)
96
+ if (code === 0x08 || code === 0x7F) {
97
+ this.state.inputBuf = this.state.inputBuf.slice(0, -1);
98
+ this.renderStatusBar();
99
+ return;
100
+ }
101
+ // Escape sequences (arrows, etc.) — skip
102
+ if (code === 0x1B) {
103
+ // consume the rest of the escape sequence
104
+ while (i + 1 < str.length && str[i + 1] >= '\x20' && str[i + 1] <= '\x7E')
105
+ i++;
106
+ if (i + 1 < str.length && str[i + 1] === '[')
107
+ i++; // CSI
108
+ return;
109
+ }
110
+ // Printable characters
111
+ if (code >= 0x20) {
112
+ this.state.inputBuf += ch;
113
+ this.renderStatusBar();
114
+ }
115
+ }
116
+ };
117
+ process.stdin.on('data', this.stdinHandler);
84
118
  }
85
- this.pendingLines = [];
119
+ // Timer to refresh status bar every second (for elapsed time)
120
+ this.statusTimer = setInterval(() => this.renderStatusBar(), 1000);
86
121
  }
87
- frame += newStatus;
88
- out.write(frame);
89
- this.lastStatusText = newStatus;
90
- this.dirty = false;
91
122
  }
92
- buildStatus() {
93
- const elapsed = fmtDur(Date.now() - this.loopStart);
94
- const iterTime = this.iterStart_ ? fmtDur(Date.now() - this.iterStart_) : '—';
95
- const pct = this.maxIter > 0 ? Math.round((this.iteration_ / this.maxIter) * 100) : 0;
96
- const tok = fmtTok(this.tokens_.input + this.tokens_.output);
97
- const parts = [
98
- `${a.fg.white}${elapsed}`,
99
- `Iter ${this.iteration_}/${this.maxIter} ${bar(pct, 8)}`,
100
- `${iterTime}`,
101
- `${a.fg.cyan}${this.tools}${a.reset} tools`,
102
- `${a.fg.yellow}${tok}${a.reset} tok`,
103
- ];
104
- if (this.agents.length > 0) {
105
- const cur = this.agents[this.agents.length - 1];
106
- parts.push(`${agentColor(cur)}${cur}${a.reset}`);
107
- }
108
- if (this.taskName_)
109
- parts.push(`${a.fg.cyan}${trunc(this.taskName_, 20)}${a.reset}`);
110
- if (this.paused_)
111
- parts.push(`${a.fg.yellow}⏸ PAUSED${a.reset}`);
112
- const info = parts.join(`${a.fg.gray} │ ${a.reset}`);
113
- const input = this.inputBuf ? ` ${a.fg.gray}›${a.reset} ${this.inputBuf}` : '';
114
- const cols = process.stdout.columns || 120;
115
- const line = ` ${info}${input} `;
116
- const pad = Math.max(0, cols - strip(line).length);
117
- return `\r${a.bg.gray}${line}${' '.repeat(pad)}${a.reset}`;
118
- }
119
- // ─── Input ───
120
- setupInput() {
121
- if (!process.stdin.isTTY)
122
- return;
123
- process.stdin.setRawMode(true);
124
- process.stdin.resume();
125
- process.stdin.on('data', (data) => {
126
- const key = data.toString();
127
- if (key === '\x03') {
128
- this.onHotkey?.('quit');
129
- return;
130
- }
131
- if (key === '\x1a') {
132
- this.paused_ = !this.paused_;
133
- this.dirty = true;
134
- this.onHotkey?.(this.paused_ ? 'pause' : 'resume');
135
- return;
136
- }
137
- if (key === '\x13') {
138
- this.onHotkey?.('stop');
139
- return;
123
+ destroy() {
124
+ if (this.interactive) {
125
+ // Clear status bar
126
+ if (this.statusBarVisible) {
127
+ process.stdout.write(CLEAR_LINE);
128
+ this.statusBarVisible = false;
140
129
  }
141
- if (key === '\r' || key === '\n') {
142
- if (this.inputBuf.trim())
143
- this.onInput?.(this.inputBuf.trim());
144
- this.inputBuf = '';
145
- this.dirty = true;
146
- return;
130
+ // Show cursor
131
+ process.stdout.write(SHOW_CURSOR);
132
+ // Stop timer
133
+ if (this.statusTimer) {
134
+ clearInterval(this.statusTimer);
135
+ this.statusTimer = null;
147
136
  }
148
- if (key === '\x7f' || key === '\b') {
149
- this.inputBuf = this.inputBuf.slice(0, -1);
150
- this.dirty = true;
151
- return;
137
+ // Restore stdin
138
+ if (this.stdinHandler) {
139
+ process.stdin.removeListener('data', this.stdinHandler);
140
+ this.stdinHandler = null;
152
141
  }
153
- if (key.length === 1 && key >= ' ') {
154
- this.inputBuf += key;
155
- this.dirty = true;
142
+ if (process.stdin.setRawMode) {
143
+ try {
144
+ process.stdin.setRawMode(false);
145
+ process.stdin.pause();
146
+ }
147
+ catch { /* may already be closed */ }
156
148
  }
157
- });
149
+ }
158
150
  }
159
- destroy() {
160
- if (this.renderTimer) {
161
- clearInterval(this.renderTimer);
162
- this.renderTimer = null;
151
+ setInputHandler(h) { this.onInput = h; }
152
+ setHotkeyHandler(h) { this.onHotkey = h; }
153
+ isPaused() { return this.state.paused; }
154
+ /** Set the top-level agent name so all log lines show it from the start. */
155
+ setTopAgent(name) {
156
+ this.topAgent = name;
157
+ if (this.state.agents.length === 0) {
158
+ this.state.agents.push(name);
163
159
  }
164
- if (this.interactive) {
165
- this.render();
166
- process.stdout.write('\x1b[?25h\n'); // show cursor + newline
167
- if (process.stdin.isTTY)
168
- process.stdin.setRawMode(false);
160
+ }
161
+ // ─── Status bar (overwrites current line, no \n) ───
162
+ buildStatusBar() {
163
+ const s = this.state;
164
+ const elapsed = fmtDur(Date.now() - s.loopStart);
165
+ const iterTime = s.iterStart ? fmtDur(Date.now() - s.iterStart) : '—';
166
+ const pct = s.maxIter > 0 ? Math.round((s.iteration / s.maxIter) * 100) : 0;
167
+ const filled = Math.round((pct / 100) * 8);
168
+ const bar = '\u2588'.repeat(filled) + '\u2591'.repeat(8 - filled);
169
+ const tok = fmtTok(s.tokensIn + s.tokensOut);
170
+ const cur = s.agents.length > 0 ? s.agents[s.agents.length - 1] : '';
171
+ let line = ` ${elapsed} | Iter ${s.iteration}/${s.maxIter} ${bar} | ${iterTime} | ${s.tools} tools | ${tok} tok`;
172
+ if (cur)
173
+ line += ` | ${cur}`;
174
+ if (s.taskName)
175
+ line += ` | ${trunc(s.taskName, 20)}`;
176
+ if (s.paused)
177
+ line += ' | PAUSED';
178
+ if (s.inputBuf)
179
+ line += ` > ${s.inputBuf}`;
180
+ // Truncate to terminal width
181
+ const cols = process.stdout.columns || 120;
182
+ if (line.length > cols)
183
+ line = line.slice(0, cols - 1) + '…';
184
+ return line;
185
+ }
186
+ buildColoredStatusBar() {
187
+ const s = this.state;
188
+ const elapsed = fmtDur(Date.now() - s.loopStart);
189
+ const iterTime = s.iterStart ? fmtDur(Date.now() - s.iterStart) : '—';
190
+ const pct = s.maxIter > 0 ? Math.round((s.iteration / s.maxIter) * 100) : 0;
191
+ const filled = Math.round((pct / 100) * 8);
192
+ const bar = `${ANSI_COLORS.green}${'\u2588'.repeat(filled)}${ANSI_COLORS.gray}${'\u2591'.repeat(8 - filled)}${RESET}`;
193
+ const tok = fmtTok(s.tokensIn + s.tokensOut);
194
+ const cur = s.agents.length > 0 ? s.agents[s.agents.length - 1] : '';
195
+ let line = `\x1b[7m ${elapsed} | Iter ${s.iteration}/${s.maxIter} \x1b[27m ${bar} \x1b[7m ${iterTime} | ${ANSI_COLORS.cyan}${s.tools}${RESET}\x1b[7m tools | ${ANSI_COLORS.yellow}${tok}${RESET}\x1b[7m tok`;
196
+ if (cur) {
197
+ const col = ANSI_COLORS[agentColor(cur)] || '';
198
+ line += ` | ${col}${BOLD}${cur}${RESET}\x1b[7m`;
169
199
  }
200
+ if (s.taskName)
201
+ line += ` | ${ANSI_COLORS.cyan}${trunc(s.taskName, 20)}${RESET}\x1b[7m`;
202
+ if (s.paused)
203
+ line += ` | ${ANSI_COLORS.yellow}⏸ PAUSED${RESET}\x1b[7m`;
204
+ if (s.inputBuf)
205
+ line += ` › ${s.inputBuf}`;
206
+ line += ` ${RESET}`;
207
+ return line;
170
208
  }
171
- setInputHandler(handler) { this.onInput = handler; }
172
- setHotkeyHandler(handler) { this.onHotkey = handler; }
173
- isPaused() { return this.paused_; }
174
- // ─── Output (buffered) ───
175
- push(formatted, raw) {
176
- if (this.interactive) {
177
- this.pendingLines.push(formatted);
209
+ renderStatusBar() {
210
+ if (!this.interactive)
211
+ return;
212
+ const line = this.buildColoredStatusBar();
213
+ process.stdout.write(CLEAR_LINE + line);
214
+ this.statusBarVisible = true;
215
+ }
216
+ // ─── Log output (stdout — preserves scroll history) ───
217
+ log(formatted, raw) {
218
+ if (this.interactive && this.statusBarVisible) {
219
+ // Clear the status bar line, write the log line, then re-render status bar
220
+ process.stdout.write(CLEAR_LINE + formatted + '\n');
221
+ this.renderStatusBar();
178
222
  }
179
223
  else {
180
- console.log(formatted);
224
+ process.stdout.write(formatted + '\n');
225
+ if (this.interactive)
226
+ this.renderStatusBar();
181
227
  }
182
- const plain = raw || strip(formatted);
228
+ const plain = raw || formatted.replace(/\x1b\[[0-9;]*m/g, '');
183
229
  if (this.logFile) {
184
230
  try {
185
231
  appendFileSync(this.logFile, `[${new Date().toISOString()}] ${plain}\n`);
@@ -196,64 +242,52 @@ export class TUI {
196
242
  }
197
243
  }
198
244
  // ─── Agent prefix ───
199
- agentPrefix() {
200
- if (this.agents.length === 0)
245
+ prefix() {
246
+ if (this.state.agents.length === 0)
201
247
  return '';
202
- const cur = this.agents[this.agents.length - 1];
203
- const col = agentColor(cur);
204
- const short = cur.length > 14 ? cur.slice(0, 14) : cur;
205
- return `${col}${short}${a.reset} `;
206
- }
207
- indent() {
208
- const prefix = this.agentPrefix();
209
- if (this.agents.length <= 1)
210
- return ` ${prefix}`;
211
- const pipes = this.agents.slice(0, -1).map(n => `${agentColor(n)}│${a.reset}`).join('');
212
- return ` ${pipes} ${prefix}`;
248
+ const cur = this.state.agents[this.state.agents.length - 1];
249
+ const col = ANSI_COLORS[agentColor(cur)] || '';
250
+ const name = cur.length > 14 ? cur.slice(0, 14) : cur;
251
+ return `${col}${name.padEnd(15)}${RESET}`;
213
252
  }
214
253
  // ─── Public API ───
215
254
  banner() {
216
- this.push('');
217
- this.push(` ${a.fg.cyan}${a.bold}╔══════════════════════════════════════════════╗${a.reset}`);
218
- this.push(` ${a.fg.cyan}${a.bold}║ ${a.fg.white}Claude Starter Kit — Autonomous Loop${a.fg.cyan} ║${a.reset}`);
219
- this.push(` ${a.fg.cyan}${a.bold}╚══════════════════════════════════════════════╝${a.reset}`);
255
+ this.log('');
256
+ this.log(` ${ANSI_COLORS.cyan}${BOLD}╔══════════════════════════════════════════════╗${RESET}`);
257
+ this.log(` ${ANSI_COLORS.cyan}${BOLD}║ ${ANSI_COLORS.white}Claude Starter Kit — Autonomous Loop${ANSI_COLORS.cyan} ║${RESET}`);
258
+ this.log(` ${ANSI_COLORS.cyan}${BOLD}╚══════════════════════════════════════════════╝${RESET}`);
220
259
  if (this.interactive) {
221
- this.push(` ${a.fg.gray} Ctrl+Z pause │ Ctrl+S stop │ Ctrl+C quit │ Type to send input${a.reset}`);
260
+ this.log(` ${ANSI_COLORS.gray} Ctrl+Z pause │ Ctrl+S stop │ Ctrl+C quit │ Type to send input${RESET}`);
222
261
  }
223
- this.push('');
262
+ this.log('');
224
263
  }
225
- info(msg) { this.push(` ${a.fg.gray}${ts()}${a.reset} ${a.fg.blue}ℹ${a.reset} ${msg}`); }
226
- warn(msg) { this.push(` ${a.fg.gray}${ts()}${a.reset} ${a.fg.yellow}⚠${a.reset} ${a.fg.yellow}${msg}${a.reset}`); }
227
- error(msg) { this.push(` ${a.fg.gray}${ts()}${a.reset} ${a.fg.red}✗${a.reset} ${a.fg.red}${msg}${a.reset}`); }
228
- success(msg) { this.push(` ${a.fg.gray}${ts()}${a.reset} ${a.fg.green}✓${a.reset} ${a.fg.green}${msg}${a.reset}`); }
264
+ info(msg) { this.log(` ${this.prefix()}${ANSI_COLORS.gray}${ts()}${RESET} ${ANSI_COLORS.blue}ℹ${RESET} ${msg}`); }
265
+ warn(msg) { this.log(` ${this.prefix()}${ANSI_COLORS.gray}${ts()}${RESET} ${ANSI_COLORS.yellow} ${msg}${RESET}`); }
266
+ error(msg) { this.log(` ${this.prefix()}${ANSI_COLORS.gray}${ts()}${RESET} ${ANSI_COLORS.red} ${msg}${RESET}`); }
267
+ success(msg) { this.log(` ${this.prefix()}${ANSI_COLORS.gray}${ts()}${RESET} ${ANSI_COLORS.green} ${msg}${RESET}`); }
229
268
  setIteration(i, max) {
230
- this.iteration_ = i;
231
- this.maxIter = max;
232
- this.iterStart_ = Date.now();
233
- this.tools = 0;
234
- this.tokens_ = { input: 0, output: 0 };
235
- this.agents = [];
269
+ this.state.iteration = i;
270
+ this.state.maxIter = max;
271
+ this.state.iterStart = Date.now();
272
+ this.state.tools = 0;
273
+ this.state.tokensIn = 0;
274
+ this.state.tokensOut = 0;
275
+ this.state.agents = this.topAgent ? [this.topAgent] : [];
236
276
  const pct = Math.round((i / max) * 100);
237
- const elapsed = fmtDur(Date.now() - this.loopStart);
238
- this.push('');
239
- this.push(` ${a.bold}${a.fg.white}━━━ Iteration ${i}/${max} ${bar(pct, 20)} ${pct}% ${a.fg.gray}│ ${elapsed} elapsed${a.fg.white} ━━━${a.reset}`);
240
- if (this.taskName_) {
241
- const tPct = this.tasksTotal_ > 0 ? Math.round((this.tasksDone_ / this.tasksTotal_) * 100) : 0;
242
- this.push(` ${a.fg.cyan}📋 ${this.taskName_}${a.reset} ${a.fg.gray}(${this.tasksDone_}/${this.tasksTotal_} tasks ${bar(tPct, 10)} ${tPct}%)${a.reset}`);
243
- }
244
- this.push('');
277
+ const elapsed = fmtDur(Date.now() - this.state.loopStart);
278
+ this.log('');
279
+ this.log(` ${BOLD}━━━ Iteration ${i}/${max} ${pct}% │ ${elapsed} elapsed ━━━${RESET}`);
280
+ this.log('');
245
281
  }
246
282
  setTask(name, done, total) {
247
- this.taskName_ = name;
248
- this.tasksDone_ = done;
249
- this.tasksTotal_ = total;
283
+ this.state.taskName = name;
284
+ this.state.tasksDone = done;
285
+ this.state.tasksTotal = total;
250
286
  }
251
287
  iterationEnd() {
252
- const iterElapsed = fmtDur(Date.now() - this.iterStart_);
253
- const totalElapsed = fmtDur(Date.now() - this.loopStart);
254
- const tok = fmtTok(this.tokens_.input + this.tokens_.output);
255
- this.push('');
256
- this.push(` ${a.fg.gray}━━━━ ${iterElapsed} (iter) │ ${totalElapsed} (total) │ ${this.tools} tools │ ${tok} tokens ━━━━${a.reset}`);
288
+ const s = this.state;
289
+ this.log('');
290
+ this.log(` ${ANSI_COLORS.gray}━━━━ ${fmtDur(Date.now() - s.iterStart)} (iter) │ ${fmtDur(Date.now() - s.loopStart)} (total) │ ${s.tools} tools │ ${fmtTok(s.tokensIn + s.tokensOut)} tokens ━━━━${RESET}`);
257
291
  }
258
292
  // ─── SDK message handler ───
259
293
  handleMessage(message) {
@@ -277,54 +311,54 @@ export class TUI {
277
311
  if (!Array.isArray(content))
278
312
  return;
279
313
  if (msg.message?.usage) {
280
- this.tokens_.input += msg.message.usage.input_tokens || 0;
281
- this.tokens_.output += msg.message.usage.output_tokens || 0;
314
+ this.state.tokensIn += msg.message.usage.input_tokens || 0;
315
+ this.state.tokensOut += msg.message.usage.output_tokens || 0;
282
316
  }
283
317
  for (const block of content) {
284
318
  if (block.type === 'text' && block.text?.trim()) {
285
- this.push(`${this.indent()}${a.fg.white}${trunc(block.text, 300)}${a.reset}`, `TEXT: ${trunc(block.text, 300)}`);
319
+ this.log(` ${this.prefix()}${trunc(block.text, 300)}`, `TEXT: ${trunc(block.text, 300)}`);
286
320
  }
287
321
  if (block.type === 'tool_use')
288
322
  this.onToolUse(block);
289
323
  }
290
324
  }
291
325
  onToolUse(block) {
292
- this.tools++;
326
+ this.state.tools++;
293
327
  const name = block.name || '?';
294
328
  const icon = ICONS[name] || '⚙️';
295
329
  const input = block.input || {};
296
- const pre = this.indent();
297
- const time = `${a.fg.gray}${ts()}${a.reset}`;
330
+ const pre = this.prefix();
331
+ const time = `${ANSI_COLORS.gray}${ts()}${RESET}`;
298
332
  if (name === 'Agent') {
299
333
  const type = input.subagent_type || input.type || 'agent';
300
- const model = input.model ? ` ${a.fg.gray}(${input.model})${a.reset}` : '';
334
+ const model = input.model || '';
301
335
  const desc = trunc(input.description || input.prompt || '', 50);
302
- const col = agentColor(type);
303
- this.agents.push(type);
304
- this.push(`${pre}${time} ${icon} ${col}${a.bold}${type}${a.reset}${model} ${a.fg.gray}${desc}${a.reset}`, `AGENT: ${type} ${input.model || ''} — ${desc}`);
336
+ const col = ANSI_COLORS[agentColor(type)] || '';
337
+ this.state.agents.push(type);
338
+ this.log(` ${pre}${time} ${icon} ${col}${BOLD}${type}${RESET}${model ? ` ${ANSI_COLORS.gray}(${model})${RESET}` : ''} ${ANSI_COLORS.gray}${desc}${RESET}`, `AGENT: ${type} ${model} — ${desc}`);
305
339
  return;
306
340
  }
307
341
  let detail;
308
342
  switch (name) {
309
343
  case 'Bash':
310
- detail = `${a.fg.yellow}${trunc(input.command || '', 70)}${a.reset}`;
344
+ detail = `${ANSI_COLORS.yellow}${trunc(input.command || '', 70)}${RESET}`;
311
345
  break;
312
346
  case 'Read':
313
- detail = `${a.fg.cyan}${(input.file_path || '').replace(/^\/project\//, '')}${a.reset}`;
347
+ detail = `${ANSI_COLORS.cyan}${(input.file_path || '').replace(/^\/project\//, '')}${RESET}`;
314
348
  break;
315
349
  case 'Write':
316
350
  case 'Edit':
317
- detail = `${a.fg.cyan}${(input.file_path || '').replace(/^\/project\//, '')}${a.reset}`;
351
+ detail = `${ANSI_COLORS.cyan}${(input.file_path || '').replace(/^\/project\//, '')}${RESET}`;
318
352
  break;
319
353
  case 'Glob':
320
- detail = `${a.fg.cyan}${input.pattern || ''}${a.reset}`;
354
+ detail = `${ANSI_COLORS.cyan}${input.pattern || ''}${RESET}`;
321
355
  break;
322
356
  case 'Grep':
323
- detail = `${a.fg.cyan}/${input.pattern || ''}/${a.reset}${input.path ? ` ${a.fg.gray}in ${input.path}` : ''}${a.reset}`;
357
+ detail = `${ANSI_COLORS.cyan}/${input.pattern || ''}/${RESET}${input.path ? ` ${ANSI_COLORS.gray}in ${input.path}${RESET}` : ''}`;
324
358
  break;
325
- default: detail = `${a.fg.gray}${trunc(JSON.stringify(input), 60)}${a.reset}`;
359
+ default: detail = `${ANSI_COLORS.gray}${trunc(JSON.stringify(input), 60)}${RESET}`;
326
360
  }
327
- this.push(`${pre}${time} ${icon} ${a.bold}${name}${a.reset} ${detail}`, `TOOL: ${name} ${JSON.stringify(input).slice(0, 200)}`);
361
+ this.log(` ${pre}${time} ${icon} ${BOLD}${name}${RESET} ${detail}`, `TOOL: ${name} ${JSON.stringify(input).slice(0, 200)}`);
328
362
  }
329
363
  onToolResult(msg) {
330
364
  const content = msg.message?.content;
@@ -336,55 +370,46 @@ export class TUI {
336
370
  const output = String(block.content || '').trim();
337
371
  if (!output)
338
372
  continue;
339
- const pre = this.indent();
373
+ const pre = this.prefix();
340
374
  if (block.is_error) {
341
- this.push(`${pre} ${a.fg.red}✗ ${trunc(output, 100)}${a.reset}`, `ERROR: ${trunc(output, 100)}`);
375
+ this.log(` ${pre} ${ANSI_COLORS.red}✗ ${trunc(output, 100)}${RESET}`, `ERROR: ${trunc(output, 100)}`);
342
376
  }
343
377
  else if (output.length < 150) {
344
- this.push(`${pre} ${a.fg.green}✓${a.reset} ${a.fg.gray}${trunc(output, 100)}${a.reset}`, `OK: ${trunc(output, 100)}`);
378
+ this.log(` ${pre} ${ANSI_COLORS.green}✓${RESET} ${DIM}${trunc(output, 100)}${RESET}`, `OK: ${trunc(output, 100)}`);
345
379
  }
346
380
  else {
347
381
  const n = output.split('\n').length;
348
- this.push(`${pre} ${a.fg.green}✓${a.reset} ${a.fg.gray}${n} lines${a.reset}`, `OK: (${n} lines)`);
382
+ this.log(` ${pre} ${ANSI_COLORS.green}✓${RESET} ${DIM}${n} lines${RESET}`, `OK: (${n} lines)`);
349
383
  }
350
384
  }
351
385
  }
352
386
  onSystem(msg) {
353
387
  if (msg.subtype === 'init') {
354
- const model = msg.model || '?';
355
- const ver = msg.claude_code_version || '';
356
- const agentList = msg.agents?.join(', ') || 'none';
357
- this.push(` ${a.fg.gray}${ts()}${a.reset} ${a.fg.cyan}⚙${a.reset} Claude Code ${ver} │ Model: ${a.bold}${model}${a.reset} │ Agents: ${a.fg.cyan}${agentList}${a.reset}`);
358
- // Set top-level agent name from permissionMode or first agent
359
- if (this.agents.length === 0 && msg.agents?.length) {
360
- const topAgent = msg.agents[0];
361
- this.agents.push(topAgent);
362
- this.push(` ${a.fg.gray}${ts()}${a.reset} 🤖 ${agentColor(topAgent)}${a.bold}${topAgent}${a.reset} ${a.fg.gray}(${model})${a.reset}`, `AGENT_START: ${topAgent} (${model})`);
363
- }
388
+ this.log(` ${ANSI_COLORS.gray}${ts()}${RESET} ${ANSI_COLORS.cyan}⚙${RESET} Claude Code ${msg.claude_code_version || ''} │ Model: ${BOLD}${msg.model || '?'}${RESET} │ Agents: ${ANSI_COLORS.cyan}${msg.agents?.join(', ') || 'none'}${RESET}`);
364
389
  return;
365
390
  }
366
391
  if (msg.subtype === 'task_started') {
367
392
  const desc = msg.description || '';
368
- const col = agentColor(desc);
369
- this.agents.push(desc);
370
- this.push(` ${a.fg.gray}${ts()}${a.reset} 🤖 ${col}${a.bold}${desc}${a.reset}`, `AGENT_START: ${desc}`);
393
+ const col = ANSI_COLORS[agentColor(desc)] || '';
394
+ this.state.agents.push(desc);
395
+ this.log(` ${ANSI_COLORS.gray}${ts()}${RESET} 🤖 ${col}${BOLD}${desc}${RESET}`, `AGENT_START: ${desc}`);
371
396
  return;
372
397
  }
373
398
  if (msg.subtype === 'task_notification') {
374
399
  const status = msg.status || '';
375
- const name = this.agents.length > 0 ? this.agents.pop() : 'agent';
376
- const col = agentColor(name);
377
- const icon = status === 'completed' ? `${a.fg.green}✓` : `${a.fg.red}✗`;
378
- const summary = msg.summary ? ` ${a.fg.gray}${trunc(msg.summary, 60)}${a.reset}` : '';
379
- this.push(` ${a.fg.gray}${ts()}${a.reset} ${icon}${a.reset} ${col}${name}${a.reset} ${status}${summary}`, `AGENT_END: ${name} ${status}`);
400
+ const name = this.state.agents.length > 0 ? this.state.agents.pop() : 'agent';
401
+ const col = ANSI_COLORS[agentColor(name)] || '';
402
+ const icon = status === 'completed' ? `${ANSI_COLORS.green}✓` : `${ANSI_COLORS.red}✗`;
403
+ const summary = msg.summary ? ` ${ANSI_COLORS.gray}${trunc(msg.summary, 60)}${RESET}` : '';
404
+ this.log(` ${ANSI_COLORS.gray}${ts()}${RESET} ${icon}${RESET} ${col}${name}${RESET} ${status}${summary}`, `AGENT_END: ${name} ${status}`);
380
405
  return;
381
406
  }
382
407
  if (msg.subtype === 'task_progress' && msg.description) {
383
- this.push(`${this.indent()} ${a.dim}${trunc(msg.description, 70)}${a.reset}`, `PROGRESS: ${msg.description}`);
408
+ this.log(` ${this.prefix()} ${DIM}${trunc(msg.description, 70)}${RESET}`, `PROGRESS: ${msg.description}`);
384
409
  return;
385
410
  }
386
411
  if (msg.subtype === 'api_retry') {
387
- this.push(` ${a.fg.gray}${ts()}${a.reset} ${a.fg.yellow}↻${a.reset} ${a.fg.yellow}API retry ${msg.attempt}/${msg.max_retries} (${msg.error || ''})${a.reset}`);
412
+ this.log(` ${ANSI_COLORS.gray}${ts()}${RESET} ${ANSI_COLORS.yellow} API retry ${msg.attempt}/${msg.max_retries} (${msg.error || ''})${RESET}`);
388
413
  return;
389
414
  }
390
415
  }
@@ -392,6 +417,6 @@ export class TUI {
392
417
  if (msg.session_id)
393
418
  this.fileOnly(`SESSION: ${msg.session_id}`);
394
419
  }
395
- resetAgentStack() { this.agents = []; }
420
+ resetAgentStack() { this.state.agents = this.topAgent ? [this.topAgent] : []; }
396
421
  getSessionId(msg) { return msg.session_id ?? null; }
397
422
  }
@@ -2,7 +2,7 @@ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
2
  import { mkdirSync, rmSync, readFileSync } from 'node:fs';
3
3
  import { resolve, dirname } from 'node:path';
4
4
  import { fileURLToPath } from 'node:url';
5
- import { TUI } from './tui.js';
5
+ import { TUI } from './tui.mjs';
6
6
  // Helper to create partial SDK messages for testing
7
7
  function msg(partial) {
8
8
  return { parent_tool_use_id: null, uuid: 'test', session_id: 'test', ...partial };
package/package.json CHANGED
@@ -1,13 +1,12 @@
1
1
  {
2
2
  "name": "create-claude-workspace",
3
- "version": "1.1.142",
3
+ "version": "1.1.144",
4
4
  "author": "",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://gitlab.com/LadaBr/claude-starter-kit.git"
8
8
  },
9
9
  "devDependencies": {
10
- "@types/react": "^19.2.14",
11
10
  "typescript": "^5.8.0",
12
11
  "vitest": "^4.0.18"
13
12
  },
@@ -43,8 +42,6 @@
43
42
  "type": "module",
44
43
  "dependencies": {
45
44
  "@anthropic-ai/claude-agent-sdk": "^0.2.81",
46
- "@dotenvx/dotenvx": "^1.57.2",
47
- "ink": "^6.8.0",
48
- "react": "^19.2.4"
45
+ "@dotenvx/dotenvx": "^1.57.2"
49
46
  }
50
47
  }
@@ -1,304 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- // ─── Terminal UI for autonomous loop ───
3
- // Log lines: normal stdout (scrollable, persistent history)
4
- // Status bar: ink renders ONLY the last line (overwrites in place)
5
- import { useState, useEffect } from 'react';
6
- import { render, Box, Text, useApp, useInput } from 'ink';
7
- import { appendFileSync } from 'node:fs';
8
- // ─── Agent colors ───
9
- const PALETTE = ['cyan', 'magenta', 'green', 'yellow', 'blue', 'red'];
10
- const agentColorMap = new Map();
11
- let nextColor = 0;
12
- function agentColor(name) {
13
- if (!agentColorMap.has(name))
14
- agentColorMap.set(name, PALETTE[nextColor++ % PALETTE.length]);
15
- return agentColorMap.get(name);
16
- }
17
- const ANSI_COLORS = {
18
- red: '\x1b[31m', green: '\x1b[32m', yellow: '\x1b[33m', blue: '\x1b[34m',
19
- magenta: '\x1b[35m', cyan: '\x1b[36m', white: '\x1b[37m', gray: '\x1b[90m',
20
- };
21
- const RESET = '\x1b[0m';
22
- const BOLD = '\x1b[1m';
23
- const DIM = '\x1b[2m';
24
- // ─── Tool icons ───
25
- const ICONS = {
26
- Bash: '⚡', Read: '📖', Write: '✏️ ', Edit: '🔧', Glob: '🔍', Grep: '🔎',
27
- Agent: '🤖', TodoRead: '📋', TodoWrite: '📝', WebSearch: '🌐', WebFetch: '🌐', AskUserQuestion: '❓',
28
- };
29
- // ─── Helpers ───
30
- function ts() { return new Date().toLocaleTimeString('en-GB', { hour12: false }); }
31
- function trunc(s, n) { const c = s.replace(/\n/g, ' ').trim(); return c.length > n ? c.slice(0, n) + '…' : c; }
32
- function fmtTok(n) { return n < 1e3 ? `${n}` : n < 1e6 ? `${(n / 1e3).toFixed(1)}K` : `${(n / 1e6).toFixed(2)}M`; }
33
- function fmtDur(ms) { return ms < 1e3 ? `${ms}ms` : ms < 6e4 ? `${(ms / 1e3).toFixed(1)}s` : `${(ms / 6e4).toFixed(1)}m`; }
34
- // ─── Ink Status Bar (only component — renders as last line) ───
35
- function StatusBar({ state }) {
36
- const [, setTick] = useState(0);
37
- useEffect(() => { const t = setInterval(() => setTick(n => n + 1), 1000); return () => clearInterval(t); }, []);
38
- const elapsed = fmtDur(Date.now() - state.loopStart);
39
- const iterTime = state.iterStart ? fmtDur(Date.now() - state.iterStart) : '—';
40
- const pct = state.maxIter > 0 ? Math.round((state.iteration / state.maxIter) * 100) : 0;
41
- const filled = Math.round((pct / 100) * 8);
42
- const tok = fmtTok(state.tokensIn + state.tokensOut);
43
- const cur = state.agents.length > 0 ? state.agents[state.agents.length - 1] : '';
44
- return (_jsx(Box, { children: _jsxs(Text, { backgroundColor: "gray", color: "white", children: [' ', elapsed, " \u2502 Iter ", state.iteration, "/", state.maxIter, ' ', _jsx(Text, { color: "green", children: '█'.repeat(filled) }), _jsx(Text, { color: "gray", children: '░'.repeat(8 - filled) }), ' ', "\u2502 ", iterTime, " \u2502", ' ', _jsx(Text, { color: "cyan", children: state.tools }), " tools \u2502", ' ', _jsx(Text, { color: "yellow", children: tok }), " tok", cur ? _jsxs(Text, { children: [" \u2502 ", _jsx(Text, { color: agentColor(cur), bold: true, children: cur })] }) : null, state.taskName ? _jsxs(Text, { children: [" \u2502 ", _jsx(Text, { color: "cyan", children: trunc(state.taskName, 20) })] }) : null, state.paused ? _jsxs(Text, { children: [" \u2502 ", _jsx(Text, { color: "yellow", children: "\u23F8 PAUSED" })] }) : null, state.inputBuf ? _jsxs(Text, { children: [" \u203A ", state.inputBuf] }) : null, ' '] }) }));
45
- }
46
- function InkApp({ state, onInput, onHotkey }) {
47
- const { exit } = useApp();
48
- useInput((input, key) => {
49
- if (key.ctrl && input === 'c') {
50
- onHotkey?.('quit');
51
- exit();
52
- }
53
- else if (key.ctrl && input === 'z') {
54
- state.paused = !state.paused;
55
- onHotkey?.(state.paused ? 'pause' : 'resume');
56
- }
57
- else if (key.ctrl && input === 's') {
58
- onHotkey?.('stop');
59
- }
60
- else if (key.return) {
61
- if (state.inputBuf.trim())
62
- onInput?.(state.inputBuf.trim());
63
- state.inputBuf = '';
64
- }
65
- else if (key.backspace || key.delete) {
66
- state.inputBuf = state.inputBuf.slice(0, -1);
67
- }
68
- else if (input && !key.ctrl && !key.meta) {
69
- state.inputBuf += input;
70
- }
71
- });
72
- return _jsx(StatusBar, { state: state });
73
- }
74
- // ─── TUI class ───
75
- export class TUI {
76
- logFile;
77
- interactive;
78
- onInput = null;
79
- onHotkey = null;
80
- inkInstance = null;
81
- state;
82
- constructor(logFile, interactive = false) {
83
- this.logFile = logFile;
84
- this.interactive = interactive && process.stdin.isTTY === true;
85
- this.state = {
86
- iteration: 0, maxIter: 0, loopStart: Date.now(), iterStart: 0,
87
- tools: 0, tokensIn: 0, tokensOut: 0, agents: [], taskName: '',
88
- tasksDone: 0, tasksTotal: 0, paused: false, inputBuf: '',
89
- };
90
- if (this.interactive) {
91
- this.inkInstance = render(_jsx(InkApp, { state: this.state, onInput: (s) => this.onInput?.(s), onHotkey: (a) => this.onHotkey?.(a) }));
92
- }
93
- }
94
- destroy() { this.inkInstance?.unmount(); }
95
- setInputHandler(h) { this.onInput = h; }
96
- setHotkeyHandler(h) { this.onHotkey = h; }
97
- isPaused() { return this.state.paused; }
98
- // ─── Log output (stdout, NOT ink — preserves scroll history) ───
99
- log(formatted, raw) {
100
- // Pause ink rendering, write log line to stdout, resume ink
101
- if (this.interactive && this.inkInstance) {
102
- this.inkInstance.clear?.();
103
- process.stdout.write(formatted + '\n');
104
- // Re-render ink status bar below the new log line
105
- this.inkInstance.rerender(_jsx(InkApp, { state: this.state, onInput: (s) => this.onInput?.(s), onHotkey: (a) => this.onHotkey?.(a) }));
106
- }
107
- else {
108
- process.stdout.write(formatted + '\n');
109
- }
110
- const plain = raw || formatted.replace(/\x1b\[[0-9;]*m/g, '');
111
- if (this.logFile) {
112
- try {
113
- appendFileSync(this.logFile, `[${new Date().toISOString()}] ${plain}\n`);
114
- }
115
- catch { /* */ }
116
- }
117
- }
118
- fileOnly(msg) {
119
- if (this.logFile) {
120
- try {
121
- appendFileSync(this.logFile, `[${new Date().toISOString()}] ${msg}\n`);
122
- }
123
- catch { /* */ }
124
- }
125
- }
126
- // ─── Agent prefix ───
127
- prefix() {
128
- if (this.state.agents.length === 0)
129
- return '';
130
- const cur = this.state.agents[this.state.agents.length - 1];
131
- const col = ANSI_COLORS[agentColor(cur)] || '';
132
- const name = cur.length > 14 ? cur.slice(0, 14) : cur;
133
- return `${col}${name.padEnd(15)}${RESET}`;
134
- }
135
- // ─── Public API ───
136
- banner() {
137
- this.log('');
138
- this.log(` ${ANSI_COLORS.cyan}${BOLD}╔══════════════════════════════════════════════╗${RESET}`);
139
- this.log(` ${ANSI_COLORS.cyan}${BOLD}║ ${ANSI_COLORS.white}Claude Starter Kit — Autonomous Loop${ANSI_COLORS.cyan} ║${RESET}`);
140
- this.log(` ${ANSI_COLORS.cyan}${BOLD}╚══════════════════════════════════════════════╝${RESET}`);
141
- if (this.interactive) {
142
- this.log(` ${ANSI_COLORS.gray} Ctrl+Z pause │ Ctrl+S stop │ Ctrl+C quit │ Type to send input${RESET}`);
143
- }
144
- this.log('');
145
- }
146
- info(msg) { this.log(` ${this.prefix()}${ANSI_COLORS.gray}${ts()}${RESET} ${ANSI_COLORS.blue}ℹ${RESET} ${msg}`); }
147
- warn(msg) { this.log(` ${this.prefix()}${ANSI_COLORS.gray}${ts()}${RESET} ${ANSI_COLORS.yellow}⚠ ${msg}${RESET}`); }
148
- error(msg) { this.log(` ${this.prefix()}${ANSI_COLORS.gray}${ts()}${RESET} ${ANSI_COLORS.red}✗ ${msg}${RESET}`); }
149
- success(msg) { this.log(` ${this.prefix()}${ANSI_COLORS.gray}${ts()}${RESET} ${ANSI_COLORS.green}✓ ${msg}${RESET}`); }
150
- setIteration(i, max) {
151
- this.state.iteration = i;
152
- this.state.maxIter = max;
153
- this.state.iterStart = Date.now();
154
- this.state.tools = 0;
155
- this.state.tokensIn = 0;
156
- this.state.tokensOut = 0;
157
- this.state.agents = [];
158
- const pct = Math.round((i / max) * 100);
159
- const elapsed = fmtDur(Date.now() - this.state.loopStart);
160
- this.log('');
161
- this.log(` ${BOLD}━━━ Iteration ${i}/${max} ${pct}% │ ${elapsed} elapsed ━━━${RESET}`);
162
- this.log('');
163
- }
164
- setTask(name, done, total) {
165
- this.state.taskName = name;
166
- this.state.tasksDone = done;
167
- this.state.tasksTotal = total;
168
- }
169
- iterationEnd() {
170
- const s = this.state;
171
- this.log('');
172
- this.log(` ${ANSI_COLORS.gray}━━━━ ${fmtDur(Date.now() - s.iterStart)} (iter) │ ${fmtDur(Date.now() - s.loopStart)} (total) │ ${s.tools} tools │ ${fmtTok(s.tokensIn + s.tokensOut)} tokens ━━━━${RESET}`);
173
- }
174
- // ─── SDK message handler ───
175
- handleMessage(message) {
176
- switch (message.type) {
177
- case 'assistant':
178
- this.onAssistant(message);
179
- break;
180
- case 'user':
181
- this.onToolResult(message);
182
- break;
183
- case 'system':
184
- this.onSystem(message);
185
- break;
186
- case 'result':
187
- this.onResult(message);
188
- break;
189
- }
190
- }
191
- onAssistant(msg) {
192
- const content = msg.message?.content;
193
- if (!Array.isArray(content))
194
- return;
195
- if (msg.message?.usage) {
196
- this.state.tokensIn += msg.message.usage.input_tokens || 0;
197
- this.state.tokensOut += msg.message.usage.output_tokens || 0;
198
- }
199
- for (const block of content) {
200
- if (block.type === 'text' && block.text?.trim()) {
201
- this.log(` ${this.prefix()}${trunc(block.text, 300)}`, `TEXT: ${trunc(block.text, 300)}`);
202
- }
203
- if (block.type === 'tool_use')
204
- this.onToolUse(block);
205
- }
206
- }
207
- onToolUse(block) {
208
- this.state.tools++;
209
- const name = block.name || '?';
210
- const icon = ICONS[name] || '⚙️';
211
- const input = block.input || {};
212
- const pre = this.prefix();
213
- const time = `${ANSI_COLORS.gray}${ts()}${RESET}`;
214
- if (name === 'Agent') {
215
- const type = input.subagent_type || input.type || 'agent';
216
- const model = input.model || '';
217
- const desc = trunc(input.description || input.prompt || '', 50);
218
- const col = ANSI_COLORS[agentColor(type)] || '';
219
- this.state.agents.push(type);
220
- this.log(` ${pre}${time} ${icon} ${col}${BOLD}${type}${RESET}${model ? ` ${ANSI_COLORS.gray}(${model})${RESET}` : ''} ${ANSI_COLORS.gray}${desc}${RESET}`, `AGENT: ${type} ${model} — ${desc}`);
221
- return;
222
- }
223
- let detail;
224
- switch (name) {
225
- case 'Bash':
226
- detail = `${ANSI_COLORS.yellow}${trunc(input.command || '', 70)}${RESET}`;
227
- break;
228
- case 'Read':
229
- detail = `${ANSI_COLORS.cyan}${(input.file_path || '').replace(/^\/project\//, '')}${RESET}`;
230
- break;
231
- case 'Write':
232
- case 'Edit':
233
- detail = `${ANSI_COLORS.cyan}${(input.file_path || '').replace(/^\/project\//, '')}${RESET}`;
234
- break;
235
- case 'Glob':
236
- detail = `${ANSI_COLORS.cyan}${input.pattern || ''}${RESET}`;
237
- break;
238
- case 'Grep':
239
- detail = `${ANSI_COLORS.cyan}/${input.pattern || ''}/${RESET}${input.path ? ` ${ANSI_COLORS.gray}in ${input.path}${RESET}` : ''}`;
240
- break;
241
- default: detail = `${ANSI_COLORS.gray}${trunc(JSON.stringify(input), 60)}${RESET}`;
242
- }
243
- this.log(` ${pre}${time} ${icon} ${BOLD}${name}${RESET} ${detail}`, `TOOL: ${name} ${JSON.stringify(input).slice(0, 200)}`);
244
- }
245
- onToolResult(msg) {
246
- const content = msg.message?.content;
247
- if (!Array.isArray(content))
248
- return;
249
- for (const block of content) {
250
- if (block.type !== 'tool_result')
251
- continue;
252
- const output = String(block.content || '').trim();
253
- if (!output)
254
- continue;
255
- const pre = this.prefix();
256
- if (block.is_error) {
257
- this.log(` ${pre} ${ANSI_COLORS.red}✗ ${trunc(output, 100)}${RESET}`, `ERROR: ${trunc(output, 100)}`);
258
- }
259
- else if (output.length < 150) {
260
- this.log(` ${pre} ${ANSI_COLORS.green}✓${RESET} ${DIM}${trunc(output, 100)}${RESET}`, `OK: ${trunc(output, 100)}`);
261
- }
262
- else {
263
- const n = output.split('\n').length;
264
- this.log(` ${pre} ${ANSI_COLORS.green}✓${RESET} ${DIM}${n} lines${RESET}`, `OK: (${n} lines)`);
265
- }
266
- }
267
- }
268
- onSystem(msg) {
269
- if (msg.subtype === 'init') {
270
- this.log(` ${ANSI_COLORS.gray}${ts()}${RESET} ${ANSI_COLORS.cyan}⚙${RESET} Claude Code ${msg.claude_code_version || ''} │ Model: ${BOLD}${msg.model || '?'}${RESET} │ Agents: ${ANSI_COLORS.cyan}${msg.agents?.join(', ') || 'none'}${RESET}`);
271
- return;
272
- }
273
- if (msg.subtype === 'task_started') {
274
- const desc = msg.description || '';
275
- const col = ANSI_COLORS[agentColor(desc)] || '';
276
- this.state.agents.push(desc);
277
- this.log(` ${ANSI_COLORS.gray}${ts()}${RESET} 🤖 ${col}${BOLD}${desc}${RESET}`, `AGENT_START: ${desc}`);
278
- return;
279
- }
280
- if (msg.subtype === 'task_notification') {
281
- const status = msg.status || '';
282
- const name = this.state.agents.length > 0 ? this.state.agents.pop() : 'agent';
283
- const col = ANSI_COLORS[agentColor(name)] || '';
284
- const icon = status === 'completed' ? `${ANSI_COLORS.green}✓` : `${ANSI_COLORS.red}✗`;
285
- const summary = msg.summary ? ` ${ANSI_COLORS.gray}${trunc(msg.summary, 60)}${RESET}` : '';
286
- this.log(` ${ANSI_COLORS.gray}${ts()}${RESET} ${icon}${RESET} ${col}${name}${RESET} ${status}${summary}`, `AGENT_END: ${name} ${status}`);
287
- return;
288
- }
289
- if (msg.subtype === 'task_progress' && msg.description) {
290
- this.log(` ${this.prefix()} ${DIM}${trunc(msg.description, 70)}${RESET}`, `PROGRESS: ${msg.description}`);
291
- return;
292
- }
293
- if (msg.subtype === 'api_retry') {
294
- this.log(` ${ANSI_COLORS.gray}${ts()}${RESET} ${ANSI_COLORS.yellow}↻ API retry ${msg.attempt}/${msg.max_retries} (${msg.error || ''})${RESET}`);
295
- return;
296
- }
297
- }
298
- onResult(msg) {
299
- if (msg.session_id)
300
- this.fileOnly(`SESSION: ${msg.session_id}`);
301
- }
302
- resetAgentStack() { this.state.agents = []; }
303
- getSessionId(msg) { return msg.session_id ?? null; }
304
- }