@litmers/cursorflow-orchestrator 0.1.18 → 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 (234) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +25 -7
  3. package/commands/cursorflow-clean.md +19 -0
  4. package/commands/cursorflow-runs.md +59 -0
  5. package/commands/cursorflow-stop.md +55 -0
  6. package/dist/cli/clean.js +178 -6
  7. package/dist/cli/clean.js.map +1 -1
  8. package/dist/cli/index.js +12 -1
  9. package/dist/cli/index.js.map +1 -1
  10. package/dist/cli/init.js +8 -7
  11. package/dist/cli/init.js.map +1 -1
  12. package/dist/cli/logs.js +126 -77
  13. package/dist/cli/logs.js.map +1 -1
  14. package/dist/cli/monitor.d.ts +7 -0
  15. package/dist/cli/monitor.js +1021 -202
  16. package/dist/cli/monitor.js.map +1 -1
  17. package/dist/cli/prepare.js +39 -21
  18. package/dist/cli/prepare.js.map +1 -1
  19. package/dist/cli/resume.js +268 -163
  20. package/dist/cli/resume.js.map +1 -1
  21. package/dist/cli/run.js +11 -5
  22. package/dist/cli/run.js.map +1 -1
  23. package/dist/cli/runs.d.ts +5 -0
  24. package/dist/cli/runs.js +214 -0
  25. package/dist/cli/runs.js.map +1 -0
  26. package/dist/cli/setup-commands.js +0 -0
  27. package/dist/cli/signal.js +8 -8
  28. package/dist/cli/signal.js.map +1 -1
  29. package/dist/cli/stop.d.ts +5 -0
  30. package/dist/cli/stop.js +215 -0
  31. package/dist/cli/stop.js.map +1 -0
  32. package/dist/cli/tasks.d.ts +10 -0
  33. package/dist/cli/tasks.js +165 -0
  34. package/dist/cli/tasks.js.map +1 -0
  35. package/dist/core/auto-recovery.d.ts +212 -0
  36. package/dist/core/auto-recovery.js +737 -0
  37. package/dist/core/auto-recovery.js.map +1 -0
  38. package/dist/core/failure-policy.d.ts +156 -0
  39. package/dist/core/failure-policy.js +488 -0
  40. package/dist/core/failure-policy.js.map +1 -0
  41. package/dist/core/orchestrator.d.ts +16 -2
  42. package/dist/core/orchestrator.js +439 -105
  43. package/dist/core/orchestrator.js.map +1 -1
  44. package/dist/core/reviewer.d.ts +2 -0
  45. package/dist/core/reviewer.js +2 -0
  46. package/dist/core/reviewer.js.map +1 -1
  47. package/dist/core/runner.d.ts +33 -10
  48. package/dist/core/runner.js +374 -164
  49. package/dist/core/runner.js.map +1 -1
  50. package/dist/services/logging/buffer.d.ts +67 -0
  51. package/dist/services/logging/buffer.js +309 -0
  52. package/dist/services/logging/buffer.js.map +1 -0
  53. package/dist/services/logging/console.d.ts +89 -0
  54. package/dist/services/logging/console.js +169 -0
  55. package/dist/services/logging/console.js.map +1 -0
  56. package/dist/services/logging/file-writer.d.ts +71 -0
  57. package/dist/services/logging/file-writer.js +516 -0
  58. package/dist/services/logging/file-writer.js.map +1 -0
  59. package/dist/services/logging/formatter.d.ts +39 -0
  60. package/dist/services/logging/formatter.js +227 -0
  61. package/dist/services/logging/formatter.js.map +1 -0
  62. package/dist/services/logging/index.d.ts +11 -0
  63. package/dist/services/logging/index.js +30 -0
  64. package/dist/services/logging/index.js.map +1 -0
  65. package/dist/services/logging/parser.d.ts +31 -0
  66. package/dist/services/logging/parser.js +222 -0
  67. package/dist/services/logging/parser.js.map +1 -0
  68. package/dist/services/process/index.d.ts +59 -0
  69. package/dist/services/process/index.js +257 -0
  70. package/dist/services/process/index.js.map +1 -0
  71. package/dist/types/agent.d.ts +20 -0
  72. package/dist/types/agent.js +6 -0
  73. package/dist/types/agent.js.map +1 -0
  74. package/dist/types/config.d.ts +65 -0
  75. package/dist/types/config.js +6 -0
  76. package/dist/types/config.js.map +1 -0
  77. package/dist/types/events.d.ts +125 -0
  78. package/dist/types/events.js +6 -0
  79. package/dist/types/events.js.map +1 -0
  80. package/dist/types/index.d.ts +12 -0
  81. package/dist/types/index.js +37 -0
  82. package/dist/types/index.js.map +1 -0
  83. package/dist/types/lane.d.ts +43 -0
  84. package/dist/types/lane.js +6 -0
  85. package/dist/types/lane.js.map +1 -0
  86. package/dist/types/logging.d.ts +71 -0
  87. package/dist/types/logging.js +16 -0
  88. package/dist/types/logging.js.map +1 -0
  89. package/dist/types/review.d.ts +17 -0
  90. package/dist/types/review.js +6 -0
  91. package/dist/types/review.js.map +1 -0
  92. package/dist/types/run.d.ts +32 -0
  93. package/dist/types/run.js +6 -0
  94. package/dist/types/run.js.map +1 -0
  95. package/dist/types/task.d.ts +71 -0
  96. package/dist/types/task.js +6 -0
  97. package/dist/types/task.js.map +1 -0
  98. package/dist/ui/components.d.ts +134 -0
  99. package/dist/ui/components.js +389 -0
  100. package/dist/ui/components.js.map +1 -0
  101. package/dist/ui/log-viewer.d.ts +49 -0
  102. package/dist/ui/log-viewer.js +449 -0
  103. package/dist/ui/log-viewer.js.map +1 -0
  104. package/dist/utils/checkpoint.d.ts +87 -0
  105. package/dist/utils/checkpoint.js +317 -0
  106. package/dist/utils/checkpoint.js.map +1 -0
  107. package/dist/utils/config.d.ts +4 -0
  108. package/dist/utils/config.js +18 -8
  109. package/dist/utils/config.js.map +1 -1
  110. package/dist/utils/cursor-agent.js.map +1 -1
  111. package/dist/utils/dependency.d.ts +74 -0
  112. package/dist/utils/dependency.js +420 -0
  113. package/dist/utils/dependency.js.map +1 -0
  114. package/dist/utils/doctor.js +17 -11
  115. package/dist/utils/doctor.js.map +1 -1
  116. package/dist/utils/enhanced-logger.d.ts +10 -33
  117. package/dist/utils/enhanced-logger.js +108 -20
  118. package/dist/utils/enhanced-logger.js.map +1 -1
  119. package/dist/utils/git.d.ts +121 -0
  120. package/dist/utils/git.js +484 -11
  121. package/dist/utils/git.js.map +1 -1
  122. package/dist/utils/health.d.ts +91 -0
  123. package/dist/utils/health.js +556 -0
  124. package/dist/utils/health.js.map +1 -0
  125. package/dist/utils/lock.d.ts +95 -0
  126. package/dist/utils/lock.js +332 -0
  127. package/dist/utils/lock.js.map +1 -0
  128. package/dist/utils/log-buffer.d.ts +17 -0
  129. package/dist/utils/log-buffer.js +14 -0
  130. package/dist/utils/log-buffer.js.map +1 -0
  131. package/dist/utils/log-constants.d.ts +23 -0
  132. package/dist/utils/log-constants.js +28 -0
  133. package/dist/utils/log-constants.js.map +1 -0
  134. package/dist/utils/log-formatter.d.ts +25 -0
  135. package/dist/utils/log-formatter.js +237 -0
  136. package/dist/utils/log-formatter.js.map +1 -0
  137. package/dist/utils/log-service.d.ts +19 -0
  138. package/dist/utils/log-service.js +47 -0
  139. package/dist/utils/log-service.js.map +1 -0
  140. package/dist/utils/logger.d.ts +46 -27
  141. package/dist/utils/logger.js +82 -60
  142. package/dist/utils/logger.js.map +1 -1
  143. package/dist/utils/path.d.ts +19 -0
  144. package/dist/utils/path.js +77 -0
  145. package/dist/utils/path.js.map +1 -0
  146. package/dist/utils/process-manager.d.ts +21 -0
  147. package/dist/utils/process-manager.js +138 -0
  148. package/dist/utils/process-manager.js.map +1 -0
  149. package/dist/utils/retry.d.ts +121 -0
  150. package/dist/utils/retry.js +374 -0
  151. package/dist/utils/retry.js.map +1 -0
  152. package/dist/utils/run-service.d.ts +88 -0
  153. package/dist/utils/run-service.js +412 -0
  154. package/dist/utils/run-service.js.map +1 -0
  155. package/dist/utils/state.d.ts +62 -3
  156. package/dist/utils/state.js +317 -11
  157. package/dist/utils/state.js.map +1 -1
  158. package/dist/utils/task-service.d.ts +82 -0
  159. package/dist/utils/task-service.js +348 -0
  160. package/dist/utils/task-service.js.map +1 -0
  161. package/dist/utils/template.d.ts +14 -0
  162. package/dist/utils/template.js +122 -0
  163. package/dist/utils/template.js.map +1 -0
  164. package/dist/utils/types.d.ts +2 -271
  165. package/dist/utils/types.js +16 -0
  166. package/dist/utils/types.js.map +1 -1
  167. package/package.json +38 -23
  168. package/scripts/ai-security-check.js +0 -1
  169. package/scripts/local-security-gate.sh +0 -0
  170. package/scripts/monitor-lanes.sh +94 -0
  171. package/scripts/patches/test-cursor-agent.js +0 -1
  172. package/scripts/release.sh +0 -0
  173. package/scripts/setup-security.sh +0 -0
  174. package/scripts/stream-logs.sh +72 -0
  175. package/scripts/verify-and-fix.sh +0 -0
  176. package/src/cli/clean.ts +187 -6
  177. package/src/cli/index.ts +12 -1
  178. package/src/cli/init.ts +8 -7
  179. package/src/cli/logs.ts +124 -77
  180. package/src/cli/monitor.ts +1815 -898
  181. package/src/cli/prepare.ts +41 -21
  182. package/src/cli/resume.ts +753 -626
  183. package/src/cli/run.ts +12 -5
  184. package/src/cli/runs.ts +212 -0
  185. package/src/cli/setup-commands.ts +0 -0
  186. package/src/cli/signal.ts +8 -7
  187. package/src/cli/stop.ts +209 -0
  188. package/src/cli/tasks.ts +154 -0
  189. package/src/core/auto-recovery.ts +909 -0
  190. package/src/core/failure-policy.ts +592 -0
  191. package/src/core/orchestrator.ts +1131 -704
  192. package/src/core/reviewer.ts +4 -0
  193. package/src/core/runner.ts +444 -180
  194. package/src/services/logging/buffer.ts +326 -0
  195. package/src/services/logging/console.ts +193 -0
  196. package/src/services/logging/file-writer.ts +526 -0
  197. package/src/services/logging/formatter.ts +268 -0
  198. package/src/services/logging/index.ts +16 -0
  199. package/src/services/logging/parser.ts +232 -0
  200. package/src/services/process/index.ts +261 -0
  201. package/src/types/agent.ts +24 -0
  202. package/src/types/config.ts +79 -0
  203. package/src/types/events.ts +156 -0
  204. package/src/types/index.ts +29 -0
  205. package/src/types/lane.ts +56 -0
  206. package/src/types/logging.ts +96 -0
  207. package/src/types/review.ts +20 -0
  208. package/src/types/run.ts +37 -0
  209. package/src/types/task.ts +79 -0
  210. package/src/ui/components.ts +430 -0
  211. package/src/ui/log-viewer.ts +485 -0
  212. package/src/utils/checkpoint.ts +374 -0
  213. package/src/utils/config.ts +18 -8
  214. package/src/utils/cursor-agent.ts +1 -1
  215. package/src/utils/dependency.ts +482 -0
  216. package/src/utils/doctor.ts +18 -11
  217. package/src/utils/enhanced-logger.ts +122 -60
  218. package/src/utils/git.ts +517 -11
  219. package/src/utils/health.ts +596 -0
  220. package/src/utils/lock.ts +346 -0
  221. package/src/utils/log-buffer.ts +28 -0
  222. package/src/utils/log-constants.ts +26 -0
  223. package/src/utils/log-formatter.ts +245 -0
  224. package/src/utils/log-service.ts +49 -0
  225. package/src/utils/logger.ts +100 -51
  226. package/src/utils/path.ts +45 -0
  227. package/src/utils/process-manager.ts +100 -0
  228. package/src/utils/retry.ts +413 -0
  229. package/src/utils/run-service.ts +433 -0
  230. package/src/utils/state.ts +385 -11
  231. package/src/utils/task-service.ts +370 -0
  232. package/src/utils/template.ts +92 -0
  233. package/src/utils/types.ts +2 -314
  234. package/templates/basic.json +21 -0
@@ -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;
@@ -40,16 +47,50 @@ const path = __importStar(require("path"));
40
47
  const readline = __importStar(require("readline"));
41
48
  const state_1 = require("../utils/state");
42
49
  const config_1 = require("../utils/config");
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
+ };
43
84
  function printHelp() {
44
- console.log(`
45
- Usage: cursorflow monitor [run-dir] [options]
46
-
47
- Interactive lane dashboard to track progress and dependencies.
48
-
49
- Options:
50
- [run-dir] Run directory to monitor (default: latest)
51
- --interval <seconds> Refresh interval (default: 2)
52
- --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
53
94
  `);
54
95
  }
55
96
  var View;
@@ -61,6 +102,8 @@ var View;
61
102
  View[View["TERMINAL"] = 4] = "TERMINAL";
62
103
  View[View["INTERVENE"] = 5] = "INTERVENE";
63
104
  View[View["TIMEOUT"] = 6] = "TIMEOUT";
105
+ View[View["UNIFIED_LOG"] = 7] = "UNIFIED_LOG";
106
+ View[View["FLOWS_DASHBOARD"] = 8] = "FLOWS_DASHBOARD";
64
107
  })(View || (View = {}));
65
108
  class InteractiveMonitor {
66
109
  runDir;
@@ -74,19 +117,92 @@ class InteractiveMonitor {
74
117
  timer = null;
75
118
  scrollOffset = 0;
76
119
  terminalScrollOffset = 0;
120
+ followMode = true;
121
+ unseenLineCount = 0;
77
122
  lastTerminalTotalLines = 0;
78
123
  interventionInput = '';
79
124
  timeoutInput = '';
80
125
  notification = null;
81
- 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) {
82
148
  this.runDir = runDir;
83
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);
84
160
  }
85
161
  async start() {
86
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();
87
174
  this.refresh();
88
175
  this.timer = setInterval(() => this.refresh(), this.interval * 1000);
89
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
+ }
90
206
  setupTerminal() {
91
207
  if (process.stdin.isTTY) {
92
208
  process.stdin.setRawMode(true);
@@ -121,6 +237,12 @@ class InteractiveMonitor {
121
237
  else if (this.view === View.MESSAGE_DETAIL) {
122
238
  this.handleMessageDetailKey(keyName);
123
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
+ }
124
246
  });
125
247
  // Hide cursor
126
248
  process.stdout.write('\x1B[?25l');
@@ -128,6 +250,10 @@ class InteractiveMonitor {
128
250
  stop() {
129
251
  if (this.timer)
130
252
  clearInterval(this.timer);
253
+ // Stop unified log streaming
254
+ if (this.unifiedLogBuffer) {
255
+ this.unifiedLogBuffer.stopStreaming();
256
+ }
131
257
  // Show cursor and clear screen
132
258
  process.stdout.write('\x1B[?25h');
133
259
  process.stdout.write('\x1Bc');
@@ -161,6 +287,19 @@ class InteractiveMonitor {
161
287
  this.view = View.FLOW;
162
288
  this.render();
163
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;
164
303
  case 'q':
165
304
  this.stop();
166
305
  break;
@@ -248,11 +387,29 @@ class InteractiveMonitor {
248
387
  handleTerminalKey(key) {
249
388
  switch (key) {
250
389
  case 'up':
390
+ this.followMode = false;
251
391
  this.terminalScrollOffset++;
252
392
  this.render();
253
393
  break;
254
394
  case 'down':
255
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;
256
413
  this.render();
257
414
  break;
258
415
  case 't':
@@ -338,6 +495,197 @@ class InteractiveMonitor {
338
495
  break;
339
496
  }
340
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
+ }
341
689
  sendIntervention(message) {
342
690
  if (!this.selectedLaneName)
343
691
  return;
@@ -345,13 +693,13 @@ class InteractiveMonitor {
345
693
  if (!lane)
346
694
  return;
347
695
  try {
348
- const interventionPath = path.join(lane.path, 'intervention.txt');
696
+ const interventionPath = (0, path_1.safeJoin)(lane.path, 'intervention.txt');
349
697
  fs.writeFileSync(interventionPath, message, 'utf8');
350
698
  // Also log it to the conversation
351
- const convoPath = path.join(lane.path, 'conversation.jsonl');
699
+ const convoPath = (0, path_1.safeJoin)(lane.path, 'conversation.jsonl');
352
700
  const entry = {
353
701
  timestamp: new Date().toISOString(),
354
- role: 'user',
702
+ role: 'intervention',
355
703
  task: 'INTERVENTION',
356
704
  fullText: `[HUMAN INTERVENTION]: ${message}`,
357
705
  textLength: message.length + 20,
@@ -376,7 +724,7 @@ class InteractiveMonitor {
376
724
  this.showNotification('Invalid timeout value', 'error');
377
725
  return;
378
726
  }
379
- const timeoutPath = path.join(lane.path, 'timeout.txt');
727
+ const timeoutPath = (0, path_1.safeJoin)(lane.path, 'timeout.txt');
380
728
  fs.writeFileSync(timeoutPath, String(timeoutMs), 'utf8');
381
729
  this.showNotification(`Timeout updated to ${Math.round(timeoutMs / 1000)}s`, 'success');
382
730
  }
@@ -390,7 +738,7 @@ class InteractiveMonitor {
390
738
  const lane = this.lanes.find(l => l.name === this.selectedLaneName);
391
739
  if (!lane)
392
740
  return;
393
- const convoPath = path.join(lane.path, 'conversation.jsonl');
741
+ const convoPath = (0, path_1.safeJoin)(lane.path, 'conversation.jsonl');
394
742
  this.currentLogs = (0, state_1.readLog)(convoPath);
395
743
  // Keep selection in bounds after refresh
396
744
  if (this.selectedMessageIndex >= this.currentLogs.length) {
@@ -399,11 +747,29 @@ class InteractiveMonitor {
399
747
  }
400
748
  refresh() {
401
749
  this.lanes = this.listLanesWithDeps(this.runDir);
402
- 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) {
403
753
  this.refreshLogs();
404
754
  }
755
+ // Refresh flows list periodically
756
+ if (this.view === View.FLOWS_DASHBOARD) {
757
+ this.discoverFlows();
758
+ }
405
759
  this.render();
406
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
+ }
407
773
  killLane() {
408
774
  if (!this.selectedLaneName)
409
775
  return;
@@ -428,6 +794,55 @@ class InteractiveMonitor {
428
794
  this.notification = { message, type, time: Date.now() };
429
795
  this.render();
430
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
+ }
431
846
  render() {
432
847
  // Clear screen
433
848
  process.stdout.write('\x1Bc');
@@ -435,9 +850,9 @@ class InteractiveMonitor {
435
850
  if (this.notification && Date.now() - this.notification.time > 3000) {
436
851
  this.notification = null;
437
852
  }
438
- if (this.notification) {
439
- const color = this.notification.type === 'error' ? '\x1b[31m' : this.notification.type === 'success' ? '\x1b[32m' : '\x1b[36m';
440
- 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;
441
856
  }
442
857
  switch (this.view) {
443
858
  case View.LIST:
@@ -461,67 +876,113 @@ class InteractiveMonitor {
461
876
  case View.TIMEOUT:
462
877
  this.renderTimeout();
463
878
  break;
879
+ case View.UNIFIED_LOG:
880
+ this.renderUnifiedLog();
881
+ break;
882
+ case View.FLOWS_DASHBOARD:
883
+ this.renderFlowsDashboard();
884
+ break;
464
885
  }
465
886
  }
466
887
  renderList() {
467
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
468
- console.log(`📊 CursorFlow Monitor - Run: ${path.basename(this.runDir)}`);
469
- console.log(`🕒 Updated: ${new Date().toLocaleTimeString()} | [↑/↓/→] Nav [←] Flow [Q] Quit`);
470
- 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`);
471
899
  if (this.lanes.length === 0) {
472
- 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']);
473
902
  return;
474
903
  }
475
904
  const laneStatuses = {};
476
905
  this.lanes.forEach(l => laneStatuses[l.name] = this.getLaneStatus(l.path, l.name));
477
- const maxNameLen = Math.max(...this.lanes.map(l => l.name.length), 15);
478
- console.log(` ${'Lane'.padEnd(maxNameLen)} Status Progress Time Tasks Next Action`);
479
- 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`);
480
909
  this.lanes.forEach((lane, i) => {
481
910
  const isSelected = i === this.selectedLaneIndex;
482
911
  const status = laneStatuses[lane.name];
483
- const statusIcon = this.getStatusIcon(status.status);
484
- const statusText = `${statusIcon} ${status.status}`.padEnd(18);
485
- const progressText = status.progress.padEnd(8);
486
- const timeText = this.formatDuration(status.duration).padEnd(8);
487
- 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);
488
951
  if (typeof status.totalTasks === 'number') {
489
- tasksDisplay = `${status.currentTask}/${status.totalTasks}`;
952
+ tasksText = `${status.currentTask}/${status.totalTasks}`.padEnd(6);
490
953
  }
491
- const tasksText = tasksDisplay.padEnd(6);
492
- // Determine "Next Action"
954
+ // Next action
493
955
  let nextAction = '-';
494
956
  if (status.status === 'completed') {
495
- const dependents = this.lanes.filter(l => laneStatuses[l.name].dependsOn.includes(lane.name));
496
- if (dependents.length > 0) {
497
- nextAction = `Unlock: ${dependents.map(d => d.name).join(', ')}`;
498
- }
499
- else {
500
- nextAction = '🏁 Done';
501
- }
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';
502
959
  }
503
960
  else if (status.status === 'waiting') {
504
- if (status.waitingFor && status.waitingFor.length > 0) {
505
- nextAction = `Wait for task: ${status.waitingFor.join(', ')}`;
961
+ if (status.waitingFor?.length > 0) {
962
+ nextAction = `⏳ ${status.waitingFor.join(', ')}`;
506
963
  }
507
964
  else {
508
- const missingDeps = status.dependsOn.filter((d) => laneStatuses[d] && laneStatuses[d].status !== 'completed');
509
- 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';
510
967
  }
511
968
  }
512
- else if (status.status === 'running') {
513
- nextAction = '🚀 Working...';
514
- }
515
- const prefix = isSelected ? ' ▶ ' : ' ';
516
- const line = `${prefix}${lane.name.padEnd(maxNameLen)} ${statusText} ${progressText} ${timeText} ${tasksText} ${nextAction}`;
517
- if (isSelected) {
518
- process.stdout.write(`\x1b[36m${line}\x1b[0m\n`);
969
+ else if (processStatus?.actualStatus === 'running') {
970
+ nextAction = '🚀 working...';
519
971
  }
520
- else {
521
- process.stdout.write(`${line}\n`);
972
+ else if (processStatus?.isStale) {
973
+ nextAction = '⚠️ died unexpectedly';
522
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`);
523
982
  });
524
- console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
983
+ this.renderFooter([
984
+ '[↑↓] Select', '[→/Enter] Details', '[F] Flow', '[U] Unified Logs', '[M] All Flows', '[Q] Quit'
985
+ ]);
525
986
  }
526
987
  renderLaneDetail() {
527
988
  const lane = this.lanes.find(l => l.name === this.selectedLaneName);
@@ -531,70 +992,92 @@ class InteractiveMonitor {
531
992
  return;
532
993
  }
533
994
  const status = this.getLaneStatus(lane.path, lane.name);
534
- const logPath = path.join(lane.path, 'terminal.log');
535
- let liveLog = '(No live terminal output)';
536
- if (fs.existsSync(logPath)) {
537
- const content = fs.readFileSync(logPath, 'utf8');
538
- liveLog = content.split('\n').slice(-15).join('\n');
539
- }
540
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
541
- console.log(`🔍 Lane: ${lane.name}`);
542
- console.log(`🕒 Updated: ${new Date().toLocaleTimeString()} | [↑/↓] Browse [T] Term [I] Intervene [O] Timeout [Esc] Back`);
543
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
544
- process.stdout.write(` Status: ${this.getStatusIcon(status.status)} ${status.status}\n`);
545
- process.stdout.write(` PID: ${status.pid || '-'}\n`);
546
- process.stdout.write(` Progress: ${status.progress} (${status.currentTask}/${status.totalTasks} tasks)\n`);
547
- process.stdout.write(` Time: ${this.formatDuration(status.duration)}\n`);
548
- process.stdout.write(` Branch: ${status.pipelineBranch}\n`);
549
- process.stdout.write(` Chat ID: ${status.chatId}\n`);
550
- process.stdout.write(` Depends: ${status.dependsOn.join(', ') || 'None'}\n`);
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
+ }
551
1018
  if (status.waitingFor && status.waitingFor.length > 0) {
552
- process.stdout.write(`\x1b[33m Wait For: ${status.waitingFor.join(', ')}\x1b[0m\n`);
1019
+ process.stdout.write(` ${UI.COLORS.yellow}Waiting${UI.COLORS.reset} ${status.waitingFor.join(', ')}\n`);
553
1020
  }
554
1021
  if (status.error) {
555
- process.stdout.write(`\x1b[31m Error: ${status.error}\x1b[0m\n`);
556
- }
557
- console.log('\n🖥️ Live Terminal Output (Last 15 lines):');
558
- console.log(''.repeat(80));
559
- console.log(`\x1b[90m${liveLog}\x1b[0m`);
560
- console.log('\n💬 Conversation History (Select to see full details):');
561
- console.log(''.repeat(80));
562
- process.stdout.write(' [↑/↓] Browse | [→/Enter] Full Msg | [I] Intervene | [K] Kill | [T] Live Terminal | [Esc/←] Back\n\n');
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');
1026
+ const logPath = (0, path_1.safeJoin)(lane.path, 'terminal.log');
1027
+ if (fs.existsSync(logPath)) {
1028
+ const content = fs.readFileSync(logPath, 'utf8');
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;
1046
+ }
563
1047
  if (this.currentLogs.length === 0) {
564
- console.log(' (No messages yet)');
1048
+ process.stdout.write(` ${UI.COLORS.dim}(No messages yet)${UI.COLORS.reset}\n`);
565
1049
  }
566
1050
  else {
567
- // Simple windowed view for long histories
568
- const maxVisible = 15; // Number of messages to show
569
- if (this.selectedMessageIndex < this.scrollOffset) {
570
- this.scrollOffset = this.selectedMessageIndex;
571
- }
572
- else if (this.selectedMessageIndex >= this.scrollOffset + maxVisible) {
573
- this.scrollOffset = this.selectedMessageIndex - maxVisible + 1;
574
- }
575
1051
  const visibleLogs = this.currentLogs.slice(this.scrollOffset, this.scrollOffset + maxVisible);
576
1052
  visibleLogs.forEach((log, i) => {
577
1053
  const actualIndex = i + this.scrollOffset;
578
1054
  const isSelected = actualIndex === this.selectedMessageIndex;
579
- const roleColor = log.role === 'user' ? '\x1b[33m' : log.role === 'reviewer' ? '\x1b[35m' : '\x1b[32m';
1055
+ const roleColor = this.getRoleColor(log.role);
580
1056
  const role = log.role.toUpperCase().padEnd(10);
581
- const prefix = isSelected ? '' : ' ';
582
- const header = `${prefix}${roleColor}${role}\x1b[0m [${new Date(log.timestamp).toLocaleTimeString()}]`;
583
- if (isSelected) {
584
- process.stdout.write(`\x1b[48;5;236m${header}\x1b[0m\n`);
585
- }
586
- else {
587
- process.stdout.write(`${header}\n`);
588
- }
589
- const lines = log.fullText.split('\n').filter(l => l.trim());
590
- const preview = lines[0]?.substring(0, 70) || '...';
591
- 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`);
592
1063
  });
593
1064
  if (this.currentLogs.length > maxVisible) {
594
- 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`);
595
1066
  }
596
1067
  }
597
- 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;
598
1081
  }
599
1082
  renderMessageDetail() {
600
1083
  const log = this.currentLogs[this.selectedMessageIndex];
@@ -603,51 +1086,143 @@ class InteractiveMonitor {
603
1086
  this.render();
604
1087
  return;
605
1088
  }
606
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
607
- console.log(`📄 Full Message Detail - ${log.role.toUpperCase()}`);
608
- console.log(`🕒 ${new Date(log.timestamp).toLocaleString()} | [Esc/←] Back to History`);
609
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
610
- const roleColor = log.role === 'user' ? '\x1b[33m' : log.role === 'reviewer' ? '\x1b[35m' : '\x1b[32m';
611
- 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`);
612
1095
  if (log.model)
613
- process.stdout.write(`MODEL: ${log.model}\n`);
1096
+ process.stdout.write(` ${UI.COLORS.dim}Model${UI.COLORS.reset} ${log.model}\n`);
614
1097
  if (log.task)
615
- process.stdout.write(`TASK: ${log.task}\n`);
616
- console.log(''.repeat(40));
617
- console.log(log.fullText);
618
- 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;
619
1147
  }
620
1148
  renderFlow() {
621
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
622
- console.log(`⛓️ Task Dependency Flow`);
623
- console.log(`🕒 Updated: ${new Date().toLocaleTimeString()} | [→/Enter/Esc] Back to List`);
624
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
1149
+ this.renderHeader('Dependency Flow', [path.basename(this.runDir), 'Flow']);
625
1150
  const laneMap = new Map();
626
1151
  this.lanes.forEach(lane => {
627
1152
  laneMap.set(lane.name, this.getLaneStatus(lane.path, lane.name));
628
1153
  });
629
- // Enhanced visualization with box-like structure and clear connections
630
- this.lanes.forEach(lane => {
631
- const status = laneMap.get(lane.name);
632
- const statusIcon = this.getStatusIcon(status.status);
633
- let statusColor = '\x1b[90m'; // Grey for pending/waiting
634
- if (status.status === 'completed')
635
- statusColor = '\x1b[32m'; // Green
636
- if (status.status === 'running')
637
- statusColor = '\x1b[36m'; // Cyan
638
- if (status.status === 'failed')
639
- statusColor = '\x1b[31m'; // Red
640
- // Render the node
641
- const nodeText = `[ ${statusIcon} ${lane.name.padEnd(18)} ]`;
642
- process.stdout.write(` ${statusColor}${nodeText}\x1b[0m`);
643
- // Render dependencies
644
- if (status.dependsOn && status.dependsOn.length > 0) {
645
- process.stdout.write(` \x1b[90m◀───\x1b[0m \x1b[33m( ${status.dependsOn.join(', ')} )\x1b[0m`);
646
- }
647
- process.stdout.write('\n');
648
- });
649
- console.log('\n\x1b[90m (Lanes wait for their dependencies to complete before starting)\x1b[0m');
650
- 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;
651
1226
  }
652
1227
  renderTerminal() {
653
1228
  const lane = this.lanes.find(l => l.name === this.selectedLaneName);
@@ -656,18 +1231,35 @@ class InteractiveMonitor {
656
1231
  this.render();
657
1232
  return;
658
1233
  }
659
- const logPath = path.join(lane.path, 'terminal.log');
1234
+ this.renderHeader('Live Terminal', [path.basename(this.runDir), lane.name, 'Terminal']);
1235
+ // Get logs based on format mode
660
1236
  let logLines = [];
661
- if (fs.existsSync(logPath)) {
662
- const content = fs.readFileSync(logPath, 'utf8');
663
- 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;
664
1243
  }
665
- const maxVisible = 40;
666
- const totalLines = logLines.length;
667
- // Sticky scroll logic: if new lines arrived and we are already scrolled up,
668
- // increase the offset to stay on the same content.
669
- if (this.terminalScrollOffset > 0 && this.lastTerminalTotalLines > 0 && totalLines > this.lastTerminalTotalLines) {
670
- 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
+ }
671
1263
  }
672
1264
  this.lastTerminalTotalLines = totalLines;
673
1265
  // Clamp scroll offset
@@ -675,70 +1267,298 @@ class InteractiveMonitor {
675
1267
  if (this.terminalScrollOffset > maxScroll) {
676
1268
  this.terminalScrollOffset = maxScroll;
677
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`);
678
1278
  // Slice based on scroll (0 means bottom, >0 means scrolled up)
679
1279
  const end = totalLines - this.terminalScrollOffset;
680
1280
  const start = Math.max(0, end - maxVisible);
681
1281
  const visibleLines = logLines.slice(start, end);
682
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
683
- console.log(`🖥️ Full Live Terminal: ${lane.name}`);
684
- console.log(`🕒 Streaming... | [↑/↓] Scroll (${this.terminalScrollOffset > 0 ? `Scrolled Up ${this.terminalScrollOffset}` : 'Bottom'}) | [I] Intervene | [T/Esc/←] Back`);
685
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
686
- visibleLines.forEach(line => {
687
- let formattedLine = line;
688
- // Highlight human intervention
689
- if (line.includes('[HUMAN INTERVENTION]') || line.includes('Injecting intervention:')) {
690
- formattedLine = `\x1b[33m\x1b[1m${line}\x1b[0m`;
691
- }
692
- // Highlight agent execution starts
693
- else if (line.includes('Executing cursor-agent')) {
694
- formattedLine = `\x1b[36m\x1b[1m${line}\x1b[0m`;
695
- }
696
- // Highlight task headers
697
- else if (line.includes('=== Task:')) {
698
- formattedLine = `\x1b[32m\x1b[1m${line}\x1b[0m`;
699
- }
700
- // Highlight errors
701
- else if (line.toLowerCase().includes('error') || line.toLowerCase().includes('failed')) {
702
- formattedLine = `\x1b[31m${line}\x1b[0m`;
703
- }
704
- process.stdout.write(` ${formattedLine}\n`);
705
- });
706
- 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 };
707
1371
  }
708
1372
  renderIntervene() {
709
- console.clear();
710
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
711
- console.log(`🙋 HUMAN INTERVENTION: ${this.selectedLaneName}`);
712
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
713
- console.log('\n Type your message to the agent. This will be sent as a direct prompt.');
714
- console.log(` Press \x1b[1mENTER\x1b[0m to send, \x1b[1mESC\x1b[0m to cancel.\n`);
715
- console.log(`\x1b[33m > \x1b[0m${this.interventionInput}\x1b[37m█\x1b[0m`);
716
- 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']);
717
1390
  }
718
1391
  renderTimeout() {
719
- console.clear();
720
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
721
- console.log(`⏱ UPDATE TIMEOUT: ${this.selectedLaneName}`);
722
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
723
- console.log('\n Enter new timeout in milliseconds (e.g., 600000 for 10 minutes).');
724
- console.log(` Press \x1b[1mENTER\x1b[0m to apply, \x1b[1mESC\x1b[0m to cancel.\n`);
725
- console.log(`\x1b[33m > \x1b[0m${this.timeoutInput}\x1b[37m█\x1b[0m`);
726
- 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
+ ]);
727
1547
  }
728
1548
  listLanesWithDeps(runDir) {
729
- const lanesDir = path.join(runDir, 'lanes');
1549
+ const lanesDir = (0, path_1.safeJoin)(runDir, 'lanes');
730
1550
  if (!fs.existsSync(lanesDir))
731
1551
  return [];
732
1552
  const config = (0, config_1.loadConfig)();
733
- const tasksDir = path.join(config.projectRoot, config.tasksDir);
1553
+ const tasksDir = (0, path_1.safeJoin)(config.projectRoot, config.tasksDir);
734
1554
  const laneConfigs = this.listLaneFilesFromDir(tasksDir);
735
1555
  return fs.readdirSync(lanesDir)
736
- .filter(d => fs.statSync(path.join(lanesDir, d)).isDirectory())
1556
+ .filter(d => fs.statSync((0, path_1.safeJoin)(lanesDir, d)).isDirectory())
737
1557
  .map(name => {
738
1558
  const config = laneConfigs.find(c => c.name === name);
739
1559
  return {
740
1560
  name,
741
- path: path.join(lanesDir, name),
1561
+ path: (0, path_1.safeJoin)(lanesDir, name),
742
1562
  dependsOn: config?.dependsOn || [],
743
1563
  };
744
1564
  });
@@ -749,7 +1569,7 @@ class InteractiveMonitor {
749
1569
  return fs.readdirSync(tasksDir)
750
1570
  .filter(f => f.endsWith('.json'))
751
1571
  .map(f => {
752
- const filePath = path.join(tasksDir, f);
1572
+ const filePath = (0, path_1.safeJoin)(tasksDir, f);
753
1573
  try {
754
1574
  const config = JSON.parse(fs.readFileSync(filePath, 'utf8'));
755
1575
  return { name: path.basename(f, '.json'), dependsOn: config.dependsOn || [] };
@@ -760,7 +1580,7 @@ class InteractiveMonitor {
760
1580
  });
761
1581
  }
762
1582
  getLaneStatus(lanePath, laneName) {
763
- const statePath = path.join(lanePath, 'state.json');
1583
+ const statePath = (0, path_1.safeJoin)(lanePath, 'state.json');
764
1584
  const state = (0, state_1.loadState)(statePath);
765
1585
  const laneInfo = this.lanes.find(l => l.name === laneName);
766
1586
  const dependsOn = state?.dependsOn || laneInfo?.dependsOn || [];
@@ -814,12 +1634,12 @@ class InteractiveMonitor {
814
1634
  * Find the latest run directory
815
1635
  */
816
1636
  function findLatestRunDir(logsDir) {
817
- const runsDir = path.join(logsDir, 'runs');
1637
+ const runsDir = (0, path_1.safeJoin)(logsDir, 'runs');
818
1638
  if (!fs.existsSync(runsDir))
819
1639
  return null;
820
1640
  const runs = fs.readdirSync(runsDir)
821
1641
  .filter(d => d.startsWith('run-'))
822
- .map(d => ({ name: d, path: path.join(runsDir, d), mtime: fs.statSync(path.join(runsDir, d)).mtime.getTime() }))
1642
+ .map(d => ({ name: d, path: (0, path_1.safeJoin)(runsDir, d), mtime: fs.statSync((0, path_1.safeJoin)(runsDir, d)).mtime.getTime() }))
823
1643
  .sort((a, b) => b.mtime - a.mtime);
824
1644
  return runs.length > 0 ? runs[0].path : null;
825
1645
  }
@@ -832,7 +1652,6 @@ async function monitor(args) {
832
1652
  printHelp();
833
1653
  return;
834
1654
  }
835
- const watchIdx = args.indexOf('--watch');
836
1655
  const intervalIdx = args.indexOf('--interval');
837
1656
  const interval = intervalIdx >= 0 ? parseInt(args[intervalIdx + 1] || '2') || 2 : 2;
838
1657
  const runDirArg = args.find(arg => !arg.startsWith('--') && args.indexOf(arg) !== intervalIdx + 1);