@litmers/cursorflow-orchestrator 0.1.6 → 0.1.8

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,6 +1,6 @@
1
1
  "use strict";
2
2
  /**
3
- * CursorFlow monitor command
3
+ * CursorFlow interactive monitor command
4
4
  */
5
5
  var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
6
6
  if (k2 === undefined) k2 = k;
@@ -37,174 +37,669 @@ var __importStar = (this && this.__importStar) || (function () {
37
37
  })();
38
38
  const fs = __importStar(require("fs"));
39
39
  const path = __importStar(require("path"));
40
- const logger = __importStar(require("../utils/logger"));
40
+ const readline = __importStar(require("readline"));
41
41
  const state_1 = require("../utils/state");
42
42
  const config_1 = require("../utils/config");
43
- function parseArgs(args) {
44
- const watch = args.includes('--watch');
45
- const intervalIdx = args.indexOf('--interval');
46
- const interval = intervalIdx >= 0 ? parseInt(args[intervalIdx + 1] || '2') || 2 : 2;
47
- // Find run directory (first non-option argument)
48
- const runDir = args.find(arg => !arg.startsWith('--') && args.indexOf(arg) !== intervalIdx + 1);
49
- return {
50
- runDir,
51
- watch,
52
- interval,
53
- };
43
+ var View;
44
+ (function (View) {
45
+ View[View["LIST"] = 0] = "LIST";
46
+ View[View["LANE_DETAIL"] = 1] = "LANE_DETAIL";
47
+ View[View["MESSAGE_DETAIL"] = 2] = "MESSAGE_DETAIL";
48
+ View[View["FLOW"] = 3] = "FLOW";
49
+ View[View["TERMINAL"] = 4] = "TERMINAL";
50
+ View[View["INTERVENE"] = 5] = "INTERVENE";
51
+ })(View || (View = {}));
52
+ class InteractiveMonitor {
53
+ runDir;
54
+ interval;
55
+ view = View.LIST;
56
+ selectedLaneIndex = 0;
57
+ selectedMessageIndex = 0;
58
+ selectedLaneName = null;
59
+ lanes = [];
60
+ currentLogs = [];
61
+ timer = null;
62
+ scrollOffset = 0;
63
+ terminalScrollOffset = 0;
64
+ lastTerminalTotalLines = 0;
65
+ interventionInput = '';
66
+ constructor(runDir, interval) {
67
+ this.runDir = runDir;
68
+ this.interval = interval;
69
+ }
70
+ async start() {
71
+ this.setupTerminal();
72
+ this.refresh();
73
+ this.timer = setInterval(() => this.refresh(), this.interval * 1000);
74
+ }
75
+ setupTerminal() {
76
+ if (process.stdin.isTTY) {
77
+ process.stdin.setRawMode(true);
78
+ }
79
+ readline.emitKeypressEvents(process.stdin);
80
+ process.stdin.on('keypress', (str, key) => {
81
+ // Handle Ctrl+C
82
+ if (key && key.ctrl && key.name === 'c') {
83
+ this.stop();
84
+ return;
85
+ }
86
+ // Safeguard against missing key object
87
+ const keyName = key ? key.name : str;
88
+ if (this.view === View.LIST) {
89
+ this.handleListKey(keyName);
90
+ }
91
+ else if (this.view === View.LANE_DETAIL) {
92
+ this.handleDetailKey(keyName);
93
+ }
94
+ else if (this.view === View.FLOW) {
95
+ this.handleFlowKey(keyName);
96
+ }
97
+ else if (this.view === View.TERMINAL) {
98
+ this.handleTerminalKey(keyName);
99
+ }
100
+ else if (this.view === View.INTERVENE) {
101
+ this.handleInterveneKey(str, key);
102
+ }
103
+ else {
104
+ this.handleMessageDetailKey(keyName);
105
+ }
106
+ });
107
+ // Hide cursor
108
+ process.stdout.write('\x1B[?25l');
109
+ }
110
+ stop() {
111
+ if (this.timer)
112
+ clearInterval(this.timer);
113
+ // Show cursor and clear screen
114
+ process.stdout.write('\x1B[?25h');
115
+ process.stdout.write('\x1Bc');
116
+ console.log('\nšŸ‘‹ Monitoring stopped\n');
117
+ process.exit(0);
118
+ }
119
+ handleListKey(key) {
120
+ switch (key) {
121
+ case 'up':
122
+ this.selectedLaneIndex = Math.max(0, this.selectedLaneIndex - 1);
123
+ this.render();
124
+ break;
125
+ case 'down':
126
+ this.selectedLaneIndex = Math.min(this.lanes.length - 1, this.selectedLaneIndex + 1);
127
+ this.render();
128
+ break;
129
+ case 'right':
130
+ case 'return':
131
+ case 'enter':
132
+ if (this.lanes[this.selectedLaneIndex]) {
133
+ this.selectedLaneName = this.lanes[this.selectedLaneIndex].name;
134
+ this.view = View.LANE_DETAIL;
135
+ this.selectedMessageIndex = 0;
136
+ this.scrollOffset = 0;
137
+ this.refreshLogs();
138
+ this.render();
139
+ }
140
+ break;
141
+ case 'left':
142
+ case 'f':
143
+ this.view = View.FLOW;
144
+ this.render();
145
+ break;
146
+ case 'q':
147
+ this.stop();
148
+ break;
149
+ }
150
+ }
151
+ handleDetailKey(key) {
152
+ switch (key) {
153
+ case 'up':
154
+ this.selectedMessageIndex = Math.max(0, this.selectedMessageIndex - 1);
155
+ this.render();
156
+ break;
157
+ case 'down':
158
+ this.selectedMessageIndex = Math.min(this.currentLogs.length - 1, this.selectedMessageIndex + 1);
159
+ this.render();
160
+ break;
161
+ case 'right':
162
+ case 'return':
163
+ case 'enter':
164
+ if (this.currentLogs[this.selectedMessageIndex]) {
165
+ this.view = View.MESSAGE_DETAIL;
166
+ this.render();
167
+ }
168
+ break;
169
+ case 't':
170
+ this.view = View.TERMINAL;
171
+ this.terminalScrollOffset = 0;
172
+ this.render();
173
+ break;
174
+ case 'i':
175
+ const lane = this.lanes.find(l => l.name === this.selectedLaneName);
176
+ if (lane) {
177
+ const status = this.getLaneStatus(lane.path, lane.name);
178
+ if (status.status === 'running') {
179
+ this.view = View.INTERVENE;
180
+ this.interventionInput = '';
181
+ this.render();
182
+ }
183
+ else {
184
+ // Show a temporary message that it's only for running lanes
185
+ console.log(`\n\x1b[31m Intervention is only available for RUNNING lanes.\x1b[0m`);
186
+ setTimeout(() => this.render(), 1500);
187
+ }
188
+ }
189
+ break;
190
+ case 'escape':
191
+ case 'backspace':
192
+ case 'left':
193
+ this.view = View.LIST;
194
+ this.selectedLaneName = null;
195
+ this.render();
196
+ break;
197
+ case 'q':
198
+ this.stop();
199
+ break;
200
+ }
201
+ }
202
+ handleMessageDetailKey(key) {
203
+ switch (key) {
204
+ case 'escape':
205
+ case 'backspace':
206
+ case 'left':
207
+ this.view = View.LANE_DETAIL;
208
+ this.render();
209
+ break;
210
+ case 'q':
211
+ this.stop();
212
+ break;
213
+ }
214
+ }
215
+ handleTerminalKey(key) {
216
+ switch (key) {
217
+ case 'up':
218
+ this.terminalScrollOffset++;
219
+ this.render();
220
+ break;
221
+ case 'down':
222
+ this.terminalScrollOffset = Math.max(0, this.terminalScrollOffset - 1);
223
+ this.render();
224
+ break;
225
+ case 't':
226
+ case 'escape':
227
+ case 'backspace':
228
+ case 'left':
229
+ this.view = View.LANE_DETAIL;
230
+ this.render();
231
+ break;
232
+ case 'i':
233
+ this.view = View.INTERVENE;
234
+ this.interventionInput = '';
235
+ this.render();
236
+ break;
237
+ case 'q':
238
+ this.stop();
239
+ break;
240
+ }
241
+ }
242
+ handleInterveneKey(str, key) {
243
+ if (key.name === 'return' || key.name === 'enter') {
244
+ if (this.interventionInput.trim()) {
245
+ this.sendIntervention(this.interventionInput.trim());
246
+ }
247
+ this.view = View.LANE_DETAIL;
248
+ this.render();
249
+ }
250
+ else if (key.name === 'escape') {
251
+ this.view = View.LANE_DETAIL;
252
+ this.render();
253
+ }
254
+ else if (key.name === 'backspace') {
255
+ this.interventionInput = this.interventionInput.slice(0, -1);
256
+ this.render();
257
+ }
258
+ else if (str && str.length === 1) {
259
+ this.interventionInput += str;
260
+ this.render();
261
+ }
262
+ }
263
+ sendIntervention(message) {
264
+ if (!this.selectedLaneName)
265
+ return;
266
+ const lane = this.lanes.find(l => l.name === this.selectedLaneName);
267
+ if (!lane)
268
+ return;
269
+ // Write to intervention.txt which runner.ts is watching
270
+ const interventionPath = path.join(path.dirname(lane.path), 'intervention.txt');
271
+ fs.writeFileSync(interventionPath, message, 'utf8');
272
+ // Also log it to the conversation
273
+ const convoPath = path.join(lane.path, 'conversation.jsonl');
274
+ const entry = {
275
+ timestamp: new Date().toISOString(),
276
+ role: 'user',
277
+ task: 'INTERVENTION',
278
+ fullText: `[HUMAN INTERVENTION]: ${message}`,
279
+ textLength: message.length + 20,
280
+ model: 'manual'
281
+ };
282
+ fs.appendFileSync(convoPath, JSON.stringify(entry) + '\n', 'utf8');
283
+ }
284
+ handleFlowKey(key) {
285
+ switch (key) {
286
+ case 'f':
287
+ case 'escape':
288
+ case 'backspace':
289
+ case 'right':
290
+ case 'return':
291
+ case 'enter':
292
+ case 'left':
293
+ this.view = View.LIST;
294
+ this.render();
295
+ break;
296
+ case 'q':
297
+ this.stop();
298
+ break;
299
+ }
300
+ }
301
+ refreshLogs() {
302
+ if (!this.selectedLaneName)
303
+ return;
304
+ const lane = this.lanes.find(l => l.name === this.selectedLaneName);
305
+ if (!lane)
306
+ return;
307
+ const convoPath = path.join(lane.path, 'conversation.jsonl');
308
+ this.currentLogs = (0, state_1.readLog)(convoPath);
309
+ // Keep selection in bounds after refresh
310
+ if (this.selectedMessageIndex >= this.currentLogs.length) {
311
+ this.selectedMessageIndex = Math.max(0, this.currentLogs.length - 1);
312
+ }
313
+ }
314
+ refresh() {
315
+ this.lanes = this.listLanesWithDeps(this.runDir);
316
+ if (this.view !== View.LIST) {
317
+ this.refreshLogs();
318
+ }
319
+ this.render();
320
+ }
321
+ render() {
322
+ // Clear screen
323
+ process.stdout.write('\x1Bc');
324
+ switch (this.view) {
325
+ case View.LIST:
326
+ this.renderList();
327
+ break;
328
+ case View.LANE_DETAIL:
329
+ this.renderLaneDetail();
330
+ break;
331
+ case View.MESSAGE_DETAIL:
332
+ this.renderMessageDetail();
333
+ break;
334
+ case View.FLOW:
335
+ this.renderFlow();
336
+ break;
337
+ case View.TERMINAL:
338
+ this.renderTerminal();
339
+ break;
340
+ case View.INTERVENE:
341
+ this.renderIntervene();
342
+ break;
343
+ }
344
+ }
345
+ renderList() {
346
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
347
+ console.log(`šŸ“Š CursorFlow Monitor - Run: ${path.basename(this.runDir)}`);
348
+ console.log(`šŸ•’ Updated: ${new Date().toLocaleTimeString()} | [↑/↓/→] Nav [←] Flow [Q] Quit`);
349
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
350
+ if (this.lanes.length === 0) {
351
+ console.log(' No lanes found\n');
352
+ return;
353
+ }
354
+ const laneStatuses = {};
355
+ this.lanes.forEach(l => laneStatuses[l.name] = this.getLaneStatus(l.path, l.name));
356
+ const maxNameLen = Math.max(...this.lanes.map(l => l.name.length), 15);
357
+ console.log(` ${'Lane'.padEnd(maxNameLen)} Status Progress Time Tasks Next Action`);
358
+ console.log(` ${'─'.repeat(maxNameLen)} ${'─'.repeat(18)} ${'─'.repeat(8)} ${'─'.repeat(8)} ${'─'.repeat(6)} ${'─'.repeat(20)}`);
359
+ this.lanes.forEach((lane, i) => {
360
+ const isSelected = i === this.selectedLaneIndex;
361
+ const status = laneStatuses[lane.name];
362
+ const statusIcon = this.getStatusIcon(status.status);
363
+ const statusText = `${statusIcon} ${status.status}`.padEnd(18);
364
+ const progressText = status.progress.padEnd(8);
365
+ const timeText = this.formatDuration(status.duration).padEnd(8);
366
+ let tasksDisplay = '-';
367
+ if (typeof status.totalTasks === 'number') {
368
+ tasksDisplay = `${status.currentTask}/${status.totalTasks}`;
369
+ }
370
+ const tasksText = tasksDisplay.padEnd(6);
371
+ // Determine "Next Action"
372
+ let nextAction = '-';
373
+ if (status.status === 'completed') {
374
+ const dependents = this.lanes.filter(l => laneStatuses[l.name].dependsOn.includes(lane.name));
375
+ if (dependents.length > 0) {
376
+ nextAction = `Unlock: ${dependents.map(d => d.name).join(', ')}`;
377
+ }
378
+ else {
379
+ nextAction = 'šŸ Done';
380
+ }
381
+ }
382
+ else if (status.status === 'waiting') {
383
+ const missingDeps = status.dependsOn.filter((d) => laneStatuses[d] && laneStatuses[d].status !== 'completed');
384
+ nextAction = `Wait for: ${missingDeps.join(', ')}`;
385
+ }
386
+ else if (status.status === 'running') {
387
+ nextAction = 'šŸš€ Working...';
388
+ }
389
+ const prefix = isSelected ? ' ā–¶ ' : ' ';
390
+ const line = `${prefix}${lane.name.padEnd(maxNameLen)} ${statusText} ${progressText} ${timeText} ${tasksText} ${nextAction}`;
391
+ if (isSelected) {
392
+ process.stdout.write(`\x1b[36m${line}\x1b[0m\n`);
393
+ }
394
+ else {
395
+ process.stdout.write(`${line}\n`);
396
+ }
397
+ });
398
+ console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
399
+ }
400
+ renderLaneDetail() {
401
+ const lane = this.lanes.find(l => l.name === this.selectedLaneName);
402
+ if (!lane) {
403
+ this.view = View.LIST;
404
+ this.render();
405
+ return;
406
+ }
407
+ const status = this.getLaneStatus(lane.path, lane.name);
408
+ const logPath = path.join(lane.path, 'terminal.log');
409
+ let liveLog = '(No live terminal output)';
410
+ if (fs.existsSync(logPath)) {
411
+ const content = fs.readFileSync(logPath, 'utf8');
412
+ liveLog = content.split('\n').slice(-15).join('\n');
413
+ }
414
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
415
+ console.log(`šŸ” Lane: ${lane.name}`);
416
+ console.log(`šŸ•’ Updated: ${new Date().toLocaleTimeString()} | [↑/↓/→] Browse History [Esc/←] Back`);
417
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
418
+ process.stdout.write(` Status: ${this.getStatusIcon(status.status)} ${status.status}\n`);
419
+ process.stdout.write(` Progress: ${status.progress} (${status.currentTask}/${status.totalTasks} tasks)\n`);
420
+ process.stdout.write(` Time: ${this.formatDuration(status.duration)}\n`);
421
+ process.stdout.write(` Branch: ${status.pipelineBranch}\n`);
422
+ process.stdout.write(` Chat ID: ${status.chatId}\n`);
423
+ process.stdout.write(` Depends: ${status.dependsOn.join(', ') || 'None'}\n`);
424
+ if (status.error) {
425
+ process.stdout.write(`\x1b[31m Error: ${status.error}\x1b[0m\n`);
426
+ }
427
+ console.log('\nšŸ–„ļø Live Terminal Output (Last 15 lines):');
428
+ console.log('─'.repeat(80));
429
+ console.log(`\x1b[90m${liveLog}\x1b[0m`);
430
+ console.log('\nšŸ’¬ Conversation History (Select to see full details):');
431
+ console.log('─'.repeat(80));
432
+ process.stdout.write(' [↑/↓] Browse | [→/Enter] Full Msg | [I] Intervene | [T] Live Terminal | [Esc/←] Back\n\n');
433
+ if (this.currentLogs.length === 0) {
434
+ console.log(' (No messages yet)');
435
+ }
436
+ else {
437
+ // Simple windowed view for long histories
438
+ const maxVisible = 15; // Number of messages to show
439
+ if (this.selectedMessageIndex < this.scrollOffset) {
440
+ this.scrollOffset = this.selectedMessageIndex;
441
+ }
442
+ else if (this.selectedMessageIndex >= this.scrollOffset + maxVisible) {
443
+ this.scrollOffset = this.selectedMessageIndex - maxVisible + 1;
444
+ }
445
+ const visibleLogs = this.currentLogs.slice(this.scrollOffset, this.scrollOffset + maxVisible);
446
+ visibleLogs.forEach((log, i) => {
447
+ const actualIndex = i + this.scrollOffset;
448
+ const isSelected = actualIndex === this.selectedMessageIndex;
449
+ const roleColor = log.role === 'user' ? '\x1b[33m' : log.role === 'reviewer' ? '\x1b[35m' : '\x1b[32m';
450
+ const role = log.role.toUpperCase().padEnd(10);
451
+ const prefix = isSelected ? 'ā–¶ ' : ' ';
452
+ const header = `${prefix}${roleColor}${role}\x1b[0m [${new Date(log.timestamp).toLocaleTimeString()}]`;
453
+ if (isSelected) {
454
+ process.stdout.write(`\x1b[48;5;236m${header}\x1b[0m\n`);
455
+ }
456
+ else {
457
+ process.stdout.write(`${header}\n`);
458
+ }
459
+ const lines = log.fullText.split('\n').filter(l => l.trim());
460
+ const preview = lines[0]?.substring(0, 70) || '...';
461
+ process.stdout.write(` ${preview}${log.fullText.length > 70 ? '...' : ''}\n\n`);
462
+ });
463
+ if (this.currentLogs.length > maxVisible) {
464
+ console.log(` -- (${this.currentLogs.length - maxVisible} more messages, use ↑/↓ to scroll) --`);
465
+ }
466
+ }
467
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
468
+ }
469
+ renderMessageDetail() {
470
+ const log = this.currentLogs[this.selectedMessageIndex];
471
+ if (!log) {
472
+ this.view = View.LANE_DETAIL;
473
+ this.render();
474
+ return;
475
+ }
476
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
477
+ console.log(`šŸ“„ Full Message Detail - ${log.role.toUpperCase()}`);
478
+ console.log(`šŸ•’ ${new Date(log.timestamp).toLocaleString()} | [Esc/←] Back to History`);
479
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
480
+ const roleColor = log.role === 'user' ? '\x1b[33m' : log.role === 'reviewer' ? '\x1b[35m' : '\x1b[32m';
481
+ process.stdout.write(`${roleColor}ROLE: ${log.role.toUpperCase()}\x1b[0m\n`);
482
+ if (log.model)
483
+ process.stdout.write(`MODEL: ${log.model}\n`);
484
+ if (log.task)
485
+ process.stdout.write(`TASK: ${log.task}\n`);
486
+ console.log('─'.repeat(40));
487
+ console.log(log.fullText);
488
+ console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
489
+ }
490
+ renderFlow() {
491
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
492
+ console.log(`ā›“ļø Task Dependency Flow`);
493
+ console.log(`šŸ•’ Updated: ${new Date().toLocaleTimeString()} | [→/Enter/Esc] Back to List`);
494
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
495
+ const laneMap = new Map();
496
+ this.lanes.forEach(lane => {
497
+ laneMap.set(lane.name, this.getLaneStatus(lane.path, lane.name));
498
+ });
499
+ // Enhanced visualization with box-like structure and clear connections
500
+ this.lanes.forEach(lane => {
501
+ const status = laneMap.get(lane.name);
502
+ const statusIcon = this.getStatusIcon(status.status);
503
+ let statusColor = '\x1b[90m'; // Grey for pending/waiting
504
+ if (status.status === 'completed')
505
+ statusColor = '\x1b[32m'; // Green
506
+ if (status.status === 'running')
507
+ statusColor = '\x1b[36m'; // Cyan
508
+ if (status.status === 'failed')
509
+ statusColor = '\x1b[31m'; // Red
510
+ // Render the node
511
+ const nodeText = `[ ${statusIcon} ${lane.name.padEnd(18)} ]`;
512
+ process.stdout.write(` ${statusColor}${nodeText}\x1b[0m`);
513
+ // Render dependencies
514
+ if (status.dependsOn && status.dependsOn.length > 0) {
515
+ process.stdout.write(` \x1b[90m◀───\x1b[0m \x1b[33m( ${status.dependsOn.join(', ')} )\x1b[0m`);
516
+ }
517
+ process.stdout.write('\n');
518
+ });
519
+ console.log('\n\x1b[90m (Lanes wait for their dependencies to complete before starting)\x1b[0m');
520
+ console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
521
+ }
522
+ renderTerminal() {
523
+ const lane = this.lanes.find(l => l.name === this.selectedLaneName);
524
+ if (!lane) {
525
+ this.view = View.LIST;
526
+ this.render();
527
+ return;
528
+ }
529
+ const logPath = path.join(lane.path, 'terminal.log');
530
+ let logLines = [];
531
+ if (fs.existsSync(logPath)) {
532
+ const content = fs.readFileSync(logPath, 'utf8');
533
+ logLines = content.split('\n');
534
+ }
535
+ const maxVisible = 40;
536
+ const totalLines = logLines.length;
537
+ // Sticky scroll logic: if new lines arrived and we are already scrolled up,
538
+ // increase the offset to stay on the same content.
539
+ if (this.terminalScrollOffset > 0 && this.lastTerminalTotalLines > 0 && totalLines > this.lastTerminalTotalLines) {
540
+ this.terminalScrollOffset += (totalLines - this.lastTerminalTotalLines);
541
+ }
542
+ this.lastTerminalTotalLines = totalLines;
543
+ // Clamp scroll offset
544
+ const maxScroll = Math.max(0, totalLines - maxVisible);
545
+ if (this.terminalScrollOffset > maxScroll) {
546
+ this.terminalScrollOffset = maxScroll;
547
+ }
548
+ // Slice based on scroll (0 means bottom, >0 means scrolled up)
549
+ const end = totalLines - this.terminalScrollOffset;
550
+ const start = Math.max(0, end - maxVisible);
551
+ const visibleLines = logLines.slice(start, end);
552
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
553
+ console.log(`šŸ–„ļø Full Live Terminal: ${lane.name}`);
554
+ console.log(`šŸ•’ Streaming... | [↑/↓] Scroll (${this.terminalScrollOffset > 0 ? `Scrolled Up ${this.terminalScrollOffset}` : 'Bottom'}) | [I] Intervene | [T/Esc/←] Back`);
555
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
556
+ visibleLines.forEach(line => {
557
+ let formattedLine = line;
558
+ // Highlight human intervention
559
+ if (line.includes('[HUMAN INTERVENTION]') || line.includes('Injecting intervention:')) {
560
+ formattedLine = `\x1b[33m\x1b[1m${line}\x1b[0m`;
561
+ }
562
+ // Highlight agent execution starts
563
+ else if (line.includes('Executing cursor-agent')) {
564
+ formattedLine = `\x1b[36m\x1b[1m${line}\x1b[0m`;
565
+ }
566
+ // Highlight task headers
567
+ else if (line.includes('=== Task:')) {
568
+ formattedLine = `\x1b[32m\x1b[1m${line}\x1b[0m`;
569
+ }
570
+ // Highlight errors
571
+ else if (line.toLowerCase().includes('error') || line.toLowerCase().includes('failed')) {
572
+ formattedLine = `\x1b[31m${line}\x1b[0m`;
573
+ }
574
+ process.stdout.write(` ${formattedLine}\n`);
575
+ });
576
+ console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
577
+ }
578
+ renderIntervene() {
579
+ console.clear();
580
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
581
+ console.log(`šŸ™‹ HUMAN INTERVENTION: ${this.selectedLaneName}`);
582
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
583
+ console.log('\n Type your message to the agent. This will be sent as a direct prompt.');
584
+ console.log(` Press \x1b[1mENTER\x1b[0m to send, \x1b[1mESC\x1b[0m to cancel.\n`);
585
+ console.log(`\x1b[33m > \x1b[0m${this.interventionInput}\x1b[37mā–ˆ\x1b[0m`);
586
+ console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
587
+ }
588
+ listLanesWithDeps(runDir) {
589
+ const lanesDir = path.join(runDir, 'lanes');
590
+ if (!fs.existsSync(lanesDir))
591
+ return [];
592
+ const config = (0, config_1.loadConfig)();
593
+ const tasksDir = path.join(config.projectRoot, config.tasksDir);
594
+ const laneConfigs = this.listLaneFilesFromDir(tasksDir);
595
+ return fs.readdirSync(lanesDir)
596
+ .filter(d => fs.statSync(path.join(lanesDir, d)).isDirectory())
597
+ .map(name => {
598
+ const config = laneConfigs.find(c => c.name === name);
599
+ return {
600
+ name,
601
+ path: path.join(lanesDir, name),
602
+ dependsOn: config?.dependsOn || [],
603
+ };
604
+ });
605
+ }
606
+ listLaneFilesFromDir(tasksDir) {
607
+ if (!fs.existsSync(tasksDir))
608
+ return [];
609
+ return fs.readdirSync(tasksDir)
610
+ .filter(f => f.endsWith('.json'))
611
+ .map(f => {
612
+ const filePath = path.join(tasksDir, f);
613
+ try {
614
+ const config = JSON.parse(fs.readFileSync(filePath, 'utf8'));
615
+ return { name: path.basename(f, '.json'), dependsOn: config.dependsOn || [] };
616
+ }
617
+ catch {
618
+ return { name: path.basename(f, '.json'), dependsOn: [] };
619
+ }
620
+ });
621
+ }
622
+ getLaneStatus(lanePath, laneName) {
623
+ const statePath = path.join(lanePath, 'state.json');
624
+ const state = (0, state_1.loadState)(statePath);
625
+ const laneInfo = this.lanes.find(l => l.name === laneName);
626
+ const dependsOn = state?.dependsOn || laneInfo?.dependsOn || [];
627
+ if (!state) {
628
+ return { status: 'pending', currentTask: 0, totalTasks: '?', progress: '0%', dependsOn, duration: 0, pipelineBranch: '-', chatId: '-' };
629
+ }
630
+ const progress = state.totalTasks > 0 ? Math.round((state.currentTaskIndex / state.totalTasks) * 100) : 0;
631
+ const duration = state.startTime ? (state.endTime
632
+ ? state.endTime - state.startTime
633
+ : (state.status === 'running' || state.status === 'reviewing' ? Date.now() - state.startTime : 0)) : 0;
634
+ return {
635
+ status: state.status || 'unknown',
636
+ currentTask: state.currentTaskIndex || 0,
637
+ totalTasks: state.totalTasks || '?',
638
+ progress: `${progress}%`,
639
+ pipelineBranch: state.pipelineBranch || '-',
640
+ chatId: state.chatId || '-',
641
+ dependsOn,
642
+ duration,
643
+ error: state.error,
644
+ };
645
+ }
646
+ formatDuration(ms) {
647
+ if (ms <= 0)
648
+ return '-';
649
+ const seconds = Math.floor((ms / 1000) % 60);
650
+ const minutes = Math.floor((ms / (1000 * 60)) % 60);
651
+ const hours = Math.floor(ms / (1000 * 60 * 60));
652
+ if (hours > 0)
653
+ return `${hours}h ${minutes}m`;
654
+ if (minutes > 0)
655
+ return `${minutes}m ${seconds}s`;
656
+ return `${seconds}s`;
657
+ }
658
+ getStatusIcon(status) {
659
+ const icons = {
660
+ 'running': 'šŸ”„',
661
+ 'waiting': 'ā³',
662
+ 'completed': 'āœ…',
663
+ 'failed': 'āŒ',
664
+ 'blocked_dependency': '🚫',
665
+ 'pending': '⚪',
666
+ 'reviewing': 'šŸ‘€',
667
+ };
668
+ return icons[status] || 'ā“';
669
+ }
54
670
  }
55
671
  /**
56
672
  * Find the latest run directory
57
673
  */
58
674
  function findLatestRunDir(logsDir) {
59
675
  const runsDir = path.join(logsDir, 'runs');
60
- if (!fs.existsSync(runsDir)) {
676
+ if (!fs.existsSync(runsDir))
61
677
  return null;
62
- }
63
678
  const runs = fs.readdirSync(runsDir)
64
679
  .filter(d => d.startsWith('run-'))
65
- .map(d => ({
66
- name: d,
67
- path: path.join(runsDir, d),
68
- mtime: fs.statSync(path.join(runsDir, d)).mtime.getTime(),
69
- }))
680
+ .map(d => ({ name: d, path: path.join(runsDir, d), mtime: fs.statSync(path.join(runsDir, d)).mtime.getTime() }))
70
681
  .sort((a, b) => b.mtime - a.mtime);
71
682
  return runs.length > 0 ? runs[0].path : null;
72
683
  }
73
- /**
74
- * List all lanes in a run directory
75
- */
76
- function listLanes(runDir) {
77
- const lanesDir = path.join(runDir, 'lanes');
78
- if (!fs.existsSync(lanesDir)) {
79
- return [];
80
- }
81
- return fs.readdirSync(lanesDir)
82
- .filter(d => {
83
- const stat = fs.statSync(path.join(lanesDir, d));
84
- return stat.isDirectory();
85
- })
86
- .map(name => ({
87
- name,
88
- path: path.join(lanesDir, name),
89
- }));
90
- }
91
- /**
92
- * Get lane status
93
- */
94
- function getLaneStatus(lanePath) {
95
- const statePath = path.join(lanePath, 'state.json');
96
- const state = (0, state_1.loadState)(statePath);
97
- if (!state) {
98
- return {
99
- status: 'no state',
100
- currentTask: '-',
101
- totalTasks: '?',
102
- progress: '0%',
103
- };
104
- }
105
- const progress = state.totalTasks > 0
106
- ? Math.round((state.currentTaskIndex / state.totalTasks) * 100)
107
- : 0;
108
- return {
109
- status: state.status || 'unknown',
110
- currentTask: (state.currentTaskIndex || 0) + 1,
111
- totalTasks: state.totalTasks || '?',
112
- progress: `${progress}%`,
113
- pipelineBranch: state.pipelineBranch || '-',
114
- chatId: state.chatId || '-',
115
- };
116
- }
117
- /**
118
- * Get status icon
119
- */
120
- function getStatusIcon(status) {
121
- const icons = {
122
- 'running': 'šŸ”„',
123
- 'completed': 'āœ…',
124
- 'failed': 'āŒ',
125
- 'blocked_dependency': '🚫',
126
- 'no state': '⚪',
127
- };
128
- return icons[status] || 'ā“';
129
- }
130
- /**
131
- * Display lane status table
132
- */
133
- function displayStatus(runDir, lanes) {
134
- console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
135
- console.log(`šŸ“Š Run: ${path.basename(runDir)}`);
136
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
137
- if (lanes.length === 0) {
138
- console.log(' No lanes found\n');
139
- return;
140
- }
141
- // Calculate column widths
142
- const maxNameLen = Math.max(...lanes.map(l => l.name.length), 10);
143
- // Header
144
- console.log(` ${'Lane'.padEnd(maxNameLen)} Status Progress Tasks`);
145
- console.log(` ${'─'.repeat(maxNameLen)} ${'─'.repeat(18)} ${'─'.repeat(8)} ${'─'.repeat(10)}`);
146
- // Lanes
147
- for (const lane of lanes) {
148
- const status = getLaneStatus(lane.path);
149
- const statusIcon = getStatusIcon(status.status);
150
- const statusText = `${statusIcon} ${status.status}`.padEnd(18);
151
- const progressText = status.progress.padEnd(8);
152
- const tasksText = `${status.currentTask}/${status.totalTasks}`;
153
- console.log(` ${lane.name.padEnd(maxNameLen)} ${statusText} ${progressText} ${tasksText}`);
154
- }
155
- console.log();
156
- }
157
684
  /**
158
685
  * Monitor lanes
159
686
  */
160
687
  async function monitor(args) {
161
- logger.section('šŸ“” Monitoring Lane Execution');
162
- const options = parseArgs(args);
688
+ const watchIdx = args.indexOf('--watch');
689
+ const intervalIdx = args.indexOf('--interval');
690
+ const interval = intervalIdx >= 0 ? parseInt(args[intervalIdx + 1] || '2') || 2 : 2;
691
+ const runDirArg = args.find(arg => !arg.startsWith('--') && args.indexOf(arg) !== intervalIdx + 1);
163
692
  const config = (0, config_1.loadConfig)();
164
- // Determine run directory
165
- let runDir = options.runDir;
693
+ let runDir = runDirArg;
166
694
  if (!runDir || runDir === 'latest') {
167
695
  runDir = findLatestRunDir(config.logsDir) || undefined;
168
- if (!runDir) {
169
- logger.error(`Runs directory: ${path.join(config.logsDir, 'runs')}`);
696
+ if (!runDir)
170
697
  throw new Error('No run directories found');
171
- }
172
- logger.info(`Using latest run: ${path.basename(runDir)}`);
173
698
  }
174
- if (!fs.existsSync(runDir)) {
699
+ if (!fs.existsSync(runDir))
175
700
  throw new Error(`Run directory not found: ${runDir}`);
176
- }
177
- // Watch mode
178
- if (options.watch) {
179
- logger.info(`Watch mode: every ${options.interval}s (Ctrl+C to stop)\n`);
180
- let iteration = 0;
181
- const refresh = () => {
182
- if (iteration > 0) {
183
- // Clear screen
184
- process.stdout.write('\x1Bc');
185
- }
186
- const lanes = listLanes(runDir);
187
- displayStatus(runDir, lanes);
188
- iteration++;
189
- };
190
- // Initial display
191
- refresh();
192
- // Set up interval
193
- const intervalId = setInterval(refresh, options.interval * 1000);
194
- // Handle Ctrl+C
195
- return new Promise((_, reject) => {
196
- process.on('SIGINT', () => {
197
- clearInterval(intervalId);
198
- console.log('\nšŸ‘‹ Monitoring stopped\n');
199
- process.exit(0);
200
- });
201
- });
202
- }
203
- else {
204
- // Single shot
205
- const lanes = listLanes(runDir);
206
- displayStatus(runDir, lanes);
207
- }
701
+ const monitor = new InteractiveMonitor(runDir, interval);
702
+ await monitor.start();
208
703
  }
209
704
  module.exports = monitor;
210
705
  //# sourceMappingURL=monitor.js.map