@litmers/cursorflow-orchestrator 0.1.20 → 0.1.26

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.
Files changed (224) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/commands/cursorflow-clean.md +19 -0
  3. package/commands/cursorflow-runs.md +59 -0
  4. package/commands/cursorflow-stop.md +55 -0
  5. package/dist/cli/clean.js +171 -0
  6. package/dist/cli/clean.js.map +1 -1
  7. package/dist/cli/index.js +7 -0
  8. package/dist/cli/index.js.map +1 -1
  9. package/dist/cli/init.js +1 -1
  10. package/dist/cli/init.js.map +1 -1
  11. package/dist/cli/logs.js +83 -42
  12. package/dist/cli/logs.js.map +1 -1
  13. package/dist/cli/monitor.d.ts +7 -0
  14. package/dist/cli/monitor.js +1007 -189
  15. package/dist/cli/monitor.js.map +1 -1
  16. package/dist/cli/prepare.js +4 -3
  17. package/dist/cli/prepare.js.map +1 -1
  18. package/dist/cli/resume.js +188 -236
  19. package/dist/cli/resume.js.map +1 -1
  20. package/dist/cli/run.js +8 -3
  21. package/dist/cli/run.js.map +1 -1
  22. package/dist/cli/runs.d.ts +5 -0
  23. package/dist/cli/runs.js +214 -0
  24. package/dist/cli/runs.js.map +1 -0
  25. package/dist/cli/setup-commands.js +0 -0
  26. package/dist/cli/signal.js +1 -1
  27. package/dist/cli/signal.js.map +1 -1
  28. package/dist/cli/stop.d.ts +5 -0
  29. package/dist/cli/stop.js +215 -0
  30. package/dist/cli/stop.js.map +1 -0
  31. package/dist/cli/tasks.d.ts +10 -0
  32. package/dist/cli/tasks.js +165 -0
  33. package/dist/cli/tasks.js.map +1 -0
  34. package/dist/core/auto-recovery.d.ts +212 -0
  35. package/dist/core/auto-recovery.js +737 -0
  36. package/dist/core/auto-recovery.js.map +1 -0
  37. package/dist/core/failure-policy.d.ts +156 -0
  38. package/dist/core/failure-policy.js +488 -0
  39. package/dist/core/failure-policy.js.map +1 -0
  40. package/dist/core/orchestrator.d.ts +15 -2
  41. package/dist/core/orchestrator.js +392 -15
  42. package/dist/core/orchestrator.js.map +1 -1
  43. package/dist/core/reviewer.d.ts +2 -0
  44. package/dist/core/reviewer.js +2 -0
  45. package/dist/core/reviewer.js.map +1 -1
  46. package/dist/core/runner.d.ts +33 -10
  47. package/dist/core/runner.js +321 -146
  48. package/dist/core/runner.js.map +1 -1
  49. package/dist/services/logging/buffer.d.ts +67 -0
  50. package/dist/services/logging/buffer.js +309 -0
  51. package/dist/services/logging/buffer.js.map +1 -0
  52. package/dist/services/logging/console.d.ts +89 -0
  53. package/dist/services/logging/console.js +169 -0
  54. package/dist/services/logging/console.js.map +1 -0
  55. package/dist/services/logging/file-writer.d.ts +71 -0
  56. package/dist/services/logging/file-writer.js +516 -0
  57. package/dist/services/logging/file-writer.js.map +1 -0
  58. package/dist/services/logging/formatter.d.ts +39 -0
  59. package/dist/services/logging/formatter.js +227 -0
  60. package/dist/services/logging/formatter.js.map +1 -0
  61. package/dist/services/logging/index.d.ts +11 -0
  62. package/dist/services/logging/index.js +30 -0
  63. package/dist/services/logging/index.js.map +1 -0
  64. package/dist/services/logging/parser.d.ts +31 -0
  65. package/dist/services/logging/parser.js +222 -0
  66. package/dist/services/logging/parser.js.map +1 -0
  67. package/dist/services/process/index.d.ts +59 -0
  68. package/dist/services/process/index.js +257 -0
  69. package/dist/services/process/index.js.map +1 -0
  70. package/dist/types/agent.d.ts +20 -0
  71. package/dist/types/agent.js +6 -0
  72. package/dist/types/agent.js.map +1 -0
  73. package/dist/types/config.d.ts +65 -0
  74. package/dist/types/config.js +6 -0
  75. package/dist/types/config.js.map +1 -0
  76. package/dist/types/events.d.ts +125 -0
  77. package/dist/types/events.js +6 -0
  78. package/dist/types/events.js.map +1 -0
  79. package/dist/types/index.d.ts +12 -0
  80. package/dist/types/index.js +37 -0
  81. package/dist/types/index.js.map +1 -0
  82. package/dist/types/lane.d.ts +43 -0
  83. package/dist/types/lane.js +6 -0
  84. package/dist/types/lane.js.map +1 -0
  85. package/dist/types/logging.d.ts +71 -0
  86. package/dist/types/logging.js +16 -0
  87. package/dist/types/logging.js.map +1 -0
  88. package/dist/types/review.d.ts +17 -0
  89. package/dist/types/review.js +6 -0
  90. package/dist/types/review.js.map +1 -0
  91. package/dist/types/run.d.ts +32 -0
  92. package/dist/types/run.js +6 -0
  93. package/dist/types/run.js.map +1 -0
  94. package/dist/types/task.d.ts +71 -0
  95. package/dist/types/task.js +6 -0
  96. package/dist/types/task.js.map +1 -0
  97. package/dist/ui/components.d.ts +134 -0
  98. package/dist/ui/components.js +389 -0
  99. package/dist/ui/components.js.map +1 -0
  100. package/dist/ui/log-viewer.d.ts +49 -0
  101. package/dist/ui/log-viewer.js +449 -0
  102. package/dist/ui/log-viewer.js.map +1 -0
  103. package/dist/utils/checkpoint.d.ts +87 -0
  104. package/dist/utils/checkpoint.js +317 -0
  105. package/dist/utils/checkpoint.js.map +1 -0
  106. package/dist/utils/config.d.ts +4 -0
  107. package/dist/utils/config.js +11 -2
  108. package/dist/utils/config.js.map +1 -1
  109. package/dist/utils/cursor-agent.js.map +1 -1
  110. package/dist/utils/dependency.d.ts +74 -0
  111. package/dist/utils/dependency.js +420 -0
  112. package/dist/utils/dependency.js.map +1 -0
  113. package/dist/utils/doctor.js +10 -5
  114. package/dist/utils/doctor.js.map +1 -1
  115. package/dist/utils/enhanced-logger.d.ts +10 -33
  116. package/dist/utils/enhanced-logger.js +94 -9
  117. package/dist/utils/enhanced-logger.js.map +1 -1
  118. package/dist/utils/git.d.ts +121 -0
  119. package/dist/utils/git.js +322 -2
  120. package/dist/utils/git.js.map +1 -1
  121. package/dist/utils/health.d.ts +91 -0
  122. package/dist/utils/health.js +556 -0
  123. package/dist/utils/health.js.map +1 -0
  124. package/dist/utils/lock.d.ts +95 -0
  125. package/dist/utils/lock.js +332 -0
  126. package/dist/utils/lock.js.map +1 -0
  127. package/dist/utils/log-buffer.d.ts +17 -0
  128. package/dist/utils/log-buffer.js +14 -0
  129. package/dist/utils/log-buffer.js.map +1 -0
  130. package/dist/utils/log-constants.d.ts +23 -0
  131. package/dist/utils/log-constants.js +28 -0
  132. package/dist/utils/log-constants.js.map +1 -0
  133. package/dist/utils/log-formatter.d.ts +9 -0
  134. package/dist/utils/log-formatter.js +113 -70
  135. package/dist/utils/log-formatter.js.map +1 -1
  136. package/dist/utils/log-service.d.ts +19 -0
  137. package/dist/utils/log-service.js +47 -0
  138. package/dist/utils/log-service.js.map +1 -0
  139. package/dist/utils/logger.d.ts +46 -27
  140. package/dist/utils/logger.js +82 -60
  141. package/dist/utils/logger.js.map +1 -1
  142. package/dist/utils/process-manager.d.ts +21 -0
  143. package/dist/utils/process-manager.js +138 -0
  144. package/dist/utils/process-manager.js.map +1 -0
  145. package/dist/utils/retry.d.ts +121 -0
  146. package/dist/utils/retry.js +374 -0
  147. package/dist/utils/retry.js.map +1 -0
  148. package/dist/utils/run-service.d.ts +88 -0
  149. package/dist/utils/run-service.js +412 -0
  150. package/dist/utils/run-service.js.map +1 -0
  151. package/dist/utils/state.d.ts +58 -2
  152. package/dist/utils/state.js +306 -3
  153. package/dist/utils/state.js.map +1 -1
  154. package/dist/utils/task-service.d.ts +82 -0
  155. package/dist/utils/task-service.js +348 -0
  156. package/dist/utils/task-service.js.map +1 -0
  157. package/dist/utils/types.d.ts +2 -272
  158. package/dist/utils/types.js +16 -0
  159. package/dist/utils/types.js.map +1 -1
  160. package/package.json +38 -23
  161. package/scripts/ai-security-check.js +0 -1
  162. package/scripts/local-security-gate.sh +0 -0
  163. package/scripts/monitor-lanes.sh +94 -0
  164. package/scripts/patches/test-cursor-agent.js +0 -1
  165. package/scripts/release.sh +0 -0
  166. package/scripts/setup-security.sh +0 -0
  167. package/scripts/stream-logs.sh +72 -0
  168. package/scripts/verify-and-fix.sh +0 -0
  169. package/src/cli/clean.ts +180 -0
  170. package/src/cli/index.ts +7 -0
  171. package/src/cli/init.ts +1 -1
  172. package/src/cli/logs.ts +79 -42
  173. package/src/cli/monitor.ts +1815 -899
  174. package/src/cli/prepare.ts +4 -3
  175. package/src/cli/resume.ts +220 -277
  176. package/src/cli/run.ts +9 -3
  177. package/src/cli/runs.ts +212 -0
  178. package/src/cli/setup-commands.ts +0 -0
  179. package/src/cli/signal.ts +1 -1
  180. package/src/cli/stop.ts +209 -0
  181. package/src/cli/tasks.ts +154 -0
  182. package/src/core/auto-recovery.ts +909 -0
  183. package/src/core/failure-policy.ts +592 -0
  184. package/src/core/orchestrator.ts +1131 -675
  185. package/src/core/reviewer.ts +4 -0
  186. package/src/core/runner.ts +388 -162
  187. package/src/services/logging/buffer.ts +326 -0
  188. package/src/services/logging/console.ts +193 -0
  189. package/src/services/logging/file-writer.ts +526 -0
  190. package/src/services/logging/formatter.ts +268 -0
  191. package/src/services/logging/index.ts +16 -0
  192. package/src/services/logging/parser.ts +232 -0
  193. package/src/services/process/index.ts +261 -0
  194. package/src/types/agent.ts +24 -0
  195. package/src/types/config.ts +79 -0
  196. package/src/types/events.ts +156 -0
  197. package/src/types/index.ts +29 -0
  198. package/src/types/lane.ts +56 -0
  199. package/src/types/logging.ts +96 -0
  200. package/src/types/review.ts +20 -0
  201. package/src/types/run.ts +37 -0
  202. package/src/types/task.ts +79 -0
  203. package/src/ui/components.ts +430 -0
  204. package/src/ui/log-viewer.ts +485 -0
  205. package/src/utils/checkpoint.ts +374 -0
  206. package/src/utils/config.ts +11 -2
  207. package/src/utils/cursor-agent.ts +1 -1
  208. package/src/utils/dependency.ts +482 -0
  209. package/src/utils/doctor.ts +11 -5
  210. package/src/utils/enhanced-logger.ts +108 -49
  211. package/src/utils/git.ts +374 -2
  212. package/src/utils/health.ts +596 -0
  213. package/src/utils/lock.ts +346 -0
  214. package/src/utils/log-buffer.ts +28 -0
  215. package/src/utils/log-constants.ts +26 -0
  216. package/src/utils/log-formatter.ts +120 -37
  217. package/src/utils/log-service.ts +49 -0
  218. package/src/utils/logger.ts +100 -51
  219. package/src/utils/process-manager.ts +100 -0
  220. package/src/utils/retry.ts +413 -0
  221. package/src/utils/run-service.ts +433 -0
  222. package/src/utils/state.ts +369 -3
  223. package/src/utils/task-service.ts +370 -0
  224. package/src/utils/types.ts +2 -315
@@ -1,6 +1,13 @@
1
1
  "use strict";
2
2
  /**
3
3
  * CursorFlow interactive monitor command
4
+ *
5
+ * Features:
6
+ * - Lane dashboard with accurate process status
7
+ * - Unified log view for all lanes
8
+ * - Readable log format support
9
+ * - Multiple flows dashboard
10
+ * - Consistent layout across all views
4
11
  */
5
12
  var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
6
13
  if (k2 === undefined) k2 = k;
@@ -41,16 +48,49 @@ const readline = __importStar(require("readline"));
41
48
  const state_1 = require("../utils/state");
42
49
  const config_1 = require("../utils/config");
43
50
  const path_1 = require("../utils/path");
51
+ const process_1 = require("../services/process");
52
+ const buffer_1 = require("../services/logging/buffer");
53
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
54
+ // UI Constants
55
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
56
+ const UI = {
57
+ COLORS: {
58
+ reset: '\x1b[0m',
59
+ bold: '\x1b[1m',
60
+ dim: '\x1b[2m',
61
+ cyan: '\x1b[36m',
62
+ green: '\x1b[32m',
63
+ yellow: '\x1b[33m',
64
+ red: '\x1b[31m',
65
+ magenta: '\x1b[35m',
66
+ gray: '\x1b[90m',
67
+ white: '\x1b[37m',
68
+ bgGray: '\x1b[48;5;236m',
69
+ bgCyan: '\x1b[46m',
70
+ },
71
+ CHARS: {
72
+ hLine: '━',
73
+ vLine: '│',
74
+ corner: {
75
+ tl: '┌', tr: '┐', bl: '└', br: '┘'
76
+ },
77
+ arrow: {
78
+ right: '▶', left: '◀', up: '▲', down: '▼'
79
+ },
80
+ bullet: '•',
81
+ check: '✓',
82
+ },
83
+ };
44
84
  function printHelp() {
45
- console.log(`
46
- Usage: cursorflow monitor [run-dir] [options]
47
-
48
- Interactive lane dashboard to track progress and dependencies.
49
-
50
- Options:
51
- [run-dir] Run directory to monitor (default: latest)
52
- --interval <seconds> Refresh interval (default: 2)
53
- --help, -h Show help
85
+ console.log(`
86
+ Usage: cursorflow monitor [run-dir] [options]
87
+
88
+ Interactive lane dashboard to track progress and dependencies.
89
+
90
+ Options:
91
+ [run-dir] Run directory to monitor (default: latest)
92
+ --interval <seconds> Refresh interval (default: 2)
93
+ --help, -h Show help
54
94
  `);
55
95
  }
56
96
  var View;
@@ -62,6 +102,8 @@ var View;
62
102
  View[View["TERMINAL"] = 4] = "TERMINAL";
63
103
  View[View["INTERVENE"] = 5] = "INTERVENE";
64
104
  View[View["TIMEOUT"] = 6] = "TIMEOUT";
105
+ View[View["UNIFIED_LOG"] = 7] = "UNIFIED_LOG";
106
+ View[View["FLOWS_DASHBOARD"] = 8] = "FLOWS_DASHBOARD";
65
107
  })(View || (View = {}));
66
108
  class InteractiveMonitor {
67
109
  runDir;
@@ -75,19 +117,92 @@ class InteractiveMonitor {
75
117
  timer = null;
76
118
  scrollOffset = 0;
77
119
  terminalScrollOffset = 0;
120
+ followMode = true;
121
+ unseenLineCount = 0;
78
122
  lastTerminalTotalLines = 0;
79
123
  interventionInput = '';
80
124
  timeoutInput = '';
81
125
  notification = null;
82
- constructor(runDir, interval) {
126
+ // Process status tracking
127
+ laneProcessStatuses = new Map();
128
+ // Unified log buffer for all lanes
129
+ unifiedLogBuffer = null;
130
+ unifiedLogScrollOffset = 0;
131
+ unifiedLogFollowMode = true;
132
+ // Multiple flows support
133
+ allFlows = [];
134
+ selectedFlowIndex = 0;
135
+ logsDir = '';
136
+ // NEW: UX improvements
137
+ readableFormat = true; // Toggle readable log format
138
+ laneFilter = null; // Filter by lane name
139
+ confirmAction = null;
140
+ // Screen dimensions
141
+ get screenWidth() {
142
+ return process.stdout.columns || 120;
143
+ }
144
+ get screenHeight() {
145
+ return process.stdout.rows || 24;
146
+ }
147
+ constructor(runDir, interval, logsDir) {
83
148
  this.runDir = runDir;
84
149
  this.interval = interval;
150
+ // Set logs directory for multiple flows discovery
151
+ if (logsDir) {
152
+ this.logsDir = logsDir;
153
+ }
154
+ else {
155
+ const config = (0, config_1.loadConfig)();
156
+ this.logsDir = (0, path_1.safeJoin)(config.logsDir, 'runs');
157
+ }
158
+ // Initialize unified log buffer
159
+ this.unifiedLogBuffer = new buffer_1.LogBufferService(runDir);
85
160
  }
86
161
  async start() {
87
162
  this.setupTerminal();
163
+ // Start unified log streaming
164
+ if (this.unifiedLogBuffer) {
165
+ this.unifiedLogBuffer.startStreaming();
166
+ this.unifiedLogBuffer.on('update', () => {
167
+ if (this.view === View.UNIFIED_LOG && this.unifiedLogFollowMode) {
168
+ this.render();
169
+ }
170
+ });
171
+ }
172
+ // Discover all flows
173
+ this.discoverFlows();
88
174
  this.refresh();
89
175
  this.timer = setInterval(() => this.refresh(), this.interval * 1000);
90
176
  }
177
+ /**
178
+ * Discover all run directories (flows) for multi-flow view
179
+ */
180
+ discoverFlows() {
181
+ try {
182
+ if (!fs.existsSync(this.logsDir))
183
+ return;
184
+ const runs = fs.readdirSync(this.logsDir)
185
+ .filter(d => d.startsWith('run-'))
186
+ .map(d => {
187
+ const runDir = (0, path_1.safeJoin)(this.logsDir, d);
188
+ const summary = (0, process_1.getFlowSummary)(runDir);
189
+ return {
190
+ runDir,
191
+ runId: d,
192
+ isAlive: summary.isAlive,
193
+ summary,
194
+ };
195
+ })
196
+ .sort((a, b) => {
197
+ // Sort by run ID (timestamp-based) descending
198
+ return b.runId.localeCompare(a.runId);
199
+ });
200
+ this.allFlows = runs;
201
+ }
202
+ catch {
203
+ // Ignore errors
204
+ }
205
+ }
91
206
  setupTerminal() {
92
207
  if (process.stdin.isTTY) {
93
208
  process.stdin.setRawMode(true);
@@ -122,6 +237,12 @@ class InteractiveMonitor {
122
237
  else if (this.view === View.MESSAGE_DETAIL) {
123
238
  this.handleMessageDetailKey(keyName);
124
239
  }
240
+ else if (this.view === View.UNIFIED_LOG) {
241
+ this.handleUnifiedLogKey(keyName);
242
+ }
243
+ else if (this.view === View.FLOWS_DASHBOARD) {
244
+ this.handleFlowsDashboardKey(keyName);
245
+ }
125
246
  });
126
247
  // Hide cursor
127
248
  process.stdout.write('\x1B[?25l');
@@ -129,6 +250,10 @@ class InteractiveMonitor {
129
250
  stop() {
130
251
  if (this.timer)
131
252
  clearInterval(this.timer);
253
+ // Stop unified log streaming
254
+ if (this.unifiedLogBuffer) {
255
+ this.unifiedLogBuffer.stopStreaming();
256
+ }
132
257
  // Show cursor and clear screen
133
258
  process.stdout.write('\x1B[?25h');
134
259
  process.stdout.write('\x1Bc');
@@ -162,6 +287,19 @@ class InteractiveMonitor {
162
287
  this.view = View.FLOW;
163
288
  this.render();
164
289
  break;
290
+ case 'u':
291
+ // Unified log view
292
+ this.view = View.UNIFIED_LOG;
293
+ this.unifiedLogScrollOffset = 0;
294
+ this.unifiedLogFollowMode = true;
295
+ this.render();
296
+ break;
297
+ case 'm':
298
+ // Multiple flows dashboard
299
+ this.discoverFlows();
300
+ this.view = View.FLOWS_DASHBOARD;
301
+ this.render();
302
+ break;
165
303
  case 'q':
166
304
  this.stop();
167
305
  break;
@@ -249,11 +387,29 @@ class InteractiveMonitor {
249
387
  handleTerminalKey(key) {
250
388
  switch (key) {
251
389
  case 'up':
390
+ this.followMode = false;
252
391
  this.terminalScrollOffset++;
253
392
  this.render();
254
393
  break;
255
394
  case 'down':
256
395
  this.terminalScrollOffset = Math.max(0, this.terminalScrollOffset - 1);
396
+ if (this.terminalScrollOffset === 0) {
397
+ this.followMode = true;
398
+ this.unseenLineCount = 0;
399
+ }
400
+ this.render();
401
+ break;
402
+ case 'f':
403
+ this.followMode = true;
404
+ this.terminalScrollOffset = 0;
405
+ this.unseenLineCount = 0;
406
+ this.render();
407
+ break;
408
+ case 'r':
409
+ // Toggle readable log format
410
+ this.readableFormat = !this.readableFormat;
411
+ this.terminalScrollOffset = 0;
412
+ this.lastTerminalTotalLines = 0;
257
413
  this.render();
258
414
  break;
259
415
  case 't':
@@ -339,6 +495,197 @@ class InteractiveMonitor {
339
495
  break;
340
496
  }
341
497
  }
498
+ handleUnifiedLogKey(key) {
499
+ const pageSize = Math.max(10, this.screenHeight - 12);
500
+ switch (key) {
501
+ case 'up':
502
+ this.unifiedLogFollowMode = false;
503
+ this.unifiedLogScrollOffset++;
504
+ this.render();
505
+ break;
506
+ case 'down':
507
+ this.unifiedLogScrollOffset = Math.max(0, this.unifiedLogScrollOffset - 1);
508
+ if (this.unifiedLogScrollOffset === 0) {
509
+ this.unifiedLogFollowMode = true;
510
+ }
511
+ this.render();
512
+ break;
513
+ case 'pageup':
514
+ this.unifiedLogFollowMode = false;
515
+ this.unifiedLogScrollOffset += pageSize;
516
+ this.render();
517
+ break;
518
+ case 'pagedown':
519
+ this.unifiedLogScrollOffset = Math.max(0, this.unifiedLogScrollOffset - pageSize);
520
+ if (this.unifiedLogScrollOffset === 0) {
521
+ this.unifiedLogFollowMode = true;
522
+ }
523
+ this.render();
524
+ break;
525
+ case 'f':
526
+ this.unifiedLogFollowMode = true;
527
+ this.unifiedLogScrollOffset = 0;
528
+ this.render();
529
+ break;
530
+ case 'r':
531
+ // Toggle readable format
532
+ this.readableFormat = !this.readableFormat;
533
+ this.render();
534
+ break;
535
+ case 'l':
536
+ // Cycle through lane filter
537
+ this.cycleLaneFilter();
538
+ this.unifiedLogScrollOffset = 0;
539
+ this.render();
540
+ break;
541
+ case 'escape':
542
+ case 'backspace':
543
+ case 'u':
544
+ this.view = View.LIST;
545
+ this.render();
546
+ break;
547
+ case 'q':
548
+ this.stop();
549
+ break;
550
+ }
551
+ }
552
+ /**
553
+ * Cycle through available lanes for filtering
554
+ */
555
+ cycleLaneFilter() {
556
+ const lanes = this.unifiedLogBuffer?.getLanes() || [];
557
+ if (lanes.length === 0) {
558
+ this.laneFilter = null;
559
+ return;
560
+ }
561
+ if (this.laneFilter === null) {
562
+ // Show first lane
563
+ this.laneFilter = lanes[0];
564
+ }
565
+ else {
566
+ const currentIndex = lanes.indexOf(this.laneFilter);
567
+ if (currentIndex === -1 || currentIndex === lanes.length - 1) {
568
+ // Reset to all lanes
569
+ this.laneFilter = null;
570
+ }
571
+ else {
572
+ // Next lane
573
+ this.laneFilter = lanes[currentIndex + 1];
574
+ }
575
+ }
576
+ }
577
+ handleFlowsDashboardKey(key) {
578
+ // Handle confirmation dialog first
579
+ if (this.confirmAction) {
580
+ if (key === 'y') {
581
+ this.executeConfirmedAction();
582
+ return;
583
+ }
584
+ else if (key === 'n' || key === 'escape') {
585
+ this.confirmAction = null;
586
+ this.render();
587
+ return;
588
+ }
589
+ // Other keys cancel confirmation
590
+ this.confirmAction = null;
591
+ this.render();
592
+ return;
593
+ }
594
+ switch (key) {
595
+ case 'up':
596
+ this.selectedFlowIndex = Math.max(0, this.selectedFlowIndex - 1);
597
+ this.render();
598
+ break;
599
+ case 'down':
600
+ this.selectedFlowIndex = Math.min(this.allFlows.length - 1, this.selectedFlowIndex + 1);
601
+ this.render();
602
+ break;
603
+ case 'right':
604
+ case 'return':
605
+ case 'enter':
606
+ // Switch to selected flow
607
+ if (this.allFlows[this.selectedFlowIndex]) {
608
+ const flow = this.allFlows[this.selectedFlowIndex];
609
+ this.runDir = flow.runDir;
610
+ // Restart log buffer for new run
611
+ if (this.unifiedLogBuffer) {
612
+ this.unifiedLogBuffer.stopStreaming();
613
+ }
614
+ this.unifiedLogBuffer = new buffer_1.LogBufferService(this.runDir);
615
+ this.unifiedLogBuffer.startStreaming();
616
+ this.lanes = [];
617
+ this.laneProcessStatuses.clear();
618
+ this.view = View.LIST;
619
+ this.showNotification(`Switched to flow: ${flow.runId}`, 'info');
620
+ this.refresh();
621
+ }
622
+ break;
623
+ case 'd':
624
+ // Delete flow (with confirmation)
625
+ if (this.allFlows[this.selectedFlowIndex]) {
626
+ const flow = this.allFlows[this.selectedFlowIndex];
627
+ if (flow.isAlive) {
628
+ this.showNotification('Cannot delete a running flow. Stop it first.', 'error');
629
+ }
630
+ else if (flow.runDir === this.runDir) {
631
+ this.showNotification('Cannot delete the currently viewed flow.', 'error');
632
+ }
633
+ else {
634
+ this.confirmAction = {
635
+ type: 'delete-flow',
636
+ target: flow.runId,
637
+ time: Date.now(),
638
+ };
639
+ this.render();
640
+ }
641
+ }
642
+ break;
643
+ case 'r':
644
+ // Refresh flows
645
+ this.discoverFlows();
646
+ this.showNotification('Flows refreshed', 'info');
647
+ this.render();
648
+ break;
649
+ case 'escape':
650
+ case 'backspace':
651
+ case 'm':
652
+ this.view = View.LIST;
653
+ this.render();
654
+ break;
655
+ case 'q':
656
+ this.stop();
657
+ break;
658
+ }
659
+ }
660
+ /**
661
+ * Execute a confirmed action (delete flow, kill process, etc.)
662
+ */
663
+ executeConfirmedAction() {
664
+ if (!this.confirmAction)
665
+ return;
666
+ const { type, target } = this.confirmAction;
667
+ this.confirmAction = null;
668
+ if (type === 'delete-flow') {
669
+ const flow = this.allFlows.find(f => f.runId === target);
670
+ if (flow) {
671
+ try {
672
+ // Delete the flow directory
673
+ fs.rmSync(flow.runDir, { recursive: true, force: true });
674
+ this.showNotification(`Deleted flow: ${target}`, 'success');
675
+ // Refresh the list
676
+ this.discoverFlows();
677
+ // Adjust selection if needed
678
+ if (this.selectedFlowIndex >= this.allFlows.length) {
679
+ this.selectedFlowIndex = Math.max(0, this.allFlows.length - 1);
680
+ }
681
+ }
682
+ catch (err) {
683
+ this.showNotification(`Failed to delete flow: ${err}`, 'error');
684
+ }
685
+ }
686
+ }
687
+ this.render();
688
+ }
342
689
  sendIntervention(message) {
343
690
  if (!this.selectedLaneName)
344
691
  return;
@@ -352,7 +699,7 @@ class InteractiveMonitor {
352
699
  const convoPath = (0, path_1.safeJoin)(lane.path, 'conversation.jsonl');
353
700
  const entry = {
354
701
  timestamp: new Date().toISOString(),
355
- role: 'user',
702
+ role: 'intervention',
356
703
  task: 'INTERVENTION',
357
704
  fullText: `[HUMAN INTERVENTION]: ${message}`,
358
705
  textLength: message.length + 20,
@@ -400,11 +747,29 @@ class InteractiveMonitor {
400
747
  }
401
748
  refresh() {
402
749
  this.lanes = this.listLanesWithDeps(this.runDir);
403
- if (this.view !== View.LIST) {
750
+ // Update process statuses for accurate display
751
+ this.updateProcessStatuses();
752
+ if (this.view !== View.LIST && this.view !== View.UNIFIED_LOG && this.view !== View.FLOWS_DASHBOARD) {
404
753
  this.refreshLogs();
405
754
  }
755
+ // Refresh flows list periodically
756
+ if (this.view === View.FLOWS_DASHBOARD) {
757
+ this.discoverFlows();
758
+ }
406
759
  this.render();
407
760
  }
761
+ /**
762
+ * Update process statuses for all lanes
763
+ */
764
+ updateProcessStatuses() {
765
+ const lanesDir = (0, path_1.safeJoin)(this.runDir, 'lanes');
766
+ if (!fs.existsSync(lanesDir))
767
+ return;
768
+ for (const lane of this.lanes) {
769
+ const status = (0, process_1.getLaneProcessStatus)(lane.path, lane.name);
770
+ this.laneProcessStatuses.set(lane.name, status);
771
+ }
772
+ }
408
773
  killLane() {
409
774
  if (!this.selectedLaneName)
410
775
  return;
@@ -429,6 +794,55 @@ class InteractiveMonitor {
429
794
  this.notification = { message, type, time: Date.now() };
430
795
  this.render();
431
796
  }
797
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
798
+ // UI Layout Helpers - Consistent header/footer across all views
799
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
800
+ renderHeader(title, breadcrumb = []) {
801
+ const width = Math.min(this.screenWidth, 120);
802
+ const line = UI.CHARS.hLine.repeat(width);
803
+ // Flow status
804
+ const flowSummary = (0, process_1.getFlowSummary)(this.runDir);
805
+ const flowStatusIcon = flowSummary.isAlive ? '🟢' : (flowSummary.completed === flowSummary.total && flowSummary.total > 0 ? '✅' : '🔴');
806
+ // Breadcrumb
807
+ const crumbs = ['CursorFlow', ...breadcrumb].join(` ${UI.COLORS.gray}›${UI.COLORS.reset} `);
808
+ // Time
809
+ const timeStr = new Date().toLocaleTimeString('en-US', { hour12: false });
810
+ process.stdout.write(`${UI.COLORS.cyan}${line}${UI.COLORS.reset}\n`);
811
+ process.stdout.write(`${UI.COLORS.bold}${crumbs}${UI.COLORS.reset} ${flowStatusIcon} `);
812
+ process.stdout.write(`${UI.COLORS.dim}${timeStr}${UI.COLORS.reset}\n`);
813
+ process.stdout.write(`${UI.COLORS.cyan}${line}${UI.COLORS.reset}\n`);
814
+ }
815
+ renderFooter(actions) {
816
+ const width = Math.min(this.screenWidth, 120);
817
+ const line = UI.CHARS.hLine.repeat(width);
818
+ // Notification area
819
+ if (this.notification && Date.now() - this.notification.time < 3000) {
820
+ const nColor = this.notification.type === 'error' ? UI.COLORS.red
821
+ : this.notification.type === 'success' ? UI.COLORS.green
822
+ : UI.COLORS.cyan;
823
+ process.stdout.write(`\n${nColor}🔔 ${this.notification.message}${UI.COLORS.reset}\n`);
824
+ }
825
+ // Confirmation dialog area
826
+ if (this.confirmAction && Date.now() - this.confirmAction.time < 10000) {
827
+ const actionName = this.confirmAction.type === 'delete-flow' ? 'DELETE FLOW' : 'KILL PROCESS';
828
+ process.stdout.write(`\n${UI.COLORS.yellow}⚠️ Confirm ${actionName}: ${this.confirmAction.target}? [Y] Yes / [N] No${UI.COLORS.reset}\n`);
829
+ }
830
+ process.stdout.write(`\n${UI.COLORS.cyan}${line}${UI.COLORS.reset}\n`);
831
+ const formattedActions = actions.map(a => {
832
+ const parts = a.split('] ');
833
+ if (parts.length === 2) {
834
+ // Use regex with global flag to replace all occurrences
835
+ return `${UI.COLORS.yellow}[${parts[0].replace(/\[/g, '')}]${UI.COLORS.reset} ${parts[1]}`;
836
+ }
837
+ return a;
838
+ });
839
+ process.stdout.write(` ${formattedActions.join(' ')}\n`);
840
+ }
841
+ renderSectionTitle(title, extra) {
842
+ const extraStr = extra ? ` ${UI.COLORS.dim}${extra}${UI.COLORS.reset}` : '';
843
+ process.stdout.write(`\n${UI.COLORS.bold}${title}${UI.COLORS.reset}${extraStr}\n`);
844
+ process.stdout.write(`${UI.COLORS.gray}${'─'.repeat(40)}${UI.COLORS.reset}\n`);
845
+ }
432
846
  render() {
433
847
  // Clear screen
434
848
  process.stdout.write('\x1Bc');
@@ -436,9 +850,9 @@ class InteractiveMonitor {
436
850
  if (this.notification && Date.now() - this.notification.time > 3000) {
437
851
  this.notification = null;
438
852
  }
439
- if (this.notification) {
440
- const color = this.notification.type === 'error' ? '\x1b[31m' : this.notification.type === 'success' ? '\x1b[32m' : '\x1b[36m';
441
- console.log(`${color}🔔 ${this.notification.message}\x1b[0m\n`);
853
+ // Clear old confirmation
854
+ if (this.confirmAction && Date.now() - this.confirmAction.time > 10000) {
855
+ this.confirmAction = null;
442
856
  }
443
857
  switch (this.view) {
444
858
  case View.LIST:
@@ -462,67 +876,113 @@ class InteractiveMonitor {
462
876
  case View.TIMEOUT:
463
877
  this.renderTimeout();
464
878
  break;
879
+ case View.UNIFIED_LOG:
880
+ this.renderUnifiedLog();
881
+ break;
882
+ case View.FLOWS_DASHBOARD:
883
+ this.renderFlowsDashboard();
884
+ break;
465
885
  }
466
886
  }
467
887
  renderList() {
468
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
469
- console.log(`📊 CursorFlow Monitor - Run: ${path.basename(this.runDir)}`);
470
- console.log(`🕒 Updated: ${new Date().toLocaleTimeString()} | [↑/↓/→] Nav [←] Flow [Q] Quit`);
471
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
888
+ const flowSummary = (0, process_1.getFlowSummary)(this.runDir);
889
+ const runId = path.basename(this.runDir);
890
+ this.renderHeader('Lane Dashboard', [runId]);
891
+ // Summary line
892
+ const summaryParts = [
893
+ `${flowSummary.running} ${UI.COLORS.cyan}running${UI.COLORS.reset}`,
894
+ `${flowSummary.completed} ${UI.COLORS.green}done${UI.COLORS.reset}`,
895
+ `${flowSummary.failed} ${UI.COLORS.red}failed${UI.COLORS.reset}`,
896
+ `${flowSummary.dead} ${UI.COLORS.yellow}stale${UI.COLORS.reset}`,
897
+ ];
898
+ process.stdout.write(` ${UI.COLORS.dim}Lanes:${UI.COLORS.reset} ${summaryParts.join(' │ ')}\n`);
472
899
  if (this.lanes.length === 0) {
473
- console.log(' No lanes found\n');
900
+ process.stdout.write(`\n ${UI.COLORS.dim}No lanes found${UI.COLORS.reset}\n`);
901
+ this.renderFooter(['[Q] Quit', '[M] All Flows']);
474
902
  return;
475
903
  }
476
904
  const laneStatuses = {};
477
905
  this.lanes.forEach(l => laneStatuses[l.name] = this.getLaneStatus(l.path, l.name));
478
- const maxNameLen = Math.max(...this.lanes.map(l => l.name.length), 15);
479
- console.log(` ${'Lane'.padEnd(maxNameLen)} Status Progress Time Tasks Next Action`);
480
- console.log(` ${'─'.repeat(maxNameLen)} ${'─'.repeat(18)} ${'─'.repeat(8)} ${'─'.repeat(8)} ${'─'.repeat(6)} ${'─'.repeat(20)}`);
906
+ const maxNameLen = Math.max(...this.lanes.map(l => l.name.length), 12);
907
+ process.stdout.write(`\n ${'Lane'.padEnd(maxNameLen)} ${'Status'.padEnd(12)} ${'PID'.padEnd(7)} ${'Time'.padEnd(8)} ${'Tasks'.padEnd(6)} Next\n`);
908
+ process.stdout.write(` ${'─'.repeat(maxNameLen)} ${'─'.repeat(12)} ${'─'.repeat(7)} ${'─'.repeat(8)} ${'─'.repeat(6)} ${'─'.repeat(25)}\n`);
481
909
  this.lanes.forEach((lane, i) => {
482
910
  const isSelected = i === this.selectedLaneIndex;
483
911
  const status = laneStatuses[lane.name];
484
- const statusIcon = this.getStatusIcon(status.status);
485
- const statusText = `${statusIcon} ${status.status}`.padEnd(18);
486
- const progressText = status.progress.padEnd(8);
487
- const timeText = this.formatDuration(status.duration).padEnd(8);
488
- let tasksDisplay = '-';
912
+ const processStatus = this.laneProcessStatuses.get(lane.name);
913
+ // Determine the accurate status based on process detection
914
+ let displayStatus = status.status;
915
+ let statusColor = UI.COLORS.gray;
916
+ let statusIcon = this.getStatusIcon(status.status);
917
+ if (processStatus) {
918
+ if (processStatus.isStale) {
919
+ displayStatus = 'STALE';
920
+ statusIcon = '💀';
921
+ statusColor = UI.COLORS.yellow;
922
+ }
923
+ else if (processStatus.actualStatus === 'dead' && status.status === 'running') {
924
+ displayStatus = 'DEAD';
925
+ statusIcon = '☠️';
926
+ statusColor = UI.COLORS.red;
927
+ }
928
+ else if (processStatus.actualStatus === 'running') {
929
+ statusColor = UI.COLORS.cyan;
930
+ }
931
+ else if (status.status === 'completed') {
932
+ statusColor = UI.COLORS.green;
933
+ }
934
+ else if (status.status === 'failed') {
935
+ statusColor = UI.COLORS.red;
936
+ }
937
+ }
938
+ const statusText = `${statusIcon} ${displayStatus}`.padEnd(12);
939
+ // Process indicator
940
+ let pidText = '-'.padEnd(7);
941
+ if (processStatus?.pid) {
942
+ const pidIcon = processStatus.processRunning ? '●' : '○';
943
+ const pidColor = processStatus.processRunning ? UI.COLORS.green : UI.COLORS.red;
944
+ pidText = `${pidColor}${pidIcon}${UI.COLORS.reset}${processStatus.pid}`.padEnd(7 + 9); // +9 for color codes
945
+ }
946
+ // Duration
947
+ const duration = processStatus?.duration || status.duration;
948
+ const timeText = this.formatDuration(duration).padEnd(8);
949
+ // Tasks
950
+ let tasksText = '-'.padEnd(6);
489
951
  if (typeof status.totalTasks === 'number') {
490
- tasksDisplay = `${status.currentTask}/${status.totalTasks}`;
952
+ tasksText = `${status.currentTask}/${status.totalTasks}`.padEnd(6);
491
953
  }
492
- const tasksText = tasksDisplay.padEnd(6);
493
- // Determine "Next Action"
954
+ // Next action
494
955
  let nextAction = '-';
495
956
  if (status.status === 'completed') {
496
- const dependents = this.lanes.filter(l => laneStatuses[l.name].dependsOn.includes(lane.name));
497
- if (dependents.length > 0) {
498
- nextAction = `Unlock: ${dependents.map(d => d.name).join(', ')}`;
499
- }
500
- else {
501
- nextAction = '🏁 Done';
502
- }
957
+ const dependents = this.lanes.filter(l => laneStatuses[l.name]?.dependsOn?.includes(lane.name));
958
+ nextAction = dependents.length > 0 ? `→ ${dependents.map(d => d.name).join(', ')}` : '✓ Done';
503
959
  }
504
960
  else if (status.status === 'waiting') {
505
- if (status.waitingFor && status.waitingFor.length > 0) {
506
- nextAction = `Wait for task: ${status.waitingFor.join(', ')}`;
961
+ if (status.waitingFor?.length > 0) {
962
+ nextAction = `⏳ ${status.waitingFor.join(', ')}`;
507
963
  }
508
964
  else {
509
- const missingDeps = status.dependsOn.filter((d) => laneStatuses[d] && laneStatuses[d].status !== 'completed');
510
- nextAction = `Wait for lane: ${missingDeps.join(', ')}`;
965
+ const missingDeps = status.dependsOn.filter((d) => laneStatuses[d]?.status !== 'completed');
966
+ nextAction = missingDeps.length > 0 ? `⏳ ${missingDeps.join(', ')}` : '⏳ waiting';
511
967
  }
512
968
  }
513
- else if (status.status === 'running') {
514
- nextAction = '🚀 Working...';
515
- }
516
- const prefix = isSelected ? ' ▶ ' : ' ';
517
- const line = `${prefix}${lane.name.padEnd(maxNameLen)} ${statusText} ${progressText} ${timeText} ${tasksText} ${nextAction}`;
518
- if (isSelected) {
519
- process.stdout.write(`\x1b[36m${line}\x1b[0m\n`);
969
+ else if (processStatus?.actualStatus === 'running') {
970
+ nextAction = '🚀 working...';
520
971
  }
521
- else {
522
- process.stdout.write(`${line}\n`);
972
+ else if (processStatus?.isStale) {
973
+ nextAction = '⚠️ died unexpectedly';
523
974
  }
975
+ // Truncate next action
976
+ if (nextAction.length > 25)
977
+ nextAction = nextAction.substring(0, 22) + '...';
978
+ const prefix = isSelected ? ` ${UI.COLORS.cyan}▶${UI.COLORS.reset} ` : ' ';
979
+ const rowBg = isSelected ? UI.COLORS.bgGray : '';
980
+ const rowEnd = isSelected ? UI.COLORS.reset : '';
981
+ process.stdout.write(`${rowBg}${prefix}${lane.name.padEnd(maxNameLen)} ${statusColor}${statusText}${UI.COLORS.reset} ${pidText} ${timeText} ${tasksText} ${nextAction}${rowEnd}\n`);
524
982
  });
525
- console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
983
+ this.renderFooter([
984
+ '[↑↓] Select', '[→/Enter] Details', '[F] Flow', '[U] Unified Logs', '[M] All Flows', '[Q] Quit'
985
+ ]);
526
986
  }
527
987
  renderLaneDetail() {
528
988
  const lane = this.lanes.find(l => l.name === this.selectedLaneName);
@@ -532,70 +992,92 @@ class InteractiveMonitor {
532
992
  return;
533
993
  }
534
994
  const status = this.getLaneStatus(lane.path, lane.name);
995
+ const processStatus = this.laneProcessStatuses.get(lane.name);
996
+ this.renderHeader('Lane Detail', [path.basename(this.runDir), lane.name]);
997
+ // Status grid
998
+ const statusColor = status.status === 'completed' ? UI.COLORS.green
999
+ : status.status === 'failed' ? UI.COLORS.red
1000
+ : status.status === 'running' ? UI.COLORS.cyan : UI.COLORS.gray;
1001
+ const actualStatus = processStatus?.actualStatus || status.status;
1002
+ const isStale = processStatus?.isStale || false;
1003
+ process.stdout.write(`\n`);
1004
+ process.stdout.write(` ${UI.COLORS.dim}Status${UI.COLORS.reset} ${statusColor}${this.getStatusIcon(actualStatus)} ${actualStatus.toUpperCase()}${UI.COLORS.reset}`);
1005
+ if (isStale)
1006
+ process.stdout.write(` ${UI.COLORS.yellow}(stale)${UI.COLORS.reset}`);
1007
+ process.stdout.write(`\n`);
1008
+ const pidDisplay = processStatus?.pid
1009
+ ? `${processStatus.processRunning ? UI.COLORS.green : UI.COLORS.red}${processStatus.pid}${UI.COLORS.reset}`
1010
+ : '-';
1011
+ process.stdout.write(` ${UI.COLORS.dim}PID${UI.COLORS.reset} ${pidDisplay}\n`);
1012
+ process.stdout.write(` ${UI.COLORS.dim}Progress${UI.COLORS.reset} ${status.currentTask}/${status.totalTasks} tasks (${status.progress})\n`);
1013
+ process.stdout.write(` ${UI.COLORS.dim}Duration${UI.COLORS.reset} ${this.formatDuration(processStatus?.duration || status.duration)}\n`);
1014
+ process.stdout.write(` ${UI.COLORS.dim}Branch${UI.COLORS.reset} ${status.pipelineBranch}\n`);
1015
+ if (status.dependsOn && status.dependsOn.length > 0) {
1016
+ process.stdout.write(` ${UI.COLORS.dim}Depends${UI.COLORS.reset} ${status.dependsOn.join(', ')}\n`);
1017
+ }
1018
+ if (status.waitingFor && status.waitingFor.length > 0) {
1019
+ process.stdout.write(` ${UI.COLORS.yellow}Waiting${UI.COLORS.reset} ${status.waitingFor.join(', ')}\n`);
1020
+ }
1021
+ if (status.error) {
1022
+ process.stdout.write(` ${UI.COLORS.red}Error${UI.COLORS.reset} ${status.error}\n`);
1023
+ }
1024
+ // Live terminal preview
1025
+ this.renderSectionTitle('Live Terminal', 'last 10 lines');
535
1026
  const logPath = (0, path_1.safeJoin)(lane.path, 'terminal.log');
536
- let liveLog = '(No live terminal output)';
537
1027
  if (fs.existsSync(logPath)) {
538
1028
  const content = fs.readFileSync(logPath, 'utf8');
539
- liveLog = content.split('\n').slice(-15).join('\n');
540
- }
541
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
542
- console.log(`🔍 Lane: ${lane.name}`);
543
- console.log(`🕒 Updated: ${new Date().toLocaleTimeString()} | [↑/↓] Browse [T] Term [I] Intervene [O] Timeout [Esc] Back`);
544
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
545
- process.stdout.write(` Status: ${this.getStatusIcon(status.status)} ${status.status}\n`);
546
- process.stdout.write(` PID: ${status.pid || '-'}\n`);
547
- process.stdout.write(` Progress: ${status.progress} (${status.currentTask}/${status.totalTasks} tasks)\n`);
548
- process.stdout.write(` Time: ${this.formatDuration(status.duration)}\n`);
549
- process.stdout.write(` Branch: ${status.pipelineBranch}\n`);
550
- process.stdout.write(` Chat ID: ${status.chatId}\n`);
551
- process.stdout.write(` Depends: ${status.dependsOn.join(', ') || 'None'}\n`);
552
- if (status.waitingFor && status.waitingFor.length > 0) {
553
- process.stdout.write(`\x1b[33m Wait For: ${status.waitingFor.join(', ')}\x1b[0m\n`);
1029
+ const lines = content.split('\n').slice(-10);
1030
+ for (const line of lines) {
1031
+ const formatted = this.formatTerminalLine(line);
1032
+ process.stdout.write(` ${UI.COLORS.dim}${formatted.substring(0, this.screenWidth - 4)}${UI.COLORS.reset}\n`);
1033
+ }
1034
+ }
1035
+ else {
1036
+ process.stdout.write(` ${UI.COLORS.dim}(No output yet)${UI.COLORS.reset}\n`);
1037
+ }
1038
+ // Conversation preview
1039
+ this.renderSectionTitle('Conversation', `${this.currentLogs.length} messages`);
1040
+ const maxVisible = 8;
1041
+ if (this.selectedMessageIndex < this.scrollOffset) {
1042
+ this.scrollOffset = this.selectedMessageIndex;
1043
+ }
1044
+ else if (this.selectedMessageIndex >= this.scrollOffset + maxVisible) {
1045
+ this.scrollOffset = this.selectedMessageIndex - maxVisible + 1;
554
1046
  }
555
- if (status.error) {
556
- process.stdout.write(`\x1b[31m Error: ${status.error}\x1b[0m\n`);
557
- }
558
- console.log('\n🖥️ Live Terminal Output (Last 15 lines):');
559
- console.log('─'.repeat(80));
560
- console.log(`\x1b[90m${liveLog}\x1b[0m`);
561
- console.log('\n💬 Conversation History (Select to see full details):');
562
- console.log('─'.repeat(80));
563
- process.stdout.write(' [↑/↓] Browse | [→/Enter] Full Msg | [I] Intervene | [K] Kill | [T] Live Terminal | [Esc/←] Back\n\n');
564
1047
  if (this.currentLogs.length === 0) {
565
- console.log(' (No messages yet)');
1048
+ process.stdout.write(` ${UI.COLORS.dim}(No messages yet)${UI.COLORS.reset}\n`);
566
1049
  }
567
1050
  else {
568
- // Simple windowed view for long histories
569
- const maxVisible = 15; // Number of messages to show
570
- if (this.selectedMessageIndex < this.scrollOffset) {
571
- this.scrollOffset = this.selectedMessageIndex;
572
- }
573
- else if (this.selectedMessageIndex >= this.scrollOffset + maxVisible) {
574
- this.scrollOffset = this.selectedMessageIndex - maxVisible + 1;
575
- }
576
1051
  const visibleLogs = this.currentLogs.slice(this.scrollOffset, this.scrollOffset + maxVisible);
577
1052
  visibleLogs.forEach((log, i) => {
578
1053
  const actualIndex = i + this.scrollOffset;
579
1054
  const isSelected = actualIndex === this.selectedMessageIndex;
580
- const roleColor = log.role === 'user' ? '\x1b[33m' : log.role === 'reviewer' ? '\x1b[35m' : '\x1b[32m';
1055
+ const roleColor = this.getRoleColor(log.role);
581
1056
  const role = log.role.toUpperCase().padEnd(10);
582
- const prefix = isSelected ? '' : ' ';
583
- const header = `${prefix}${roleColor}${role}\x1b[0m [${new Date(log.timestamp).toLocaleTimeString()}]`;
584
- if (isSelected) {
585
- process.stdout.write(`\x1b[48;5;236m${header}\x1b[0m\n`);
586
- }
587
- else {
588
- process.stdout.write(`${header}\n`);
589
- }
590
- const lines = log.fullText.split('\n').filter(l => l.trim());
591
- const preview = lines[0]?.substring(0, 70) || '...';
592
- process.stdout.write(` ${preview}${log.fullText.length > 70 ? '...' : ''}\n\n`);
1057
+ const ts = new Date(log.timestamp).toLocaleTimeString('en-US', { hour12: false });
1058
+ const prefix = isSelected ? `${UI.COLORS.cyan}▶${UI.COLORS.reset}` : ' ';
1059
+ const bg = isSelected ? UI.COLORS.bgGray : '';
1060
+ const reset = isSelected ? UI.COLORS.reset : '';
1061
+ const preview = log.fullText.replace(/\n/g, ' ').substring(0, 60);
1062
+ process.stdout.write(`${bg}${prefix} ${roleColor}${role}${UI.COLORS.reset} ${UI.COLORS.dim}${ts}${UI.COLORS.reset} ${preview}...${reset}\n`);
593
1063
  });
594
1064
  if (this.currentLogs.length > maxVisible) {
595
- console.log(` -- (${this.currentLogs.length - maxVisible} more messages, use ↑/↓ to scroll) --`);
1065
+ process.stdout.write(` ${UI.COLORS.dim}(${this.currentLogs.length - maxVisible} more messages)${UI.COLORS.reset}\n`);
596
1066
  }
597
1067
  }
598
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
1068
+ this.renderFooter([
1069
+ '[↑↓] Scroll', '[→/Enter] Full Msg', '[T] Terminal', '[I] Intervene', '[K] Kill', '[←/Esc] Back'
1070
+ ]);
1071
+ }
1072
+ getRoleColor(role) {
1073
+ const colors = {
1074
+ user: UI.COLORS.yellow,
1075
+ assistant: UI.COLORS.green,
1076
+ reviewer: UI.COLORS.magenta,
1077
+ intervention: UI.COLORS.red,
1078
+ system: UI.COLORS.cyan,
1079
+ };
1080
+ return colors[role] || UI.COLORS.gray;
599
1081
  }
600
1082
  renderMessageDetail() {
601
1083
  const log = this.currentLogs[this.selectedMessageIndex];
@@ -604,51 +1086,143 @@ class InteractiveMonitor {
604
1086
  this.render();
605
1087
  return;
606
1088
  }
607
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
608
- console.log(`📄 Full Message Detail - ${log.role.toUpperCase()}`);
609
- console.log(`🕒 ${new Date(log.timestamp).toLocaleString()} | [Esc/←] Back to History`);
610
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
611
- const roleColor = log.role === 'user' ? '\x1b[33m' : log.role === 'reviewer' ? '\x1b[35m' : '\x1b[32m';
612
- process.stdout.write(`${roleColor}ROLE: ${log.role.toUpperCase()}\x1b[0m\n`);
1089
+ this.renderHeader('Message Detail', [path.basename(this.runDir), this.selectedLaneName || '', log.role.toUpperCase()]);
1090
+ const roleColor = this.getRoleColor(log.role);
1091
+ const ts = new Date(log.timestamp).toLocaleString();
1092
+ process.stdout.write(`\n`);
1093
+ process.stdout.write(` ${UI.COLORS.dim}Role${UI.COLORS.reset} ${roleColor}${log.role.toUpperCase()}${UI.COLORS.reset}\n`);
1094
+ process.stdout.write(` ${UI.COLORS.dim}Time${UI.COLORS.reset} ${ts}\n`);
613
1095
  if (log.model)
614
- process.stdout.write(`MODEL: ${log.model}\n`);
1096
+ process.stdout.write(` ${UI.COLORS.dim}Model${UI.COLORS.reset} ${log.model}\n`);
615
1097
  if (log.task)
616
- process.stdout.write(`TASK: ${log.task}\n`);
617
- console.log(''.repeat(40));
618
- console.log(log.fullText);
619
- console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
1098
+ process.stdout.write(` ${UI.COLORS.dim}Task${UI.COLORS.reset} ${log.task}\n`);
1099
+ this.renderSectionTitle('Content');
1100
+ // Display message content with wrapping
1101
+ const maxWidth = this.screenWidth - 4;
1102
+ const lines = log.fullText.split('\n');
1103
+ const maxLines = this.screenHeight - 16;
1104
+ let lineCount = 0;
1105
+ for (const line of lines) {
1106
+ if (lineCount >= maxLines) {
1107
+ process.stdout.write(` ${UI.COLORS.dim}... (truncated, ${lines.length - lineCount} more lines)${UI.COLORS.reset}\n`);
1108
+ break;
1109
+ }
1110
+ // Word wrap long lines
1111
+ if (line.length > maxWidth) {
1112
+ const wrapped = this.wrapText(line, maxWidth);
1113
+ for (const wl of wrapped) {
1114
+ if (lineCount >= maxLines)
1115
+ break;
1116
+ process.stdout.write(` ${wl}\n`);
1117
+ lineCount++;
1118
+ }
1119
+ }
1120
+ else {
1121
+ process.stdout.write(` ${line}\n`);
1122
+ lineCount++;
1123
+ }
1124
+ }
1125
+ this.renderFooter(['[←/Esc] Back']);
1126
+ }
1127
+ /**
1128
+ * Wrap text to specified width
1129
+ */
1130
+ wrapText(text, maxWidth) {
1131
+ const words = text.split(' ');
1132
+ const lines = [];
1133
+ let currentLine = '';
1134
+ for (const word of words) {
1135
+ if (currentLine.length + word.length + 1 <= maxWidth) {
1136
+ currentLine += (currentLine ? ' ' : '') + word;
1137
+ }
1138
+ else {
1139
+ if (currentLine)
1140
+ lines.push(currentLine);
1141
+ currentLine = word;
1142
+ }
1143
+ }
1144
+ if (currentLine)
1145
+ lines.push(currentLine);
1146
+ return lines;
620
1147
  }
621
1148
  renderFlow() {
622
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
623
- console.log(`⛓️ Task Dependency Flow`);
624
- console.log(`🕒 Updated: ${new Date().toLocaleTimeString()} | [→/Enter/Esc] Back to List`);
625
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
1149
+ this.renderHeader('Dependency Flow', [path.basename(this.runDir), 'Flow']);
626
1150
  const laneMap = new Map();
627
1151
  this.lanes.forEach(lane => {
628
1152
  laneMap.set(lane.name, this.getLaneStatus(lane.path, lane.name));
629
1153
  });
630
- // Enhanced visualization with box-like structure and clear connections
631
- this.lanes.forEach(lane => {
632
- const status = laneMap.get(lane.name);
633
- const statusIcon = this.getStatusIcon(status.status);
634
- let statusColor = '\x1b[90m'; // Grey for pending/waiting
635
- if (status.status === 'completed')
636
- statusColor = '\x1b[32m'; // Green
637
- if (status.status === 'running')
638
- statusColor = '\x1b[36m'; // Cyan
639
- if (status.status === 'failed')
640
- statusColor = '\x1b[31m'; // Red
641
- // Render the node
642
- const nodeText = `[ ${statusIcon} ${lane.name.padEnd(18)} ]`;
643
- process.stdout.write(` ${statusColor}${nodeText}\x1b[0m`);
644
- // Render dependencies
645
- if (status.dependsOn && status.dependsOn.length > 0) {
646
- process.stdout.write(` \x1b[90m◀───\x1b[0m \x1b[33m( ${status.dependsOn.join(', ')} )\x1b[0m`);
647
- }
648
- process.stdout.write('\n');
649
- });
650
- console.log('\n\x1b[90m (Lanes wait for their dependencies to complete before starting)\x1b[0m');
651
- console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
1154
+ process.stdout.write('\n');
1155
+ // Group lanes by dependency level
1156
+ const levels = this.calculateDependencyLevels();
1157
+ const maxLevelWidth = Math.max(...levels.map(l => l.length));
1158
+ for (let level = 0; level < levels.length; level++) {
1159
+ const lanesAtLevel = levels[level];
1160
+ // Level header
1161
+ process.stdout.write(` ${UI.COLORS.dim}Level ${level}${UI.COLORS.reset}\n`);
1162
+ for (const laneName of lanesAtLevel) {
1163
+ const status = laneMap.get(laneName);
1164
+ const statusIcon = this.getStatusIcon(status?.status || 'pending');
1165
+ let statusColor = UI.COLORS.gray;
1166
+ if (status?.status === 'completed')
1167
+ statusColor = UI.COLORS.green;
1168
+ else if (status?.status === 'running')
1169
+ statusColor = UI.COLORS.cyan;
1170
+ else if (status?.status === 'failed')
1171
+ statusColor = UI.COLORS.red;
1172
+ // Render the node
1173
+ const nodeText = `${statusIcon} ${laneName}`;
1174
+ process.stdout.write(` ${statusColor}${nodeText.padEnd(20)}${UI.COLORS.reset}`);
1175
+ // Render dependencies
1176
+ if (status?.dependsOn?.length > 0) {
1177
+ process.stdout.write(` ${UI.COLORS.dim}←${UI.COLORS.reset} ${UI.COLORS.yellow}${status.dependsOn.join(', ')}${UI.COLORS.reset}`);
1178
+ }
1179
+ process.stdout.write('\n');
1180
+ }
1181
+ if (level < levels.length - 1) {
1182
+ process.stdout.write(` ${UI.COLORS.dim}│${UI.COLORS.reset}\n`);
1183
+ process.stdout.write(` ${UI.COLORS.dim}▼${UI.COLORS.reset}\n`);
1184
+ }
1185
+ }
1186
+ process.stdout.write(`\n ${UI.COLORS.dim}Lanes wait for dependencies to complete before starting${UI.COLORS.reset}\n`);
1187
+ this.renderFooter(['[←/Esc] Back']);
1188
+ }
1189
+ /**
1190
+ * Calculate dependency levels for visualization
1191
+ */
1192
+ calculateDependencyLevels() {
1193
+ const levels = [];
1194
+ const assigned = new Set();
1195
+ // First, find lanes with no dependencies
1196
+ const noDeps = this.lanes.filter(l => !l.dependsOn || l.dependsOn.length === 0);
1197
+ if (noDeps.length > 0) {
1198
+ levels.push(noDeps.map(l => l.name));
1199
+ noDeps.forEach(l => assigned.add(l.name));
1200
+ }
1201
+ // Then assign remaining lanes by dependency completion
1202
+ let maxIterations = 10;
1203
+ while (assigned.size < this.lanes.length && maxIterations-- > 0) {
1204
+ const nextLevel = [];
1205
+ for (const lane of this.lanes) {
1206
+ if (assigned.has(lane.name))
1207
+ continue;
1208
+ // Check if all dependencies are assigned
1209
+ const allDepsAssigned = lane.dependsOn.every(d => assigned.has(d));
1210
+ if (allDepsAssigned) {
1211
+ nextLevel.push(lane.name);
1212
+ }
1213
+ }
1214
+ if (nextLevel.length === 0) {
1215
+ // Remaining lanes have circular deps or missing deps
1216
+ const remaining = this.lanes.filter(l => !assigned.has(l.name)).map(l => l.name);
1217
+ if (remaining.length > 0) {
1218
+ levels.push(remaining);
1219
+ }
1220
+ break;
1221
+ }
1222
+ levels.push(nextLevel);
1223
+ nextLevel.forEach(n => assigned.add(n));
1224
+ }
1225
+ return levels;
652
1226
  }
653
1227
  renderTerminal() {
654
1228
  const lane = this.lanes.find(l => l.name === this.selectedLaneName);
@@ -657,18 +1231,35 @@ class InteractiveMonitor {
657
1231
  this.render();
658
1232
  return;
659
1233
  }
660
- const logPath = (0, path_1.safeJoin)(lane.path, 'terminal.log');
1234
+ this.renderHeader('Live Terminal', [path.basename(this.runDir), lane.name, 'Terminal']);
1235
+ // Get logs based on format mode
661
1236
  let logLines = [];
662
- if (fs.existsSync(logPath)) {
663
- const content = fs.readFileSync(logPath, 'utf8');
664
- logLines = content.split('\n');
1237
+ let totalLines = 0;
1238
+ if (this.readableFormat) {
1239
+ // Use JSONL for readable format
1240
+ const jsonlPath = (0, path_1.safeJoin)(lane.path, 'terminal.jsonl');
1241
+ logLines = this.getReadableLogLines(jsonlPath, lane.name);
1242
+ totalLines = logLines.length;
665
1243
  }
666
- const maxVisible = 40;
667
- const totalLines = logLines.length;
668
- // Sticky scroll logic: if new lines arrived and we are already scrolled up,
669
- // increase the offset to stay on the same content.
670
- if (this.terminalScrollOffset > 0 && this.lastTerminalTotalLines > 0 && totalLines > this.lastTerminalTotalLines) {
671
- this.terminalScrollOffset += (totalLines - this.lastTerminalTotalLines);
1244
+ else {
1245
+ // Use raw log
1246
+ const logPath = (0, path_1.safeJoin)(lane.path, 'terminal.log');
1247
+ if (fs.existsSync(logPath)) {
1248
+ const content = fs.readFileSync(logPath, 'utf8');
1249
+ logLines = content.split('\n');
1250
+ totalLines = logLines.length;
1251
+ }
1252
+ }
1253
+ const maxVisible = this.screenHeight - 10;
1254
+ // Follow mode logic
1255
+ if (this.followMode) {
1256
+ this.terminalScrollOffset = 0;
1257
+ }
1258
+ else {
1259
+ if (this.lastTerminalTotalLines > 0 && totalLines > this.lastTerminalTotalLines) {
1260
+ this.unseenLineCount += (totalLines - this.lastTerminalTotalLines);
1261
+ this.terminalScrollOffset += (totalLines - this.lastTerminalTotalLines);
1262
+ }
672
1263
  }
673
1264
  this.lastTerminalTotalLines = totalLines;
674
1265
  // Clamp scroll offset
@@ -676,55 +1267,283 @@ class InteractiveMonitor {
676
1267
  if (this.terminalScrollOffset > maxScroll) {
677
1268
  this.terminalScrollOffset = maxScroll;
678
1269
  }
1270
+ // Mode and status indicators
1271
+ const formatMode = this.readableFormat
1272
+ ? `${UI.COLORS.green}[R] Readable ✓${UI.COLORS.reset}`
1273
+ : `${UI.COLORS.dim}[R] Raw${UI.COLORS.reset}`;
1274
+ const followStatus = this.followMode
1275
+ ? `${UI.COLORS.green}[F] Follow ✓${UI.COLORS.reset}`
1276
+ : `${UI.COLORS.yellow}[F] Follow OFF${this.unseenLineCount > 0 ? ` (↓${this.unseenLineCount})` : ''}${UI.COLORS.reset}`;
1277
+ process.stdout.write(` ${formatMode} ${followStatus} ${UI.COLORS.dim}Lines: ${totalLines}${UI.COLORS.reset}\n\n`);
679
1278
  // Slice based on scroll (0 means bottom, >0 means scrolled up)
680
1279
  const end = totalLines - this.terminalScrollOffset;
681
1280
  const start = Math.max(0, end - maxVisible);
682
1281
  const visibleLines = logLines.slice(start, end);
683
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
684
- console.log(`🖥️ Full Live Terminal: ${lane.name}`);
685
- console.log(`🕒 Streaming... | [↑/↓] Scroll (${this.terminalScrollOffset > 0 ? `Scrolled Up ${this.terminalScrollOffset}` : 'Bottom'}) | [I] Intervene | [T/Esc/←] Back`);
686
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
687
- visibleLines.forEach(line => {
688
- let formattedLine = line;
689
- // Highlight human intervention
690
- if (line.includes('[HUMAN INTERVENTION]') || line.includes('Injecting intervention:')) {
691
- formattedLine = `\x1b[33m\x1b[1m${line}\x1b[0m`;
692
- }
693
- // Highlight agent execution starts
694
- else if (line.includes('Executing cursor-agent')) {
695
- formattedLine = `\x1b[36m\x1b[1m${line}\x1b[0m`;
696
- }
697
- // Highlight task headers
698
- else if (line.includes('=== Task:')) {
699
- formattedLine = `\x1b[32m\x1b[1m${line}\x1b[0m`;
700
- }
701
- // Highlight errors
702
- else if (line.toLowerCase().includes('error') || line.toLowerCase().includes('failed')) {
703
- formattedLine = `\x1b[31m${line}\x1b[0m`;
704
- }
705
- process.stdout.write(` ${formattedLine}\n`);
706
- });
707
- console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
1282
+ for (const line of visibleLines) {
1283
+ const formatted = this.readableFormat ? line : this.formatTerminalLine(line);
1284
+ // Truncate to screen width
1285
+ const displayLine = formatted.length > this.screenWidth - 2
1286
+ ? formatted.substring(0, this.screenWidth - 5) + '...'
1287
+ : formatted;
1288
+ process.stdout.write(` ${displayLine}\n`);
1289
+ }
1290
+ if (visibleLines.length === 0) {
1291
+ process.stdout.write(` ${UI.COLORS.dim}(No output yet)${UI.COLORS.reset}\n`);
1292
+ }
1293
+ this.renderFooter([
1294
+ '[↑↓] Scroll', '[F] Follow', '[R] Toggle Readable', '[I] Intervene', '[←/Esc] Back'
1295
+ ]);
1296
+ }
1297
+ /**
1298
+ * Format a raw terminal line with syntax highlighting
1299
+ */
1300
+ formatTerminalLine(line) {
1301
+ // Highlight patterns
1302
+ if (line.includes('[HUMAN INTERVENTION]') || line.includes('Injecting intervention:')) {
1303
+ return `${UI.COLORS.yellow}${UI.COLORS.bold}${line}${UI.COLORS.reset}`;
1304
+ }
1305
+ if (line.includes('Executing cursor-agent')) {
1306
+ return `${UI.COLORS.cyan}${UI.COLORS.bold}${line}${UI.COLORS.reset}`;
1307
+ }
1308
+ if (line.includes('=== Task:') || line.includes('Starting task:')) {
1309
+ return `${UI.COLORS.green}${UI.COLORS.bold}${line}${UI.COLORS.reset}`;
1310
+ }
1311
+ if (line.toLowerCase().includes('error') || line.toLowerCase().includes('failed')) {
1312
+ return `${UI.COLORS.red}${line}${UI.COLORS.reset}`;
1313
+ }
1314
+ if (line.toLowerCase().includes('success') || line.toLowerCase().includes('completed')) {
1315
+ return `${UI.COLORS.green}${line}${UI.COLORS.reset}`;
1316
+ }
1317
+ return line;
1318
+ }
1319
+ /**
1320
+ * Get readable log lines from JSONL file
1321
+ */
1322
+ getReadableLogLines(jsonlPath, laneName) {
1323
+ if (!fs.existsSync(jsonlPath)) {
1324
+ // Fallback: try to read raw log
1325
+ const rawPath = jsonlPath.replace('.jsonl', '.log');
1326
+ if (fs.existsSync(rawPath)) {
1327
+ return fs.readFileSync(rawPath, 'utf8').split('\n').map(l => this.formatTerminalLine(l));
1328
+ }
1329
+ return [];
1330
+ }
1331
+ try {
1332
+ const content = fs.readFileSync(jsonlPath, 'utf8');
1333
+ const lines = content.split('\n').filter(l => l.trim());
1334
+ return lines.map(line => {
1335
+ try {
1336
+ const entry = JSON.parse(line);
1337
+ const ts = new Date(entry.timestamp || Date.now()).toLocaleTimeString('en-US', { hour12: false });
1338
+ const type = (entry.type || 'info').toLowerCase();
1339
+ const content = entry.content || entry.message || '';
1340
+ // Format based on type
1341
+ const typeInfo = this.getLogTypeInfo(type);
1342
+ const preview = content.replace(/\n/g, ' ').substring(0, 100);
1343
+ return `${UI.COLORS.dim}[${ts}]${UI.COLORS.reset} ${typeInfo.color}[${typeInfo.label}]${UI.COLORS.reset} ${preview}`;
1344
+ }
1345
+ catch {
1346
+ return this.formatTerminalLine(line);
1347
+ }
1348
+ });
1349
+ }
1350
+ catch {
1351
+ return [];
1352
+ }
1353
+ }
1354
+ /**
1355
+ * Get log type display info
1356
+ */
1357
+ getLogTypeInfo(type) {
1358
+ const typeMap = {
1359
+ user: { label: 'USER ', color: UI.COLORS.cyan },
1360
+ assistant: { label: 'ASST ', color: UI.COLORS.green },
1361
+ tool: { label: 'TOOL ', color: UI.COLORS.yellow },
1362
+ tool_result: { label: 'RESULT', color: UI.COLORS.gray },
1363
+ result: { label: 'DONE ', color: UI.COLORS.green },
1364
+ system: { label: 'SYSTEM', color: UI.COLORS.gray },
1365
+ thinking: { label: 'THINK ', color: UI.COLORS.dim },
1366
+ error: { label: 'ERROR ', color: UI.COLORS.red },
1367
+ stderr: { label: 'STDERR', color: UI.COLORS.red },
1368
+ stdout: { label: 'STDOUT', color: UI.COLORS.white },
1369
+ };
1370
+ return typeMap[type] || { label: type.toUpperCase().padEnd(6).substring(0, 6), color: UI.COLORS.gray };
708
1371
  }
709
1372
  renderIntervene() {
710
- console.clear();
711
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
712
- console.log(`🙋 HUMAN INTERVENTION: ${this.selectedLaneName}`);
713
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
714
- console.log('\n Type your message to the agent. This will be sent as a direct prompt.');
715
- console.log(` Press \x1b[1mENTER\x1b[0m to send, \x1b[1mESC\x1b[0m to cancel.\n`);
716
- console.log(`\x1b[33m > \x1b[0m${this.interventionInput}\x1b[37m█\x1b[0m`);
717
- console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
1373
+ this.renderHeader('Human Intervention', [path.basename(this.runDir), this.selectedLaneName || '', 'Intervene']);
1374
+ process.stdout.write(`\n`);
1375
+ process.stdout.write(` ${UI.COLORS.yellow}Send a message directly to the agent.${UI.COLORS.reset}\n`);
1376
+ process.stdout.write(` ${UI.COLORS.dim}This will interrupt the current flow and inject your instruction.${UI.COLORS.reset}\n\n`);
1377
+ // Input box
1378
+ const width = Math.min(this.screenWidth - 8, 80);
1379
+ process.stdout.write(` ${UI.COLORS.cyan}┌${'─'.repeat(width)}┐${UI.COLORS.reset}\n`);
1380
+ // Wrap input text
1381
+ const inputLines = this.wrapText(this.interventionInput || ' ', width - 4);
1382
+ for (const line of inputLines) {
1383
+ process.stdout.write(` ${UI.COLORS.cyan}│${UI.COLORS.reset} ${line.padEnd(width - 2)} ${UI.COLORS.cyan}│${UI.COLORS.reset}\n`);
1384
+ }
1385
+ if (inputLines.length === 0 || inputLines[inputLines.length - 1] === ' ') {
1386
+ process.stdout.write(` ${UI.COLORS.cyan}│${UI.COLORS.reset} ${UI.COLORS.white}█${UI.COLORS.reset}${' '.repeat(width - 3)} ${UI.COLORS.cyan}│${UI.COLORS.reset}\n`);
1387
+ }
1388
+ process.stdout.write(` ${UI.COLORS.cyan}└${'─'.repeat(width)}┘${UI.COLORS.reset}\n`);
1389
+ this.renderFooter(['[Enter] Send', '[Esc] Cancel']);
718
1390
  }
719
1391
  renderTimeout() {
720
- console.clear();
721
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
722
- console.log(`⏱ UPDATE TIMEOUT: ${this.selectedLaneName}`);
723
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
724
- console.log('\n Enter new timeout in milliseconds (e.g., 600000 for 10 minutes).');
725
- console.log(` Press \x1b[1mENTER\x1b[0m to apply, \x1b[1mESC\x1b[0m to cancel.\n`);
726
- console.log(`\x1b[33m > \x1b[0m${this.timeoutInput}\x1b[37m█\x1b[0m`);
727
- console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
1392
+ this.renderHeader('Update Timeout', [path.basename(this.runDir), this.selectedLaneName || '', 'Timeout']);
1393
+ process.stdout.write(`\n`);
1394
+ process.stdout.write(` ${UI.COLORS.yellow}Update the task timeout for this lane.${UI.COLORS.reset}\n`);
1395
+ process.stdout.write(` ${UI.COLORS.dim}Enter timeout in milliseconds (e.g., 600000 = 10 minutes)${UI.COLORS.reset}\n\n`);
1396
+ // Common presets
1397
+ process.stdout.write(` ${UI.COLORS.dim}Presets: 300000 (5m) | 600000 (10m) | 1800000 (30m) | 3600000 (1h)${UI.COLORS.reset}\n\n`);
1398
+ // Input box
1399
+ const width = 40;
1400
+ process.stdout.write(` ${UI.COLORS.cyan}┌${'─'.repeat(width)}┐${UI.COLORS.reset}\n`);
1401
+ process.stdout.write(` ${UI.COLORS.cyan}│${UI.COLORS.reset} ${(this.timeoutInput || '').padEnd(width - 2)}${UI.COLORS.white}█${UI.COLORS.reset} ${UI.COLORS.cyan}│${UI.COLORS.reset}\n`);
1402
+ process.stdout.write(` ${UI.COLORS.cyan}└${'─'.repeat(width)}┘${UI.COLORS.reset}\n`);
1403
+ // Show human-readable interpretation
1404
+ if (this.timeoutInput) {
1405
+ const ms = parseInt(this.timeoutInput);
1406
+ if (!isNaN(ms) && ms > 0) {
1407
+ const formatted = this.formatDuration(ms);
1408
+ process.stdout.write(`\n ${UI.COLORS.green}= ${formatted}${UI.COLORS.reset}\n`);
1409
+ }
1410
+ }
1411
+ this.renderFooter(['[Enter] Apply', '[Esc] Cancel']);
1412
+ }
1413
+ /**
1414
+ * Render unified log view - all lanes combined
1415
+ */
1416
+ renderUnifiedLog() {
1417
+ this.renderHeader('Unified Logs', [path.basename(this.runDir), 'All Lanes']);
1418
+ const bufferState = this.unifiedLogBuffer?.getState();
1419
+ const totalEntries = bufferState?.totalEntries || 0;
1420
+ const availableLanes = bufferState?.lanes || [];
1421
+ // Status bar
1422
+ const formatMode = this.readableFormat
1423
+ ? `${UI.COLORS.green}[R] Readable ✓${UI.COLORS.reset}`
1424
+ : `${UI.COLORS.dim}[R] Compact${UI.COLORS.reset}`;
1425
+ const followStatus = this.unifiedLogFollowMode
1426
+ ? `${UI.COLORS.green}[F] Follow ✓${UI.COLORS.reset}`
1427
+ : `${UI.COLORS.yellow}[F] Follow OFF${UI.COLORS.reset}`;
1428
+ const filterStatus = this.laneFilter
1429
+ ? `${UI.COLORS.cyan}[L] ${this.laneFilter}${UI.COLORS.reset}`
1430
+ : `${UI.COLORS.dim}[L] All Lanes${UI.COLORS.reset}`;
1431
+ process.stdout.write(` ${formatMode} ${followStatus} ${filterStatus} ${UI.COLORS.dim}Total: ${totalEntries}${UI.COLORS.reset}\n`);
1432
+ // Lane list for filtering hint
1433
+ if (availableLanes.length > 1) {
1434
+ process.stdout.write(` ${UI.COLORS.dim}Lanes: ${availableLanes.join(', ')}${UI.COLORS.reset}\n`);
1435
+ }
1436
+ process.stdout.write('\n');
1437
+ if (!this.unifiedLogBuffer) {
1438
+ process.stdout.write(` ${UI.COLORS.dim}(No log buffer available)${UI.COLORS.reset}\n`);
1439
+ this.renderFooter(['[U/Esc] Back', '[Q] Quit']);
1440
+ return;
1441
+ }
1442
+ const pageSize = this.screenHeight - 12;
1443
+ const filter = this.laneFilter ? { lane: this.laneFilter } : undefined;
1444
+ const entries = this.unifiedLogBuffer.getEntries({
1445
+ offset: this.unifiedLogScrollOffset,
1446
+ limit: pageSize,
1447
+ filter,
1448
+ fromEnd: true,
1449
+ });
1450
+ if (entries.length === 0) {
1451
+ process.stdout.write(` ${UI.COLORS.dim}(No log entries yet)${UI.COLORS.reset}\n`);
1452
+ }
1453
+ else {
1454
+ for (const entry of entries) {
1455
+ const formatted = this.formatUnifiedLogEntry(entry);
1456
+ const displayLine = formatted.length > this.screenWidth - 2
1457
+ ? formatted.substring(0, this.screenWidth - 5) + '...'
1458
+ : formatted;
1459
+ process.stdout.write(` ${displayLine}\n`);
1460
+ }
1461
+ }
1462
+ this.renderFooter([
1463
+ '[↑↓/PgUp/PgDn] Scroll', '[F] Follow', '[R] Readable', '[L] Filter Lane', '[U/Esc] Back'
1464
+ ]);
1465
+ }
1466
+ /**
1467
+ * Format a unified log entry
1468
+ */
1469
+ formatUnifiedLogEntry(entry) {
1470
+ const ts = entry.timestamp.toLocaleTimeString('en-US', { hour12: false });
1471
+ const lane = entry.laneName.padEnd(12);
1472
+ const typeInfo = this.getLogTypeInfo(entry.type || 'info');
1473
+ if (this.readableFormat) {
1474
+ // Readable format: show more context
1475
+ const content = entry.message.replace(/\n/g, ' ');
1476
+ return `${UI.COLORS.dim}[${ts}]${UI.COLORS.reset} ${entry.laneColor}[${lane}]${UI.COLORS.reset} ${typeInfo.color}[${typeInfo.label}]${UI.COLORS.reset} ${content}`;
1477
+ }
1478
+ else {
1479
+ // Compact format
1480
+ const preview = entry.message.replace(/\n/g, ' ').substring(0, 60);
1481
+ return `${UI.COLORS.dim}${ts}${UI.COLORS.reset} ${entry.laneColor}${entry.laneName.substring(0, 8).padEnd(8)}${UI.COLORS.reset} ${typeInfo.color}${typeInfo.label.trim().substring(0, 4)}${UI.COLORS.reset} ${preview}`;
1482
+ }
1483
+ }
1484
+ /**
1485
+ * Render multiple flows dashboard
1486
+ */
1487
+ renderFlowsDashboard() {
1488
+ this.renderHeader('All Flows', ['Flows Dashboard']);
1489
+ process.stdout.write(` ${UI.COLORS.dim}Total: ${this.allFlows.length} flows${UI.COLORS.reset}\n\n`);
1490
+ if (this.allFlows.length === 0) {
1491
+ process.stdout.write(` ${UI.COLORS.dim}No flow runs found.${UI.COLORS.reset}\n\n`);
1492
+ process.stdout.write(` Run ${UI.COLORS.cyan}cursorflow run${UI.COLORS.reset} to start a new flow.\n`);
1493
+ this.renderFooter(['[M/Esc] Back', '[Q] Quit']);
1494
+ return;
1495
+ }
1496
+ // Header
1497
+ process.stdout.write(` ${'Status'.padEnd(8)} ${'Run ID'.padEnd(32)} ${'Lanes'.padEnd(12)} Progress\n`);
1498
+ process.stdout.write(` ${'─'.repeat(8)} ${'─'.repeat(32)} ${'─'.repeat(12)} ${'─'.repeat(20)}\n`);
1499
+ const maxVisible = this.screenHeight - 14;
1500
+ const startIdx = Math.max(0, this.selectedFlowIndex - Math.floor(maxVisible / 2));
1501
+ const endIdx = Math.min(this.allFlows.length, startIdx + maxVisible);
1502
+ for (let i = startIdx; i < endIdx; i++) {
1503
+ const flow = this.allFlows[i];
1504
+ const isSelected = i === this.selectedFlowIndex;
1505
+ const isCurrent = flow.runDir === this.runDir;
1506
+ // Status icon based on flow state
1507
+ let statusIcon = '⚪';
1508
+ if (flow.isAlive) {
1509
+ statusIcon = '🟢';
1510
+ }
1511
+ else if (flow.summary.completed === flow.summary.total && flow.summary.total > 0) {
1512
+ statusIcon = '✅';
1513
+ }
1514
+ else if (flow.summary.failed > 0 || flow.summary.dead > 0) {
1515
+ statusIcon = '🔴';
1516
+ }
1517
+ // Lanes summary
1518
+ const lanesSummary = [
1519
+ flow.summary.running > 0 ? `${UI.COLORS.cyan}${flow.summary.running}R${UI.COLORS.reset}` : '',
1520
+ flow.summary.completed > 0 ? `${UI.COLORS.green}${flow.summary.completed}C${UI.COLORS.reset}` : '',
1521
+ flow.summary.failed > 0 ? `${UI.COLORS.red}${flow.summary.failed}F${UI.COLORS.reset}` : '',
1522
+ flow.summary.dead > 0 ? `${UI.COLORS.yellow}${flow.summary.dead}D${UI.COLORS.reset}` : '',
1523
+ ].filter(Boolean).join('/') || '0';
1524
+ // Progress bar
1525
+ const total = flow.summary.total || 1;
1526
+ const completed = flow.summary.completed;
1527
+ const ratio = completed / total;
1528
+ const barWidth = 12;
1529
+ const filled = Math.round(ratio * barWidth);
1530
+ const progressBar = `${UI.COLORS.green}${'█'.repeat(filled)}${UI.COLORS.reset}${UI.COLORS.gray}${'░'.repeat(barWidth - filled)}${UI.COLORS.reset}`;
1531
+ const pct = `${Math.round(ratio * 100)}%`;
1532
+ // Display
1533
+ const prefix = isSelected ? ` ${UI.COLORS.cyan}▶${UI.COLORS.reset} ` : ' ';
1534
+ const currentTag = isCurrent ? ` ${UI.COLORS.cyan}●${UI.COLORS.reset}` : '';
1535
+ const bg = isSelected ? UI.COLORS.bgGray : '';
1536
+ const resetBg = isSelected ? UI.COLORS.reset : '';
1537
+ // Truncate run ID if needed
1538
+ const runIdDisplay = flow.runId.length > 30 ? flow.runId.substring(0, 27) + '...' : flow.runId.padEnd(30);
1539
+ process.stdout.write(`${bg}${prefix}${statusIcon} ${runIdDisplay} ${lanesSummary.padEnd(12 + 30)} ${progressBar} ${pct}${currentTag}${resetBg}\n`);
1540
+ }
1541
+ if (this.allFlows.length > maxVisible) {
1542
+ process.stdout.write(`\n ${UI.COLORS.dim}(${this.allFlows.length - maxVisible} more flows, scroll to see)${UI.COLORS.reset}\n`);
1543
+ }
1544
+ this.renderFooter([
1545
+ '[↑↓] Select', '[→/Enter] Switch', '[D] Delete', '[R] Refresh', '[M/Esc] Back', '[Q] Quit'
1546
+ ]);
728
1547
  }
729
1548
  listLanesWithDeps(runDir) {
730
1549
  const lanesDir = (0, path_1.safeJoin)(runDir, 'lanes');
@@ -833,7 +1652,6 @@ async function monitor(args) {
833
1652
  printHelp();
834
1653
  return;
835
1654
  }
836
- const watchIdx = args.indexOf('--watch');
837
1655
  const intervalIdx = args.indexOf('--interval');
838
1656
  const interval = intervalIdx >= 0 ? parseInt(args[intervalIdx + 1] || '2') || 2 : 2;
839
1657
  const runDirArg = args.find(arg => !arg.startsWith('--') && args.indexOf(arg) !== intervalIdx + 1);