@litmers/cursorflow-orchestrator 0.1.40 → 0.2.3

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 (214) hide show
  1. package/CHANGELOG.md +0 -2
  2. package/README.md +8 -3
  3. package/commands/cursorflow-init.md +0 -4
  4. package/dist/cli/index.js +0 -6
  5. package/dist/cli/index.js.map +1 -1
  6. package/dist/cli/logs.js +108 -9
  7. package/dist/cli/logs.js.map +1 -1
  8. package/dist/cli/models.js +20 -3
  9. package/dist/cli/models.js.map +1 -1
  10. package/dist/cli/monitor.d.ts +7 -10
  11. package/dist/cli/monitor.js +1103 -1239
  12. package/dist/cli/monitor.js.map +1 -1
  13. package/dist/cli/resume.js +21 -1
  14. package/dist/cli/resume.js.map +1 -1
  15. package/dist/cli/run.js +28 -9
  16. package/dist/cli/run.js.map +1 -1
  17. package/dist/cli/signal.d.ts +6 -1
  18. package/dist/cli/signal.js +99 -13
  19. package/dist/cli/signal.js.map +1 -1
  20. package/dist/cli/tasks.js +3 -46
  21. package/dist/cli/tasks.js.map +1 -1
  22. package/dist/core/agent-supervisor.d.ts +23 -0
  23. package/dist/core/agent-supervisor.js +42 -0
  24. package/dist/core/agent-supervisor.js.map +1 -0
  25. package/dist/core/auto-recovery.d.ts +3 -117
  26. package/dist/core/auto-recovery.js +4 -482
  27. package/dist/core/auto-recovery.js.map +1 -1
  28. package/dist/core/failure-policy.d.ts +0 -53
  29. package/dist/core/failure-policy.js +7 -175
  30. package/dist/core/failure-policy.js.map +1 -1
  31. package/dist/core/git-lifecycle-manager.d.ts +284 -0
  32. package/dist/core/git-lifecycle-manager.js +778 -0
  33. package/dist/core/git-lifecycle-manager.js.map +1 -0
  34. package/dist/core/git-pipeline-coordinator.d.ts +21 -0
  35. package/dist/core/git-pipeline-coordinator.js +205 -0
  36. package/dist/core/git-pipeline-coordinator.js.map +1 -0
  37. package/dist/core/intervention.d.ts +170 -0
  38. package/dist/core/intervention.js +408 -0
  39. package/dist/core/intervention.js.map +1 -0
  40. package/dist/core/lane-state-machine.d.ts +423 -0
  41. package/dist/core/lane-state-machine.js +890 -0
  42. package/dist/core/lane-state-machine.js.map +1 -0
  43. package/dist/core/orchestrator.d.ts +4 -1
  44. package/dist/core/orchestrator.js +39 -65
  45. package/dist/core/orchestrator.js.map +1 -1
  46. package/dist/core/runner/agent.d.ts +7 -1
  47. package/dist/core/runner/agent.js +54 -36
  48. package/dist/core/runner/agent.js.map +1 -1
  49. package/dist/core/runner/pipeline.js +283 -123
  50. package/dist/core/runner/pipeline.js.map +1 -1
  51. package/dist/core/runner/task.d.ts +4 -5
  52. package/dist/core/runner/task.js +6 -80
  53. package/dist/core/runner/task.js.map +1 -1
  54. package/dist/core/runner.js +8 -2
  55. package/dist/core/runner.js.map +1 -1
  56. package/dist/core/stall-detection.d.ts +11 -4
  57. package/dist/core/stall-detection.js +64 -27
  58. package/dist/core/stall-detection.js.map +1 -1
  59. package/dist/hooks/contexts/index.d.ts +104 -0
  60. package/dist/hooks/contexts/index.js +134 -0
  61. package/dist/hooks/contexts/index.js.map +1 -0
  62. package/dist/hooks/data-accessor.d.ts +86 -0
  63. package/dist/hooks/data-accessor.js +410 -0
  64. package/dist/hooks/data-accessor.js.map +1 -0
  65. package/dist/hooks/flow-controller.d.ts +136 -0
  66. package/dist/hooks/flow-controller.js +351 -0
  67. package/dist/hooks/flow-controller.js.map +1 -0
  68. package/dist/hooks/index.d.ts +68 -0
  69. package/dist/hooks/index.js +105 -0
  70. package/dist/hooks/index.js.map +1 -0
  71. package/dist/hooks/manager.d.ts +129 -0
  72. package/dist/hooks/manager.js +389 -0
  73. package/dist/hooks/manager.js.map +1 -0
  74. package/dist/hooks/types.d.ts +463 -0
  75. package/dist/hooks/types.js +45 -0
  76. package/dist/hooks/types.js.map +1 -0
  77. package/dist/services/logging/buffer.d.ts +2 -2
  78. package/dist/services/logging/buffer.js +95 -42
  79. package/dist/services/logging/buffer.js.map +1 -1
  80. package/dist/services/logging/console.js +6 -1
  81. package/dist/services/logging/console.js.map +1 -1
  82. package/dist/services/logging/formatter.d.ts +9 -4
  83. package/dist/services/logging/formatter.js +64 -18
  84. package/dist/services/logging/formatter.js.map +1 -1
  85. package/dist/services/logging/index.d.ts +0 -1
  86. package/dist/services/logging/index.js +0 -1
  87. package/dist/services/logging/index.js.map +1 -1
  88. package/dist/services/logging/paths.d.ts +8 -0
  89. package/dist/services/logging/paths.js +48 -0
  90. package/dist/services/logging/paths.js.map +1 -0
  91. package/dist/services/logging/raw-log.d.ts +6 -0
  92. package/dist/services/logging/raw-log.js +37 -0
  93. package/dist/services/logging/raw-log.js.map +1 -0
  94. package/dist/services/process/index.js +1 -1
  95. package/dist/services/process/index.js.map +1 -1
  96. package/dist/types/agent.d.ts +15 -0
  97. package/dist/types/config.d.ts +22 -1
  98. package/dist/types/event-categories.d.ts +601 -0
  99. package/dist/types/event-categories.js +233 -0
  100. package/dist/types/event-categories.js.map +1 -0
  101. package/dist/types/events.d.ts +0 -20
  102. package/dist/types/flow.d.ts +10 -6
  103. package/dist/types/index.d.ts +1 -1
  104. package/dist/types/index.js +17 -3
  105. package/dist/types/index.js.map +1 -1
  106. package/dist/types/lane.d.ts +1 -1
  107. package/dist/types/logging.d.ts +1 -1
  108. package/dist/types/task.d.ts +12 -1
  109. package/dist/ui/log-viewer.d.ts +3 -0
  110. package/dist/ui/log-viewer.js +3 -0
  111. package/dist/ui/log-viewer.js.map +1 -1
  112. package/dist/utils/config.js +10 -1
  113. package/dist/utils/config.js.map +1 -1
  114. package/dist/utils/cursor-agent.d.ts +11 -1
  115. package/dist/utils/cursor-agent.js +63 -16
  116. package/dist/utils/cursor-agent.js.map +1 -1
  117. package/dist/utils/enhanced-logger.d.ts +5 -1
  118. package/dist/utils/enhanced-logger.js +98 -19
  119. package/dist/utils/enhanced-logger.js.map +1 -1
  120. package/dist/utils/event-registry.d.ts +222 -0
  121. package/dist/utils/event-registry.js +463 -0
  122. package/dist/utils/event-registry.js.map +1 -0
  123. package/dist/utils/events.d.ts +1 -13
  124. package/dist/utils/events.js.map +1 -1
  125. package/dist/utils/flow.d.ts +10 -0
  126. package/dist/utils/flow.js +75 -0
  127. package/dist/utils/flow.js.map +1 -1
  128. package/dist/utils/log-constants.d.ts +1 -0
  129. package/dist/utils/log-constants.js +2 -1
  130. package/dist/utils/log-constants.js.map +1 -1
  131. package/dist/utils/log-formatter.d.ts +2 -1
  132. package/dist/utils/log-formatter.js +10 -10
  133. package/dist/utils/log-formatter.js.map +1 -1
  134. package/dist/utils/logger.d.ts +11 -0
  135. package/dist/utils/logger.js +82 -3
  136. package/dist/utils/logger.js.map +1 -1
  137. package/dist/utils/repro-thinking-logs.js +0 -13
  138. package/dist/utils/repro-thinking-logs.js.map +1 -1
  139. package/dist/utils/run-service.js +1 -1
  140. package/dist/utils/run-service.js.map +1 -1
  141. package/examples/README.md +0 -2
  142. package/examples/demo-project/README.md +1 -2
  143. package/package.json +13 -34
  144. package/scripts/setup-security.sh +0 -1
  145. package/scripts/test-log-parser.ts +171 -0
  146. package/scripts/verify-change.sh +272 -0
  147. package/src/cli/index.ts +0 -6
  148. package/src/cli/logs.ts +121 -10
  149. package/src/cli/models.ts +20 -3
  150. package/src/cli/monitor.ts +1273 -1342
  151. package/src/cli/resume.ts +27 -1
  152. package/src/cli/run.ts +29 -11
  153. package/src/cli/signal.ts +120 -18
  154. package/src/cli/tasks.ts +2 -59
  155. package/src/core/agent-supervisor.ts +64 -0
  156. package/src/core/auto-recovery.ts +14 -590
  157. package/src/core/failure-policy.ts +7 -229
  158. package/src/core/git-lifecycle-manager.ts +1011 -0
  159. package/src/core/git-pipeline-coordinator.ts +221 -0
  160. package/src/core/intervention.ts +463 -0
  161. package/src/core/lane-state-machine.ts +1097 -0
  162. package/src/core/orchestrator.ts +48 -64
  163. package/src/core/runner/agent.ts +77 -39
  164. package/src/core/runner/pipeline.ts +318 -138
  165. package/src/core/runner/task.ts +12 -97
  166. package/src/core/runner.ts +8 -2
  167. package/src/core/stall-detection.ts +74 -27
  168. package/src/hooks/contexts/index.ts +256 -0
  169. package/src/hooks/data-accessor.ts +488 -0
  170. package/src/hooks/flow-controller.ts +425 -0
  171. package/src/hooks/index.ts +154 -0
  172. package/src/hooks/manager.ts +434 -0
  173. package/src/hooks/types.ts +544 -0
  174. package/src/services/logging/buffer.ts +104 -43
  175. package/src/services/logging/console.ts +7 -1
  176. package/src/services/logging/formatter.ts +74 -18
  177. package/src/services/logging/index.ts +0 -2
  178. package/src/services/logging/paths.ts +14 -0
  179. package/src/services/logging/raw-log.ts +43 -0
  180. package/src/services/process/index.ts +1 -1
  181. package/src/types/agent.ts +15 -0
  182. package/src/types/config.ts +23 -1
  183. package/src/types/event-categories.ts +663 -0
  184. package/src/types/events.ts +0 -25
  185. package/src/types/flow.ts +10 -6
  186. package/src/types/index.ts +50 -4
  187. package/src/types/lane.ts +1 -2
  188. package/src/types/logging.ts +2 -1
  189. package/src/types/task.ts +12 -1
  190. package/src/ui/log-viewer.ts +3 -0
  191. package/src/utils/config.ts +11 -1
  192. package/src/utils/cursor-agent.ts +68 -16
  193. package/src/utils/enhanced-logger.ts +105 -19
  194. package/src/utils/event-registry.ts +595 -0
  195. package/src/utils/events.ts +0 -16
  196. package/src/utils/flow.ts +83 -0
  197. package/src/utils/log-constants.ts +2 -1
  198. package/src/utils/log-formatter.ts +10 -11
  199. package/src/utils/logger.ts +49 -3
  200. package/src/utils/repro-thinking-logs.ts +0 -15
  201. package/src/utils/run-service.ts +1 -1
  202. package/dist/cli/prepare.d.ts +0 -7
  203. package/dist/cli/prepare.js +0 -690
  204. package/dist/cli/prepare.js.map +0 -1
  205. package/dist/services/logging/file-writer.d.ts +0 -71
  206. package/dist/services/logging/file-writer.js +0 -516
  207. package/dist/services/logging/file-writer.js.map +0 -1
  208. package/dist/types/review.d.ts +0 -17
  209. package/dist/types/review.js +0 -6
  210. package/dist/types/review.js.map +0 -1
  211. package/scripts/ai-security-check.js +0 -233
  212. package/src/cli/prepare.ts +0 -777
  213. package/src/services/logging/file-writer.ts +0 -526
  214. package/src/types/review.ts +0 -20
@@ -1,13 +1,13 @@
1
1
  "use strict";
2
2
  /**
3
- * CursorFlow interactive monitor command
3
+ * CursorFlow Interactive Monitor v2.0
4
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
5
+ * Redesigned UX with:
6
+ * - Tab-based main dashboard
7
+ * - Arrow-key-first navigation
8
+ * - Context-aware action menus
9
+ * - Maximum 3-level depth
10
+ * - Consistent key mappings across all views
11
11
  */
12
12
  var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
13
13
  if (k2 === undefined) k2 = k;
@@ -50,6 +50,8 @@ const config_1 = require("../utils/config");
50
50
  const path_1 = require("../utils/path");
51
51
  const process_1 = require("../services/process");
52
52
  const buffer_1 = require("../services/logging/buffer");
53
+ const formatter_1 = require("../services/logging/formatter");
54
+ const intervention_1 = require("../core/intervention");
53
55
  // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
54
56
  // UI Constants
55
57
  // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
@@ -67,82 +69,95 @@ const UI = {
67
69
  white: '\x1b[37m',
68
70
  bgGray: '\x1b[48;5;236m',
69
71
  bgCyan: '\x1b[46m',
72
+ bgYellow: '\x1b[43m',
73
+ bgBlack: '\x1b[40m',
74
+ },
75
+ ICONS: {
76
+ running: '🔄',
77
+ completed: '✅',
78
+ done: '✅',
79
+ failed: '❌',
80
+ waiting: '⏳',
81
+ pending: '⚪',
82
+ stale: '💀',
83
+ dead: '☠️',
84
+ live: '🟢',
85
+ stopped: '🔴',
86
+ arrow: '▶',
87
+ arrowLeft: '◀',
88
+ selected: '●',
89
+ unselected: '○',
70
90
  },
71
91
  CHARS: {
72
92
  hLine: '━',
93
+ hLineLight: '─',
73
94
  vLine: '│',
74
- corner: {
75
- tl: '', tr: '', bl: '', br: ''
76
- },
77
- arrow: {
78
- right: '▶', left: '◀', up: '▲', down: '▼'
79
- },
80
- bullet: '•',
81
- check: '✓',
95
+ corner: { tl: '┌', tr: '┐', bl: '└', br: '┘' },
96
+ tee: { left: '', right: '', top: '', bottom: '' },
97
+ cross: '┼',
82
98
  },
83
99
  };
100
+ var Tab;
101
+ (function (Tab) {
102
+ Tab[Tab["CURRENT_FLOW"] = 0] = "CURRENT_FLOW";
103
+ Tab[Tab["ALL_FLOWS"] = 1] = "ALL_FLOWS";
104
+ Tab[Tab["UNIFIED_LOG"] = 2] = "UNIFIED_LOG";
105
+ })(Tab || (Tab = {}));
106
+ var Level;
107
+ (function (Level) {
108
+ Level[Level["DASHBOARD"] = 1] = "DASHBOARD";
109
+ Level[Level["DETAIL"] = 2] = "DETAIL";
110
+ Level[Level["ACTION_MENU"] = 3] = "ACTION_MENU";
111
+ })(Level || (Level = {}));
112
+ var Panel;
113
+ (function (Panel) {
114
+ Panel[Panel["LEFT"] = 0] = "LEFT";
115
+ Panel[Panel["RIGHT"] = 1] = "RIGHT";
116
+ })(Panel || (Panel = {}));
117
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
118
+ // Helper functions
119
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
84
120
  function printHelp() {
85
121
  console.log(`
86
122
  Usage: cursorflow monitor [run-dir] [options]
87
123
 
88
- Interactive lane dashboard to track progress and dependencies.
124
+ Interactive lane dashboard with improved UX.
89
125
 
90
126
  Options:
91
127
  [run-dir] Run directory to monitor (default: latest)
92
- --list, -l List all runs (multiple flows dashboard)
128
+ --list, -l Start with All Flows tab
93
129
  --interval <seconds> Refresh interval (default: 2)
94
130
  --help, -h Show help
95
131
 
132
+ Navigation:
133
+ ←/→ Tab switch (Level 1) or Panel switch (Level 2)
134
+ ↑/↓ Select item or scroll
135
+ Enter Open action menu
136
+ Esc Go back / Close menu
137
+ Q Quit
138
+
96
139
  Examples:
97
140
  cursorflow monitor # Monitor latest run
98
- cursorflow monitor --list # Show all runs dashboard
141
+ cursorflow monitor --list # Show all runs
99
142
  cursorflow monitor run-123 # Monitor specific run
100
143
  `);
101
144
  }
102
- var View;
103
- (function (View) {
104
- View[View["LIST"] = 0] = "LIST";
105
- View[View["LANE_DETAIL"] = 1] = "LANE_DETAIL";
106
- View[View["MESSAGE_DETAIL"] = 2] = "MESSAGE_DETAIL";
107
- View[View["FLOW"] = 3] = "FLOW";
108
- View[View["TERMINAL"] = 4] = "TERMINAL";
109
- View[View["INTERVENE"] = 5] = "INTERVENE";
110
- View[View["TIMEOUT"] = 6] = "TIMEOUT";
111
- View[View["UNIFIED_LOG"] = 7] = "UNIFIED_LOG";
112
- View[View["FLOWS_DASHBOARD"] = 8] = "FLOWS_DASHBOARD";
113
- })(View || (View = {}));
145
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
146
+ // Interactive Monitor Class
147
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
114
148
  class InteractiveMonitor {
115
149
  runDir;
116
150
  interval;
117
- view = View.LIST;
118
- selectedLaneIndex = 0;
119
- selectedMessageIndex = 0;
120
- selectedLaneName = null;
151
+ logsDir;
152
+ timer = null;
153
+ // Data
121
154
  lanes = [];
155
+ allFlows = [];
122
156
  currentLogs = [];
123
- timer = null;
124
- scrollOffset = 0;
125
- terminalScrollOffset = 0;
126
- followMode = true;
127
- unseenLineCount = 0;
128
- lastTerminalTotalLines = 0;
129
- interventionInput = '';
130
- timeoutInput = '';
131
- notification = null;
132
- // Process status tracking
133
157
  laneProcessStatuses = new Map();
134
- // Unified log buffer for all lanes
135
158
  unifiedLogBuffer = null;
136
- unifiedLogScrollOffset = 0;
137
- unifiedLogFollowMode = true;
138
- // Multiple flows support
139
- allFlows = [];
140
- selectedFlowIndex = 0;
141
- logsDir = '';
142
- // NEW: UX improvements
143
- readableFormat = true; // Toggle readable log format
144
- laneFilter = null; // Filter by lane name
145
- confirmAction = null;
159
+ // State
160
+ state;
146
161
  // Screen dimensions
147
162
  get screenWidth() {
148
163
  return process.stdout.columns || 120;
@@ -150,22 +165,41 @@ class InteractiveMonitor {
150
165
  get screenHeight() {
151
166
  return process.stdout.rows || 24;
152
167
  }
153
- constructor(runDir, interval, logsDir, initialView = View.LIST) {
168
+ constructor(runDir, interval, initialTab = Tab.CURRENT_FLOW) {
154
169
  const config = (0, config_1.loadConfig)();
155
- // Change current directory to project root for consistent path handling
156
170
  if (config.projectRoot !== process.cwd()) {
157
171
  process.chdir(config.projectRoot);
158
172
  }
159
173
  this.runDir = runDir;
160
174
  this.interval = interval;
161
- this.view = initialView;
162
- // Set logs directory for multiple flows discovery
163
- if (logsDir) {
164
- this.logsDir = logsDir;
165
- }
166
- else {
167
- this.logsDir = (0, path_1.safeJoin)((0, config_1.getLogsDir)(config), 'runs');
168
- }
175
+ this.logsDir = (0, path_1.safeJoin)((0, config_1.getLogsDir)(config), 'runs');
176
+ // Initialize state
177
+ this.state = {
178
+ level: Level.DASHBOARD,
179
+ currentTab: initialTab,
180
+ selectedLaneIndex: 0,
181
+ selectedFlowIndex: 0,
182
+ selectedLogIndex: 0,
183
+ selectedActionIndex: 0,
184
+ selectedMessageIndex: 0,
185
+ selectedLaneName: null,
186
+ currentPanel: Panel.LEFT,
187
+ terminalScrollOffset: 0,
188
+ messageScrollOffset: 0,
189
+ followMode: true,
190
+ readableFormat: true,
191
+ unifiedLogScrollOffset: 0,
192
+ unifiedLogFollowMode: true,
193
+ laneFilter: null,
194
+ readableLogFormat: true,
195
+ actionMenuVisible: false,
196
+ actionItems: [],
197
+ inputMode: 'none',
198
+ inputBuffer: '',
199
+ inputTarget: null,
200
+ showHelp: false,
201
+ notification: null,
202
+ };
169
203
  // Initialize unified log buffer
170
204
  this.unifiedLogBuffer = new buffer_1.LogBufferService(runDir);
171
205
  }
@@ -174,15 +208,9 @@ class InteractiveMonitor {
174
208
  if (!process.stdout.isTTY || !process.stdin.isTTY) {
175
209
  this.discoverFlows();
176
210
  this.refresh();
177
- // Print summary and exit
178
- if (this.view === View.FLOWS_DASHBOARD) {
179
- console.log(`\nFound ${this.allFlows.length} flows`);
180
- }
181
- else {
182
- console.log(`\nMonitoring run: ${path.basename(this.runDir)}`);
183
- const flowSummary = (0, process_1.getFlowSummary)(this.runDir);
184
- console.log(`Status: ${flowSummary.running} running, ${flowSummary.completed} completed, ${flowSummary.failed} failed`);
185
- }
211
+ const flowSummary = (0, process_1.getFlowSummary)(this.runDir);
212
+ console.log(`\nMonitoring run: ${path.basename(this.runDir)}`);
213
+ console.log(`Status: ${flowSummary.running} running, ${flowSummary.completed} completed, ${flowSummary.failed} failed`);
186
214
  return;
187
215
  }
188
216
  this.setupTerminal();
@@ -190,40 +218,27 @@ class InteractiveMonitor {
190
218
  if (this.unifiedLogBuffer) {
191
219
  this.unifiedLogBuffer.startStreaming();
192
220
  this.unifiedLogBuffer.on('update', () => {
193
- if (this.view === View.UNIFIED_LOG && this.unifiedLogFollowMode) {
221
+ if (this.state.currentTab === Tab.UNIFIED_LOG && this.state.unifiedLogFollowMode) {
194
222
  this.render();
195
223
  }
196
224
  });
197
225
  }
198
- // Discover all flows
199
226
  this.discoverFlows();
200
227
  this.refresh();
201
228
  this.timer = setInterval(() => this.refresh(), this.interval * 1000);
202
229
  }
203
- /**
204
- * Discover all run directories (flows) for multi-flow view
205
- */
206
230
  discoverFlows() {
207
231
  try {
208
232
  if (!fs.existsSync(this.logsDir))
209
233
  return;
210
- const runs = fs.readdirSync(this.logsDir)
234
+ this.allFlows = fs.readdirSync(this.logsDir)
211
235
  .filter(d => d.startsWith('run-'))
212
236
  .map(d => {
213
237
  const runDir = (0, path_1.safeJoin)(this.logsDir, d);
214
238
  const summary = (0, process_1.getFlowSummary)(runDir);
215
- return {
216
- runDir,
217
- runId: d,
218
- isAlive: summary.isAlive,
219
- summary,
220
- };
239
+ return { runDir, runId: d, isAlive: summary.isAlive, summary };
221
240
  })
222
- .sort((a, b) => {
223
- // Sort by run ID (timestamp-based) descending
224
- return b.runId.localeCompare(a.runId);
225
- });
226
- this.allFlows = runs;
241
+ .sort((a, b) => b.runId.localeCompare(a.runId));
227
242
  }
228
243
  catch {
229
244
  // Ignore errors
@@ -234,494 +249,492 @@ class InteractiveMonitor {
234
249
  process.stdin.setRawMode(true);
235
250
  }
236
251
  readline.emitKeypressEvents(process.stdin);
237
- process.stdin.on('keypress', (str, key) => {
238
- // Handle Ctrl+C
239
- if (key && key.ctrl && key.name === 'c') {
240
- this.stop();
241
- return;
242
- }
243
- // Safeguard against missing key object
244
- const keyName = key ? key.name : str;
245
- if (this.view === View.LIST) {
246
- this.handleListKey(keyName);
247
- }
248
- else if (this.view === View.LANE_DETAIL) {
249
- this.handleDetailKey(keyName);
250
- }
251
- else if (this.view === View.FLOW) {
252
- this.handleFlowKey(keyName);
253
- }
254
- else if (this.view === View.TERMINAL) {
255
- this.handleTerminalKey(keyName);
256
- }
257
- else if (this.view === View.INTERVENE) {
258
- this.handleInterveneKey(str, key);
259
- }
260
- else if (this.view === View.TIMEOUT) {
261
- this.handleTimeoutKey(str, key);
262
- }
263
- else if (this.view === View.MESSAGE_DETAIL) {
264
- this.handleMessageDetailKey(keyName);
265
- }
266
- else if (this.view === View.UNIFIED_LOG) {
267
- this.handleUnifiedLogKey(keyName);
268
- }
269
- else if (this.view === View.FLOWS_DASHBOARD) {
270
- this.handleFlowsDashboardKey(keyName);
271
- }
272
- });
273
- // Hide cursor
274
- process.stdout.write('\x1B[?25l');
252
+ process.stdin.on('keypress', (str, key) => this.handleKeypress(str, key));
253
+ process.stdout.write('\x1B[?25l'); // Hide cursor
275
254
  }
276
255
  stop() {
277
256
  if (this.timer)
278
257
  clearInterval(this.timer);
279
- // Stop unified log streaming
280
- if (this.unifiedLogBuffer) {
258
+ if (this.unifiedLogBuffer)
281
259
  this.unifiedLogBuffer.stopStreaming();
282
- }
283
- // Show cursor and clear screen
284
- process.stdout.write('\x1B[?25h');
285
- process.stdout.write('\x1Bc');
260
+ process.stdout.write('\x1B[?25h'); // Show cursor
261
+ process.stdout.write('\x1Bc'); // Clear screen
286
262
  console.log('\n👋 Monitoring stopped\n');
287
263
  process.exit(0);
288
264
  }
289
- handleListKey(key) {
290
- switch (key) {
291
- case 'up':
292
- this.selectedLaneIndex = Math.max(0, this.selectedLaneIndex - 1);
293
- this.render();
294
- break;
295
- case 'down':
296
- this.selectedLaneIndex = Math.min(this.lanes.length - 1, this.selectedLaneIndex + 1);
265
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
266
+ // Key Handling - Unified handler
267
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
268
+ handleKeypress(str, key) {
269
+ // Ctrl+C always quits
270
+ if (key && key.ctrl && key.name === 'c') {
271
+ this.stop();
272
+ return;
273
+ }
274
+ const keyName = key ? key.name : str;
275
+ // Handle input mode first
276
+ if (this.state.inputMode !== 'none') {
277
+ this.handleInputKey(str, key);
278
+ return;
279
+ }
280
+ // Handle action menu
281
+ if (this.state.actionMenuVisible) {
282
+ this.handleActionMenuKey(keyName);
283
+ return;
284
+ }
285
+ // Handle based on level and tab
286
+ if (this.state.level === Level.DASHBOARD) {
287
+ this.handleDashboardKey(keyName);
288
+ }
289
+ else if (this.state.level === Level.DETAIL) {
290
+ this.handleDetailKey(keyName);
291
+ }
292
+ }
293
+ handleDashboardKey(keyName) {
294
+ switch (keyName) {
295
+ // Tab navigation (Left/Right at dashboard level)
296
+ case 'left':
297
+ this.state.currentTab = Math.max(0, this.state.currentTab - 1);
298
+ this.resetSelectionForTab();
297
299
  this.render();
298
300
  break;
299
301
  case 'right':
300
- case 'return':
301
- case 'enter':
302
- if (this.lanes[this.selectedLaneIndex]) {
303
- this.selectedLaneName = this.lanes[this.selectedLaneIndex].name;
304
- this.view = View.LANE_DETAIL;
305
- this.selectedMessageIndex = 0;
306
- this.scrollOffset = 0;
307
- this.refreshLogs();
308
- this.render();
302
+ // If an item is selected, go to detail
303
+ if (this.canEnterDetail()) {
304
+ this.enterDetail();
305
+ }
306
+ else {
307
+ // Otherwise switch tab
308
+ this.state.currentTab = Math.min(2, this.state.currentTab + 1);
309
+ this.resetSelectionForTab();
309
310
  }
311
+ this.render();
310
312
  break;
311
- case 'left':
312
- case 'f':
313
- this.view = View.FLOW;
313
+ case 'tab':
314
+ this.state.currentTab = ((this.state.currentTab + 1) % 3);
315
+ this.resetSelectionForTab();
316
+ this.render();
317
+ break;
318
+ // Item selection
319
+ case 'up':
320
+ this.moveSelectionUp();
314
321
  this.render();
315
322
  break;
316
- case 'u':
317
- // Unified log view
318
- this.view = View.UNIFIED_LOG;
319
- this.unifiedLogScrollOffset = 0;
320
- this.unifiedLogFollowMode = true;
323
+ case 'down':
324
+ this.moveSelectionDown();
321
325
  this.render();
322
326
  break;
323
- case 'm':
324
- // Multiple flows dashboard
325
- this.discoverFlows();
326
- this.view = View.FLOWS_DASHBOARD;
327
+ // Actions
328
+ case 'return':
329
+ case 'enter':
330
+ this.openActionMenu();
327
331
  this.render();
328
332
  break;
333
+ // Quit
329
334
  case 'q':
330
335
  this.stop();
331
336
  break;
337
+ // Space for follow toggle in log tab
338
+ case 'space':
339
+ if (this.state.currentTab === Tab.UNIFIED_LOG) {
340
+ this.state.unifiedLogFollowMode = !this.state.unifiedLogFollowMode;
341
+ if (this.state.unifiedLogFollowMode) {
342
+ this.state.unifiedLogScrollOffset = 0;
343
+ }
344
+ this.render();
345
+ }
346
+ break;
347
+ // Help
348
+ case '?':
349
+ this.state.showHelp = !this.state.showHelp;
350
+ this.render();
351
+ break;
352
+ // Readable format toggle (for log tab)
353
+ case 'r':
354
+ if (this.state.currentTab === Tab.UNIFIED_LOG) {
355
+ this.state.readableLogFormat = !this.state.readableLogFormat;
356
+ this.render();
357
+ }
358
+ break;
332
359
  }
333
360
  }
334
- handleDetailKey(key) {
335
- switch (key) {
336
- case 'up':
337
- this.selectedMessageIndex = Math.max(0, this.selectedMessageIndex - 1);
361
+ handleDetailKey(keyName) {
362
+ switch (keyName) {
363
+ // Back to dashboard
364
+ case 'left':
365
+ if (this.state.currentPanel === Panel.RIGHT) {
366
+ this.state.currentPanel = Panel.LEFT;
367
+ }
368
+ else {
369
+ this.state.level = Level.DASHBOARD;
370
+ this.state.selectedLaneName = null;
371
+ }
338
372
  this.render();
339
373
  break;
340
- case 'down':
341
- this.selectedMessageIndex = Math.min(this.currentLogs.length - 1, this.selectedMessageIndex + 1);
374
+ case 'escape':
375
+ this.state.level = Level.DASHBOARD;
376
+ this.state.selectedLaneName = null;
342
377
  this.render();
343
378
  break;
379
+ // Panel switch
344
380
  case 'right':
345
- case 'return':
346
- case 'enter':
347
- if (this.currentLogs[this.selectedMessageIndex]) {
348
- this.view = View.MESSAGE_DETAIL;
349
- this.render();
381
+ if (this.state.currentPanel === Panel.LEFT) {
382
+ this.state.currentPanel = Panel.RIGHT;
350
383
  }
351
- break;
352
- case 't':
353
- this.view = View.TERMINAL;
354
- this.terminalScrollOffset = 0;
355
384
  this.render();
356
385
  break;
357
- case 'k':
358
- this.killLane();
359
- break;
360
- case 'i':
361
- const lane = this.lanes.find(l => l.name === this.selectedLaneName);
362
- if (lane) {
363
- const status = this.getLaneStatus(lane.path, lane.name);
364
- if (status.status === 'running') {
365
- this.view = View.INTERVENE;
366
- this.interventionInput = '';
367
- this.render();
368
- }
369
- else {
370
- this.showNotification('Intervention only available for RUNNING lanes', 'error');
371
- }
386
+ // Scroll in current panel
387
+ case 'up':
388
+ if (this.state.currentPanel === Panel.LEFT) {
389
+ this.state.followMode = false;
390
+ this.state.terminalScrollOffset++;
372
391
  }
392
+ else {
393
+ this.state.messageScrollOffset = Math.max(0, this.state.messageScrollOffset - 1);
394
+ }
395
+ this.render();
373
396
  break;
374
- case 'o':
375
- const timeoutLane = this.lanes.find(l => l.name === this.selectedLaneName);
376
- if (timeoutLane) {
377
- const status = this.getLaneStatus(timeoutLane.path, timeoutLane.name);
378
- if (status.status === 'running') {
379
- this.view = View.TIMEOUT;
380
- this.timeoutInput = '';
381
- this.render();
382
- }
383
- else {
384
- this.showNotification('Timeout update only available for RUNNING lanes', 'error');
397
+ case 'down':
398
+ if (this.state.currentPanel === Panel.LEFT) {
399
+ this.state.terminalScrollOffset = Math.max(0, this.state.terminalScrollOffset - 1);
400
+ if (this.state.terminalScrollOffset === 0) {
401
+ this.state.followMode = true;
385
402
  }
386
403
  }
404
+ else {
405
+ this.state.messageScrollOffset++;
406
+ }
407
+ this.render();
387
408
  break;
388
- case 'escape':
389
- case 'backspace':
390
- case 'left':
391
- this.view = View.LIST;
392
- this.selectedLaneName = null;
409
+ // Actions
410
+ case 'return':
411
+ case 'enter':
412
+ this.openActionMenu();
393
413
  this.render();
394
414
  break;
395
- case 'q':
396
- this.stop();
415
+ // Follow toggle
416
+ case 'space':
417
+ this.state.followMode = !this.state.followMode;
418
+ if (this.state.followMode) {
419
+ this.state.terminalScrollOffset = 0;
420
+ }
421
+ this.render();
397
422
  break;
398
- }
399
- }
400
- handleMessageDetailKey(key) {
401
- switch (key) {
402
- case 'escape':
403
- case 'backspace':
404
- case 'left':
405
- this.view = View.LANE_DETAIL;
423
+ // Readable format toggle
424
+ case 'r':
425
+ this.state.readableFormat = !this.state.readableFormat;
426
+ this.render();
427
+ break;
428
+ // Help
429
+ case '?':
430
+ this.state.showHelp = !this.state.showHelp;
406
431
  this.render();
407
432
  break;
433
+ // Quit
408
434
  case 'q':
409
435
  this.stop();
410
436
  break;
411
437
  }
412
438
  }
413
- handleTerminalKey(key) {
414
- switch (key) {
439
+ handleActionMenuKey(keyName) {
440
+ switch (keyName) {
415
441
  case 'up':
416
- this.followMode = false;
417
- this.terminalScrollOffset++;
442
+ this.state.selectedActionIndex = Math.max(0, this.state.selectedActionIndex - 1);
418
443
  this.render();
419
444
  break;
420
445
  case 'down':
421
- this.terminalScrollOffset = Math.max(0, this.terminalScrollOffset - 1);
422
- if (this.terminalScrollOffset === 0) {
423
- this.followMode = true;
424
- this.unseenLineCount = 0;
425
- }
446
+ this.state.selectedActionIndex = Math.min(this.state.actionItems.length - 1, this.state.selectedActionIndex + 1);
426
447
  this.render();
427
448
  break;
428
- case 'f':
429
- this.followMode = true;
430
- this.terminalScrollOffset = 0;
431
- this.unseenLineCount = 0;
432
- this.render();
433
- break;
434
- case 'r':
435
- // Toggle readable log format
436
- this.readableFormat = !this.readableFormat;
437
- this.terminalScrollOffset = 0;
438
- this.lastTerminalTotalLines = 0;
439
- this.render();
449
+ case 'return':
450
+ case 'enter':
451
+ this.executeAction();
440
452
  break;
441
- case 't':
442
453
  case 'escape':
443
- case 'backspace':
444
454
  case 'left':
445
- this.view = View.LANE_DETAIL;
455
+ this.state.actionMenuVisible = false;
446
456
  this.render();
447
457
  break;
448
- case 'i':
449
- this.view = View.INTERVENE;
450
- this.interventionInput = '';
458
+ case 'q':
459
+ this.state.actionMenuVisible = false;
451
460
  this.render();
452
461
  break;
453
- case 'q':
454
- this.stop();
462
+ default:
463
+ // Number keys for quick selection
464
+ const num = parseInt(keyName);
465
+ if (!isNaN(num) && num >= 1 && num <= this.state.actionItems.length) {
466
+ this.state.selectedActionIndex = num - 1;
467
+ this.executeAction();
468
+ }
455
469
  break;
456
470
  }
457
471
  }
458
- handleInterveneKey(str, key) {
472
+ handleInputKey(str, key) {
459
473
  if (key && key.name === 'escape') {
460
- this.view = View.LANE_DETAIL;
474
+ this.state.inputMode = 'none';
475
+ this.state.inputBuffer = '';
461
476
  this.render();
462
477
  return;
463
478
  }
464
479
  if (key && (key.name === 'return' || key.name === 'enter')) {
465
- if (this.interventionInput.trim()) {
466
- this.sendIntervention(this.interventionInput.trim());
467
- }
468
- this.view = View.LANE_DETAIL;
469
- this.render();
480
+ this.submitInput();
470
481
  return;
471
482
  }
472
483
  if (key && key.name === 'backspace') {
473
- this.interventionInput = this.interventionInput.slice(0, -1);
484
+ this.state.inputBuffer = this.state.inputBuffer.slice(0, -1);
474
485
  this.render();
475
486
  return;
476
487
  }
477
- if (str && str.length === 1 && !key.ctrl && !key.meta) {
478
- this.interventionInput += str;
488
+ if (str && str.length === 1 && !key?.ctrl && !key?.meta) {
489
+ // For timeout, only allow numbers
490
+ if (this.state.inputMode === 'timeout' && !/^\d$/.test(str)) {
491
+ return;
492
+ }
493
+ this.state.inputBuffer += str;
479
494
  this.render();
480
495
  }
481
496
  }
482
- handleTimeoutKey(str, key) {
483
- if (key && key.name === 'escape') {
484
- this.view = View.LANE_DETAIL;
485
- this.render();
486
- return;
487
- }
488
- if (key && (key.name === 'return' || key.name === 'enter')) {
489
- if (this.timeoutInput.trim()) {
490
- this.sendTimeoutUpdate(this.timeoutInput.trim());
491
- }
492
- this.view = View.LANE_DETAIL;
493
- this.render();
494
- return;
497
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
498
+ // Navigation Helpers
499
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
500
+ resetSelectionForTab() {
501
+ this.state.selectedLaneIndex = 0;
502
+ this.state.selectedFlowIndex = 0;
503
+ this.state.selectedLogIndex = 0;
504
+ this.state.unifiedLogScrollOffset = 0;
505
+ }
506
+ canEnterDetail() {
507
+ switch (this.state.currentTab) {
508
+ case Tab.CURRENT_FLOW:
509
+ return this.lanes.length > 0;
510
+ case Tab.ALL_FLOWS:
511
+ return this.allFlows.length > 0;
512
+ case Tab.UNIFIED_LOG:
513
+ return false; // Log detail is inline
495
514
  }
496
- if (key && key.name === 'backspace') {
497
- this.timeoutInput = this.timeoutInput.slice(0, -1);
498
- this.render();
499
- return;
515
+ }
516
+ enterDetail() {
517
+ if (this.state.currentTab === Tab.CURRENT_FLOW && this.lanes[this.state.selectedLaneIndex]) {
518
+ this.state.selectedLaneName = this.lanes[this.state.selectedLaneIndex].name;
519
+ this.state.level = Level.DETAIL;
520
+ this.state.currentPanel = Panel.LEFT;
521
+ this.state.terminalScrollOffset = 0;
522
+ this.state.messageScrollOffset = 0;
523
+ this.state.followMode = true;
524
+ this.refreshLogs();
500
525
  }
501
- // Only allow numbers
502
- if (str && /^\d$/.test(str)) {
503
- this.timeoutInput += str;
504
- this.render();
526
+ else if (this.state.currentTab === Tab.ALL_FLOWS && this.allFlows[this.state.selectedFlowIndex]) {
527
+ // Switch to selected flow
528
+ const flow = this.allFlows[this.state.selectedFlowIndex];
529
+ this.switchToFlow(flow);
505
530
  }
506
531
  }
507
- handleFlowKey(key) {
508
- switch (key) {
509
- case 'f':
510
- case 'escape':
511
- case 'backspace':
512
- case 'right':
513
- case 'return':
514
- case 'enter':
515
- case 'left':
516
- this.view = View.LIST;
517
- this.render();
532
+ moveSelectionUp() {
533
+ switch (this.state.currentTab) {
534
+ case Tab.CURRENT_FLOW:
535
+ this.state.selectedLaneIndex = Math.max(0, this.state.selectedLaneIndex - 1);
518
536
  break;
519
- case 'q':
520
- this.stop();
537
+ case Tab.ALL_FLOWS:
538
+ this.state.selectedFlowIndex = Math.max(0, this.state.selectedFlowIndex - 1);
539
+ break;
540
+ case Tab.UNIFIED_LOG:
541
+ this.state.unifiedLogFollowMode = false;
542
+ this.state.unifiedLogScrollOffset++;
521
543
  break;
522
544
  }
523
545
  }
524
- handleUnifiedLogKey(key) {
525
- const pageSize = Math.max(10, this.screenHeight - 12);
526
- switch (key) {
527
- case 'up':
528
- this.unifiedLogFollowMode = false;
529
- this.unifiedLogScrollOffset++;
530
- this.render();
546
+ moveSelectionDown() {
547
+ switch (this.state.currentTab) {
548
+ case Tab.CURRENT_FLOW:
549
+ this.state.selectedLaneIndex = Math.min(this.lanes.length - 1, this.state.selectedLaneIndex + 1);
531
550
  break;
532
- case 'down':
533
- this.unifiedLogScrollOffset = Math.max(0, this.unifiedLogScrollOffset - 1);
534
- if (this.unifiedLogScrollOffset === 0) {
535
- this.unifiedLogFollowMode = true;
536
- }
537
- this.render();
538
- break;
539
- case 'pageup':
540
- this.unifiedLogFollowMode = false;
541
- this.unifiedLogScrollOffset += pageSize;
542
- this.render();
551
+ case Tab.ALL_FLOWS:
552
+ this.state.selectedFlowIndex = Math.min(this.allFlows.length - 1, this.state.selectedFlowIndex + 1);
543
553
  break;
544
- case 'pagedown':
545
- this.unifiedLogScrollOffset = Math.max(0, this.unifiedLogScrollOffset - pageSize);
546
- if (this.unifiedLogScrollOffset === 0) {
547
- this.unifiedLogFollowMode = true;
554
+ case Tab.UNIFIED_LOG:
555
+ this.state.unifiedLogScrollOffset = Math.max(0, this.state.unifiedLogScrollOffset - 1);
556
+ if (this.state.unifiedLogScrollOffset === 0) {
557
+ this.state.unifiedLogFollowMode = true;
548
558
  }
549
- this.render();
550
- break;
551
- case 'f':
552
- this.unifiedLogFollowMode = true;
553
- this.unifiedLogScrollOffset = 0;
554
- this.render();
555
- break;
556
- case 'r':
557
- // Toggle readable format
558
- this.readableFormat = !this.readableFormat;
559
- this.render();
560
- break;
561
- case 'l':
562
- // Cycle through lane filter
563
- this.cycleLaneFilter();
564
- this.unifiedLogScrollOffset = 0;
565
- this.render();
566
- break;
567
- case 'escape':
568
- case 'backspace':
569
- case 'u':
570
- this.view = View.LIST;
571
- this.render();
572
- break;
573
- case 'q':
574
- this.stop();
575
559
  break;
576
560
  }
577
561
  }
578
- /**
579
- * Cycle through available lanes for filtering
580
- */
581
- cycleLaneFilter() {
582
- const lanes = this.unifiedLogBuffer?.getLanes() || [];
583
- if (lanes.length === 0) {
584
- this.laneFilter = null;
562
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
563
+ // Action Menu
564
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
565
+ openActionMenu() {
566
+ this.state.actionItems = this.getContextActions();
567
+ if (this.state.actionItems.length === 0)
585
568
  return;
586
- }
587
- if (this.laneFilter === null) {
588
- // Show first lane
589
- this.laneFilter = lanes[0];
590
- }
591
- else {
592
- const currentIndex = lanes.indexOf(this.laneFilter);
593
- if (currentIndex === -1 || currentIndex === lanes.length - 1) {
594
- // Reset to all lanes
595
- this.laneFilter = null;
569
+ this.state.selectedActionIndex = 0;
570
+ this.state.actionMenuVisible = true;
571
+ }
572
+ getContextActions() {
573
+ // Get actions based on current context
574
+ if (this.state.level === Level.DASHBOARD) {
575
+ if (this.state.currentTab === Tab.CURRENT_FLOW && this.lanes[this.state.selectedLaneIndex]) {
576
+ return this.getLaneActions(this.lanes[this.state.selectedLaneIndex]);
596
577
  }
597
- else {
598
- // Next lane
599
- this.laneFilter = lanes[currentIndex + 1];
578
+ else if (this.state.currentTab === Tab.ALL_FLOWS && this.allFlows[this.state.selectedFlowIndex]) {
579
+ return this.getFlowActions(this.allFlows[this.state.selectedFlowIndex]);
600
580
  }
581
+ else if (this.state.currentTab === Tab.UNIFIED_LOG) {
582
+ return this.getLogActions();
583
+ }
584
+ }
585
+ else if (this.state.level === Level.DETAIL && this.state.selectedLaneName) {
586
+ const lane = this.lanes.find(l => l.name === this.state.selectedLaneName);
587
+ if (lane)
588
+ return this.getLaneActions(lane);
601
589
  }
590
+ return [];
602
591
  }
603
- handleFlowsDashboardKey(key) {
604
- // Handle confirmation dialog first
605
- if (this.confirmAction) {
606
- if (key === 'y') {
607
- this.executeConfirmedAction();
608
- return;
609
- }
610
- else if (key === 'n' || key === 'escape') {
611
- this.confirmAction = null;
612
- this.render();
613
- return;
614
- }
615
- // Other keys cancel confirmation
616
- this.confirmAction = null;
617
- this.render();
618
- return;
592
+ getLaneActions(lane) {
593
+ const status = this.getLaneStatus(lane.path, lane.name);
594
+ const isRunning = status.status === 'running';
595
+ return [
596
+ {
597
+ id: 'message',
598
+ label: 'Send Message',
599
+ icon: '💬',
600
+ action: () => this.startMessageInput(lane.name),
601
+ disabled: !isRunning,
602
+ disabledReason: 'Lane not running',
603
+ },
604
+ {
605
+ id: 'timeout',
606
+ label: 'Set Timeout',
607
+ icon: '⏱️',
608
+ action: () => this.startTimeoutInput(lane.name),
609
+ disabled: !isRunning,
610
+ disabledReason: 'Lane not running',
611
+ },
612
+ {
613
+ id: 'stop',
614
+ label: 'Stop Lane',
615
+ icon: '🔴',
616
+ action: () => this.killLane(lane),
617
+ disabled: !isRunning,
618
+ disabledReason: 'Lane not running',
619
+ },
620
+ {
621
+ id: 'logs',
622
+ label: 'View Full Logs',
623
+ icon: '📋',
624
+ action: () => {
625
+ this.state.selectedLaneName = lane.name;
626
+ this.state.level = Level.DETAIL;
627
+ this.state.currentPanel = Panel.LEFT;
628
+ this.refreshLogs();
629
+ this.state.actionMenuVisible = false;
630
+ this.render();
631
+ },
632
+ },
633
+ ];
634
+ }
635
+ getFlowActions(flow) {
636
+ const isCurrent = flow.runDir === this.runDir;
637
+ return [
638
+ {
639
+ id: 'switch',
640
+ label: 'Switch to Flow',
641
+ icon: '🔄',
642
+ action: () => this.switchToFlow(flow),
643
+ disabled: isCurrent,
644
+ disabledReason: 'Already viewing this flow',
645
+ },
646
+ {
647
+ id: 'delete',
648
+ label: 'Delete Flow',
649
+ icon: '🗑️',
650
+ action: () => this.deleteFlow(flow),
651
+ disabled: flow.isAlive || isCurrent,
652
+ disabledReason: flow.isAlive ? 'Cannot delete running flow' : 'Cannot delete current flow',
653
+ },
654
+ ];
655
+ }
656
+ getLogActions() {
657
+ const lanes = this.unifiedLogBuffer?.getLanes() || [];
658
+ return [
659
+ {
660
+ id: 'filter',
661
+ label: this.state.laneFilter ? `Filter: ${this.state.laneFilter}` : 'Filter by Lane',
662
+ icon: '🔍',
663
+ action: () => this.cycleLaneFilter(lanes),
664
+ },
665
+ {
666
+ id: 'follow',
667
+ label: this.state.unifiedLogFollowMode ? 'Pause Follow' : 'Resume Follow',
668
+ icon: this.state.unifiedLogFollowMode ? '⏸️' : '▶️',
669
+ action: () => {
670
+ this.state.unifiedLogFollowMode = !this.state.unifiedLogFollowMode;
671
+ this.state.actionMenuVisible = false;
672
+ this.render();
673
+ },
674
+ },
675
+ ];
676
+ }
677
+ executeAction() {
678
+ const action = this.state.actionItems[this.state.selectedActionIndex];
679
+ if (action && !action.disabled) {
680
+ this.state.actionMenuVisible = false;
681
+ action.action();
619
682
  }
620
- switch (key) {
621
- case 'up':
622
- this.selectedFlowIndex = Math.max(0, this.selectedFlowIndex - 1);
623
- this.render();
624
- break;
625
- case 'down':
626
- this.selectedFlowIndex = Math.min(this.allFlows.length - 1, this.selectedFlowIndex + 1);
627
- this.render();
628
- break;
629
- case 'right':
630
- case 'return':
631
- case 'enter':
632
- // Switch to selected flow
633
- if (this.allFlows[this.selectedFlowIndex]) {
634
- const flow = this.allFlows[this.selectedFlowIndex];
635
- this.runDir = flow.runDir;
636
- // Restart log buffer for new run
637
- if (this.unifiedLogBuffer) {
638
- this.unifiedLogBuffer.stopStreaming();
639
- }
640
- this.unifiedLogBuffer = new buffer_1.LogBufferService(this.runDir);
641
- this.unifiedLogBuffer.startStreaming();
642
- this.lanes = [];
643
- this.laneProcessStatuses.clear();
644
- this.view = View.LIST;
645
- this.showNotification(`Switched to flow: ${flow.runId}`, 'info');
646
- this.refresh();
647
- }
648
- break;
649
- case 'd':
650
- // Delete flow (with confirmation)
651
- if (this.allFlows[this.selectedFlowIndex]) {
652
- const flow = this.allFlows[this.selectedFlowIndex];
653
- if (flow.isAlive) {
654
- this.showNotification('Cannot delete a running flow. Stop it first.', 'error');
655
- }
656
- else if (flow.runDir === this.runDir) {
657
- this.showNotification('Cannot delete the currently viewed flow.', 'error');
658
- }
659
- else {
660
- this.confirmAction = {
661
- type: 'delete-flow',
662
- target: flow.runId,
663
- time: Date.now(),
664
- };
665
- this.render();
666
- }
667
- }
668
- break;
669
- case 'r':
670
- // Refresh flows
671
- this.discoverFlows();
672
- this.showNotification('Flows refreshed', 'info');
673
- this.render();
674
- break;
675
- case 'escape':
676
- case 'backspace':
677
- case 'm':
678
- this.view = View.LIST;
679
- this.render();
680
- break;
681
- case 'q':
682
- this.stop();
683
- break;
683
+ else if (action?.disabled) {
684
+ this.showNotification(action.disabledReason || 'Action not available', 'error');
684
685
  }
685
686
  }
686
- /**
687
- * Execute a confirmed action (delete flow, kill process, etc.)
688
- */
689
- executeConfirmedAction() {
690
- if (!this.confirmAction)
691
- return;
692
- const { type, target } = this.confirmAction;
693
- this.confirmAction = null;
694
- if (type === 'delete-flow') {
695
- const flow = this.allFlows.find(f => f.runId === target);
696
- if (flow) {
697
- try {
698
- // Delete the flow directory
699
- fs.rmSync(flow.runDir, { recursive: true, force: true });
700
- this.showNotification(`Deleted flow: ${target}`, 'success');
701
- // Refresh the list
702
- this.discoverFlows();
703
- // Adjust selection if needed
704
- if (this.selectedFlowIndex >= this.allFlows.length) {
705
- this.selectedFlowIndex = Math.max(0, this.allFlows.length - 1);
706
- }
707
- }
708
- catch (err) {
709
- this.showNotification(`Failed to delete flow: ${err}`, 'error');
710
- }
711
- }
687
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
688
+ // Actions Implementation
689
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
690
+ startMessageInput(laneName) {
691
+ this.state.inputMode = 'message';
692
+ this.state.inputBuffer = '';
693
+ this.state.inputTarget = laneName;
694
+ this.state.actionMenuVisible = false;
695
+ this.render();
696
+ }
697
+ startTimeoutInput(laneName) {
698
+ this.state.inputMode = 'timeout';
699
+ this.state.inputBuffer = '';
700
+ this.state.inputTarget = laneName;
701
+ this.state.actionMenuVisible = false;
702
+ this.render();
703
+ }
704
+ submitInput() {
705
+ if (this.state.inputMode === 'message') {
706
+ this.sendMessage(this.state.inputTarget, this.state.inputBuffer);
712
707
  }
708
+ else if (this.state.inputMode === 'timeout') {
709
+ this.setLaneTimeout(this.state.inputTarget, this.state.inputBuffer);
710
+ }
711
+ this.state.inputMode = 'none';
712
+ this.state.inputBuffer = '';
713
+ this.state.inputTarget = null;
713
714
  this.render();
714
715
  }
715
- sendIntervention(message) {
716
- if (!this.selectedLaneName)
717
- return;
718
- const lane = this.lanes.find(l => l.name === this.selectedLaneName);
719
- if (!lane)
716
+ sendMessage(laneName, message) {
717
+ const lane = this.lanes.find(l => l.name === laneName);
718
+ if (!lane || !message.trim())
720
719
  return;
721
720
  try {
722
- const interventionPath = (0, path_1.safeJoin)(lane.path, 'intervention.txt');
723
- fs.writeFileSync(interventionPath, message, 'utf8');
724
- // Also log it to the conversation
721
+ // Create pending-intervention.json for the system
722
+ (0, intervention_1.createInterventionRequest)(lane.path, {
723
+ type: intervention_1.InterventionType.USER_MESSAGE,
724
+ message: (0, intervention_1.wrapUserIntervention)(message),
725
+ source: 'user',
726
+ priority: 10
727
+ });
728
+ // Kill the process if it's running - this triggers the restart in orchestrator
729
+ const status = this.laneProcessStatuses.get(lane.name);
730
+ if (status && status.pid && status.actualStatus === 'running') {
731
+ try {
732
+ process.kill(status.pid, 'SIGTERM');
733
+ }
734
+ catch {
735
+ // Ignore kill errors
736
+ }
737
+ }
725
738
  const convoPath = (0, path_1.safeJoin)(lane.path, 'conversation.jsonl');
726
739
  const entry = {
727
740
  timestamp: new Date().toISOString(),
@@ -732,16 +745,14 @@ class InteractiveMonitor {
732
745
  model: 'manual'
733
746
  };
734
747
  fs.appendFileSync(convoPath, JSON.stringify(entry) + '\n', 'utf8');
735
- this.showNotification('Intervention message sent', 'success');
748
+ this.showNotification('Message sent', 'success');
736
749
  }
737
- catch (e) {
738
- this.showNotification('Failed to send intervention', 'error');
750
+ catch {
751
+ this.showNotification('Failed to send message', 'error');
739
752
  }
740
753
  }
741
- sendTimeoutUpdate(timeoutStr) {
742
- if (!this.selectedLaneName)
743
- return;
744
- const lane = this.lanes.find(l => l.name === this.selectedLaneName);
754
+ setLaneTimeout(laneName, timeoutStr) {
755
+ const lane = this.lanes.find(l => l.name === laneName);
745
756
  if (!lane)
746
757
  return;
747
758
  try {
@@ -752,846 +763,609 @@ class InteractiveMonitor {
752
763
  }
753
764
  const timeoutPath = (0, path_1.safeJoin)(lane.path, 'timeout.txt');
754
765
  fs.writeFileSync(timeoutPath, String(timeoutMs), 'utf8');
755
- this.showNotification(`Timeout updated to ${Math.round(timeoutMs / 1000)}s`, 'success');
766
+ this.showNotification(`Timeout set to ${Math.round(timeoutMs / 1000)}s`, 'success');
756
767
  }
757
- catch (e) {
758
- this.showNotification('Failed to update timeout', 'error');
768
+ catch {
769
+ this.showNotification('Failed to set timeout', 'error');
759
770
  }
760
771
  }
761
- refreshLogs() {
762
- if (!this.selectedLaneName)
763
- return;
764
- const lane = this.lanes.find(l => l.name === this.selectedLaneName);
765
- if (!lane)
766
- return;
767
- const convoPath = (0, path_1.safeJoin)(lane.path, 'conversation.jsonl');
768
- this.currentLogs = (0, state_1.readLog)(convoPath);
769
- // Keep selection in bounds after refresh
770
- if (this.selectedMessageIndex >= this.currentLogs.length) {
771
- this.selectedMessageIndex = Math.max(0, this.currentLogs.length - 1);
772
+ killLane(lane) {
773
+ const status = this.getLaneStatus(lane.path, lane.name);
774
+ if (status.pid && status.status === 'running') {
775
+ try {
776
+ process.kill(status.pid, 'SIGTERM');
777
+ this.showNotification(`Sent SIGTERM to PID ${status.pid}`, 'success');
778
+ }
779
+ catch {
780
+ this.showNotification(`Failed to kill PID ${status.pid}`, 'error');
781
+ }
782
+ }
783
+ this.state.actionMenuVisible = false;
784
+ this.render();
785
+ }
786
+ switchToFlow(flow) {
787
+ this.runDir = flow.runDir;
788
+ if (this.unifiedLogBuffer) {
789
+ this.unifiedLogBuffer.stopStreaming();
790
+ }
791
+ this.unifiedLogBuffer = new buffer_1.LogBufferService(this.runDir);
792
+ this.unifiedLogBuffer.startStreaming();
793
+ this.lanes = [];
794
+ this.laneProcessStatuses.clear();
795
+ this.state.currentTab = Tab.CURRENT_FLOW;
796
+ this.state.level = Level.DASHBOARD;
797
+ this.state.selectedLaneIndex = 0;
798
+ this.state.actionMenuVisible = false;
799
+ this.showNotification(`Switched to: ${flow.runId}`, 'info');
800
+ this.refresh();
801
+ }
802
+ deleteFlow(flow) {
803
+ try {
804
+ fs.rmSync(flow.runDir, { recursive: true, force: true });
805
+ this.showNotification(`Deleted: ${flow.runId}`, 'success');
806
+ this.discoverFlows();
807
+ if (this.state.selectedFlowIndex >= this.allFlows.length) {
808
+ this.state.selectedFlowIndex = Math.max(0, this.allFlows.length - 1);
809
+ }
810
+ }
811
+ catch {
812
+ this.showNotification('Failed to delete flow', 'error');
772
813
  }
814
+ this.state.actionMenuVisible = false;
815
+ this.render();
773
816
  }
817
+ cycleLaneFilter(lanes) {
818
+ if (lanes.length === 0) {
819
+ this.state.laneFilter = null;
820
+ }
821
+ else if (this.state.laneFilter === null) {
822
+ this.state.laneFilter = lanes[0];
823
+ }
824
+ else {
825
+ const idx = lanes.indexOf(this.state.laneFilter);
826
+ if (idx === -1 || idx === lanes.length - 1) {
827
+ this.state.laneFilter = null;
828
+ }
829
+ else {
830
+ this.state.laneFilter = lanes[idx + 1];
831
+ }
832
+ }
833
+ this.state.unifiedLogScrollOffset = 0;
834
+ this.state.actionMenuVisible = false;
835
+ this.render();
836
+ }
837
+ showHelp() {
838
+ this.showNotification('←/→: Navigate | ↑/↓: Select | Enter: Action | Esc: Back | Q: Quit', 'info');
839
+ this.render();
840
+ }
841
+ showNotification(message, type) {
842
+ this.state.notification = { message, type, time: Date.now() };
843
+ this.render();
844
+ }
845
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
846
+ // Data Refresh
847
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
774
848
  refresh() {
775
- this.lanes = this.listLanesWithDeps(this.runDir);
776
- // Update process statuses for accurate display
849
+ this.lanes = this.listLanes();
777
850
  this.updateProcessStatuses();
778
- if (this.view !== View.LIST && this.view !== View.UNIFIED_LOG && this.view !== View.FLOWS_DASHBOARD) {
851
+ if (this.state.level === Level.DETAIL && this.state.selectedLaneName) {
779
852
  this.refreshLogs();
780
853
  }
781
- // Refresh flows list periodically
782
- if (this.view === View.FLOWS_DASHBOARD) {
854
+ if (this.state.currentTab === Tab.ALL_FLOWS) {
783
855
  this.discoverFlows();
784
856
  }
785
857
  this.render();
786
858
  }
787
- /**
788
- * Update process statuses for all lanes
789
- */
790
- updateProcessStatuses() {
791
- const lanesDir = (0, path_1.safeJoin)(this.runDir, 'lanes');
792
- if (!fs.existsSync(lanesDir))
859
+ refreshLogs() {
860
+ if (!this.state.selectedLaneName)
861
+ return;
862
+ const lane = this.lanes.find(l => l.name === this.state.selectedLaneName);
863
+ if (!lane)
793
864
  return;
865
+ const convoPath = (0, path_1.safeJoin)(lane.path, 'conversation.jsonl');
866
+ this.currentLogs = (0, state_1.readLog)(convoPath);
867
+ if (this.state.messageScrollOffset >= this.currentLogs.length) {
868
+ this.state.messageScrollOffset = Math.max(0, this.currentLogs.length - 1);
869
+ }
870
+ }
871
+ updateProcessStatuses() {
794
872
  for (const lane of this.lanes) {
795
873
  const status = (0, process_1.getLaneProcessStatus)(lane.path, lane.name);
796
874
  this.laneProcessStatuses.set(lane.name, status);
797
875
  }
798
876
  }
799
- killLane() {
800
- if (!this.selectedLaneName)
801
- return;
802
- const lane = this.lanes.find(l => l.name === this.selectedLaneName);
803
- if (!lane)
804
- return;
805
- const status = this.getLaneStatus(lane.path, lane.name);
806
- if (status.pid && status.status === 'running') {
807
- try {
808
- process.kill(status.pid, 'SIGTERM');
809
- this.showNotification(`Sent SIGTERM to PID ${status.pid}`, 'success');
810
- }
811
- catch (e) {
812
- this.showNotification(`Failed to kill PID ${status.pid}`, 'error');
813
- }
877
+ listLanes() {
878
+ const lanesDir = (0, path_1.safeJoin)(this.runDir, 'lanes');
879
+ if (!fs.existsSync(lanesDir))
880
+ return [];
881
+ return fs.readdirSync(lanesDir)
882
+ .filter(d => fs.statSync((0, path_1.safeJoin)(lanesDir, d)).isDirectory())
883
+ .map(name => ({ name, path: (0, path_1.safeJoin)(lanesDir, name) }));
884
+ }
885
+ getLaneStatus(lanePath, _laneName) {
886
+ const statePath = (0, path_1.safeJoin)(lanePath, 'state.json');
887
+ const state = (0, state_1.loadState)(statePath);
888
+ if (!state) {
889
+ return { status: 'pending', currentTask: 0, totalTasks: '?', progress: '0%', duration: 0, pipelineBranch: '-', chatId: '-' };
890
+ }
891
+ const progress = state.totalTasks > 0 ? Math.round((state.currentTaskIndex / state.totalTasks) * 100) : 0;
892
+ const duration = state.startTime ? (state.endTime
893
+ ? state.endTime - state.startTime
894
+ : (state.status === 'running' ? Date.now() - state.startTime : 0)) : 0;
895
+ return {
896
+ status: state.status || 'unknown',
897
+ currentTask: state.currentTaskIndex || 0,
898
+ totalTasks: state.totalTasks || '?',
899
+ progress: `${progress}%`,
900
+ pipelineBranch: state.pipelineBranch || '-',
901
+ chatId: state.chatId || '-',
902
+ duration,
903
+ error: state.error,
904
+ pid: state.pid,
905
+ waitingFor: state.waitingFor || [],
906
+ };
907
+ }
908
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
909
+ // Rendering
910
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
911
+ render() {
912
+ process.stdout.write('\x1Bc'); // Clear screen
913
+ // Clear old notification
914
+ if (this.state.notification && Date.now() - this.state.notification.time > 3000) {
915
+ this.state.notification = null;
916
+ }
917
+ if (this.state.level === Level.DASHBOARD) {
918
+ this.renderDashboard();
814
919
  }
815
920
  else {
816
- this.showNotification(`No running process found for ${this.selectedLaneName}`, 'info');
921
+ this.renderDetail();
922
+ }
923
+ // Overlay: Action Menu
924
+ if (this.state.actionMenuVisible) {
925
+ this.renderActionMenu();
926
+ }
927
+ // Overlay: Input Mode
928
+ if (this.state.inputMode !== 'none') {
929
+ this.renderInputOverlay();
930
+ }
931
+ // Overlay: Help
932
+ if (this.state.showHelp) {
933
+ this.renderHelpOverlay();
817
934
  }
818
935
  }
819
- showNotification(message, type) {
820
- this.notification = { message, type, time: Date.now() };
821
- this.render();
822
- }
823
- // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
824
- // UI Layout Helpers - Consistent header/footer across all views
825
- // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
826
- renderHeader(title, breadcrumb = []) {
827
- const width = Math.min(this.screenWidth, 120);
828
- const line = UI.CHARS.hLine.repeat(width);
829
- // Flow status
936
+ renderDashboard() {
937
+ const w = this.screenWidth;
938
+ const h = this.screenHeight;
939
+ const { cyan, reset, bold, dim, gray, green, yellow, red } = UI.COLORS;
940
+ // Header
830
941
  const flowSummary = (0, process_1.getFlowSummary)(this.runDir);
831
- const flowStatusIcon = flowSummary.isAlive ? '🟢' : (flowSummary.completed === flowSummary.total && flowSummary.total > 0 ? '✅' : '🔴');
832
- // Breadcrumb
833
- const crumbs = ['CursorFlow', ...breadcrumb].join(` ${UI.COLORS.gray}›${UI.COLORS.reset} `);
834
- // Time
942
+ const statusIcon = flowSummary.isAlive ? UI.ICONS.live : UI.ICONS.stopped;
835
943
  const timeStr = new Date().toLocaleTimeString('en-US', { hour12: false });
836
- process.stdout.write(`${UI.COLORS.cyan}${line}${UI.COLORS.reset}\n`);
837
- process.stdout.write(`${UI.COLORS.bold}${crumbs}${UI.COLORS.reset} ${flowStatusIcon} `);
838
- process.stdout.write(`${UI.COLORS.dim}${timeStr}${UI.COLORS.reset}\n`);
839
- process.stdout.write(`${UI.COLORS.cyan}${line}${UI.COLORS.reset}\n`);
840
- }
841
- renderFooter(actions) {
842
- const width = Math.min(this.screenWidth, 120);
843
- const line = UI.CHARS.hLine.repeat(width);
844
- // Notification area
845
- if (this.notification && Date.now() - this.notification.time < 3000) {
846
- const nColor = this.notification.type === 'error' ? UI.COLORS.red
847
- : this.notification.type === 'success' ? UI.COLORS.green
848
- : UI.COLORS.cyan;
849
- process.stdout.write(`\n${nColor}🔔 ${this.notification.message}${UI.COLORS.reset}\n`);
850
- }
851
- // Confirmation dialog area
852
- if (this.confirmAction && Date.now() - this.confirmAction.time < 10000) {
853
- const actionName = this.confirmAction.type === 'delete-flow' ? 'DELETE FLOW' : 'KILL PROCESS';
854
- process.stdout.write(`\n${UI.COLORS.yellow}⚠️ Confirm ${actionName}: ${this.confirmAction.target}? [Y] Yes / [N] No${UI.COLORS.reset}\n`);
855
- }
856
- process.stdout.write(`\n${UI.COLORS.cyan}${line}${UI.COLORS.reset}\n`);
857
- const formattedActions = actions.map(a => {
858
- const parts = a.split('] ');
859
- if (parts.length === 2) {
860
- // Use regex with global flag to replace all occurrences
861
- return `${UI.COLORS.yellow}[${parts[0].replace(/\[/g, '')}]${UI.COLORS.reset} ${parts[1]}`;
944
+ const runId = path.basename(this.runDir);
945
+ const hLine = UI.CHARS.hLine.repeat(w);
946
+ process.stdout.write(`${cyan}${hLine}${reset}\n`);
947
+ process.stdout.write(`${bold} CursorFlow Monitor${reset} ${gray}${runId}${reset} ${statusIcon} ${dim}${timeStr}${reset}\n`);
948
+ // Tabs
949
+ const tabs = [
950
+ { label: '현재 플로우', active: this.state.currentTab === Tab.CURRENT_FLOW },
951
+ { label: '모든 플로우', active: this.state.currentTab === Tab.ALL_FLOWS },
952
+ { label: '통합 로그', active: this.state.currentTab === Tab.UNIFIED_LOG },
953
+ ];
954
+ let tabLine = '';
955
+ tabs.forEach((tab, i) => {
956
+ if (tab.active) {
957
+ tabLine += ` ${cyan}[${UI.ICONS.selected} ${tab.label}]${reset}`;
958
+ }
959
+ else {
960
+ tabLine += ` ${dim}[${UI.ICONS.unselected} ${tab.label}]${reset}`;
862
961
  }
863
- return a;
864
962
  });
865
- process.stdout.write(` ${formattedActions.join(' ')}\n`);
866
- }
867
- renderSectionTitle(title, extra) {
868
- const extraStr = extra ? ` ${UI.COLORS.dim}${extra}${UI.COLORS.reset}` : '';
869
- process.stdout.write(`\n${UI.COLORS.bold}${title}${UI.COLORS.reset}${extraStr}\n`);
870
- process.stdout.write(`${UI.COLORS.gray}${'─'.repeat(40)}${UI.COLORS.reset}\n`);
871
- }
872
- render() {
873
- // Clear screen
874
- process.stdout.write('\x1Bc');
875
- // Clear old notifications
876
- if (this.notification && Date.now() - this.notification.time > 3000) {
877
- this.notification = null;
878
- }
879
- // Clear old confirmation
880
- if (this.confirmAction && Date.now() - this.confirmAction.time > 10000) {
881
- this.confirmAction = null;
882
- }
883
- switch (this.view) {
884
- case View.LIST:
885
- this.renderList();
886
- break;
887
- case View.LANE_DETAIL:
888
- this.renderLaneDetail();
889
- break;
890
- case View.MESSAGE_DETAIL:
891
- this.renderMessageDetail();
892
- break;
893
- case View.FLOW:
894
- this.renderFlow();
895
- break;
896
- case View.TERMINAL:
897
- this.renderTerminal();
898
- break;
899
- case View.INTERVENE:
900
- this.renderIntervene();
901
- break;
902
- case View.TIMEOUT:
903
- this.renderTimeout();
904
- break;
905
- case View.UNIFIED_LOG:
906
- this.renderUnifiedLog();
907
- break;
908
- case View.FLOWS_DASHBOARD:
909
- this.renderFlowsDashboard();
910
- break;
963
+ process.stdout.write(`${cyan}${hLine}${reset}\n`);
964
+ process.stdout.write(`${tabLine}\n`);
965
+ process.stdout.write(`${cyan}${hLine}${reset}\n`);
966
+ // Content based on tab
967
+ const contentHeight = h - 10;
968
+ if (this.state.currentTab === Tab.CURRENT_FLOW) {
969
+ this.renderLaneList(contentHeight);
911
970
  }
971
+ else if (this.state.currentTab === Tab.ALL_FLOWS) {
972
+ this.renderFlowList(contentHeight);
973
+ }
974
+ else {
975
+ this.renderUnifiedLog(contentHeight);
976
+ }
977
+ // Notification
978
+ if (this.state.notification) {
979
+ const nColor = this.state.notification.type === 'error' ? red
980
+ : this.state.notification.type === 'success' ? green : cyan;
981
+ process.stdout.write(`\n${nColor} 🔔 ${this.state.notification.message}${reset}\n`);
982
+ }
983
+ else {
984
+ process.stdout.write('\n');
985
+ }
986
+ // Footer
987
+ process.stdout.write(`${cyan}${hLine}${reset}\n`);
988
+ const help = this.state.currentTab === Tab.UNIFIED_LOG
989
+ ? `${yellow}[←/→]${reset} Tab ${yellow}[↑/↓]${reset} Scroll ${yellow}[Space]${reset} Follow ${yellow}[R]${reset} Format ${yellow}[Enter]${reset} Action ${yellow}[?]${reset} Help`
990
+ : `${yellow}[←/→]${reset} Tab/Enter ${yellow}[↑/↓]${reset} Select ${yellow}[Enter]${reset} Action ${yellow}[?]${reset} Help ${yellow}[Q]${reset} Quit`;
991
+ process.stdout.write(` ${help}\n`);
912
992
  }
913
- renderList() {
993
+ renderLaneList(maxLines) {
994
+ const { cyan, reset, dim, gray, green, yellow, red, bgGray } = UI.COLORS;
995
+ // Summary
914
996
  const flowSummary = (0, process_1.getFlowSummary)(this.runDir);
915
- const runId = path.basename(this.runDir);
916
- this.renderHeader('Lane Dashboard', [runId]);
917
- // Summary line
918
- const summaryParts = [
919
- `${flowSummary.running} ${UI.COLORS.cyan}running${UI.COLORS.reset}`,
920
- `${flowSummary.completed} ${UI.COLORS.green}done${UI.COLORS.reset}`,
921
- `${flowSummary.failed} ${UI.COLORS.red}failed${UI.COLORS.reset}`,
922
- `${flowSummary.dead} ${UI.COLORS.yellow}stale${UI.COLORS.reset}`,
923
- ];
924
- process.stdout.write(` ${UI.COLORS.dim}Lanes:${UI.COLORS.reset} ${summaryParts.join(' │ ')}\n`);
997
+ const summary = `${cyan}${flowSummary.running}${reset} running │ ${green}${flowSummary.completed}${reset} done │ ${yellow}${flowSummary.pending || 0}${reset} waiting │ ${red}${flowSummary.failed}${reset} failed`;
998
+ process.stdout.write(` ${dim}Summary:${reset} ${summary}\n\n`);
925
999
  if (this.lanes.length === 0) {
926
- process.stdout.write(`\n ${UI.COLORS.dim}No lanes found${UI.COLORS.reset}\n`);
927
- this.renderFooter(['[Q] Quit', '[M] All Flows']);
1000
+ process.stdout.write(` ${dim}No lanes found. Run ${cyan}cursorflow run${reset}${dim} to start.${reset}\n`);
928
1001
  return;
929
1002
  }
930
- const laneStatuses = {};
931
- this.lanes.forEach(l => laneStatuses[l.name] = this.getLaneStatus(l.path, l.name));
932
- const maxNameLen = Math.max(...this.lanes.map(l => l.name.length), 12);
933
- process.stdout.write(`\n ${'Lane'.padEnd(maxNameLen)} ${'Status'.padEnd(12)} ${'PID'.padEnd(7)} ${'Time'.padEnd(8)} ${'Tasks'.padEnd(6)} Next\n`);
934
- process.stdout.write(` ${'─'.repeat(maxNameLen)} ${'─'.repeat(12)} ${'─'.repeat(7)} ${'─'.repeat(8)} ${'─'.repeat(6)} ${'─'.repeat(25)}\n`);
935
- this.lanes.forEach((lane, i) => {
936
- const isSelected = i === this.selectedLaneIndex;
937
- const status = laneStatuses[lane.name];
1003
+ // Header
1004
+ const maxNameLen = Math.max(12, ...this.lanes.map(l => l.name.length));
1005
+ process.stdout.write(` ${dim} # ${'Lane'.padEnd(maxNameLen)} │ Status │ Progress │ Duration │ Next${reset}\n`);
1006
+ process.stdout.write(` ${dim}${''.repeat(maxNameLen + 60)}${reset}\n`);
1007
+ // List
1008
+ const visibleLanes = this.lanes.slice(0, maxLines - 4);
1009
+ visibleLanes.forEach((lane, i) => {
1010
+ const isSelected = i === this.state.selectedLaneIndex;
1011
+ const status = this.getLaneStatus(lane.path, lane.name);
938
1012
  const processStatus = this.laneProcessStatuses.get(lane.name);
939
- // Determine the accurate status based on process detection
1013
+ // Status display
940
1014
  let displayStatus = status.status;
941
- let statusColor = UI.COLORS.gray;
942
1015
  let statusIcon = this.getStatusIcon(status.status);
943
- if (processStatus) {
944
- if (processStatus.isStale) {
945
- displayStatus = 'STALE';
946
- statusIcon = '💀';
947
- statusColor = UI.COLORS.yellow;
948
- }
949
- else if (processStatus.actualStatus === 'dead' && status.status === 'running') {
950
- displayStatus = 'DEAD';
951
- statusIcon = '☠️';
952
- statusColor = UI.COLORS.red;
953
- }
954
- else if (processStatus.actualStatus === 'running') {
955
- statusColor = UI.COLORS.cyan;
956
- }
957
- else if (status.status === 'completed') {
958
- statusColor = UI.COLORS.green;
959
- }
960
- else if (status.status === 'failed') {
961
- statusColor = UI.COLORS.red;
962
- }
1016
+ let statusColor = gray;
1017
+ if (processStatus?.isStale) {
1018
+ displayStatus = 'STALE';
1019
+ statusIcon = UI.ICONS.stale;
1020
+ statusColor = yellow;
963
1021
  }
964
- const statusText = `${statusIcon} ${displayStatus}`.padEnd(12);
965
- // Process indicator
966
- let pidText = '-'.padEnd(7);
967
- if (processStatus?.pid) {
968
- const pidIcon = processStatus.processRunning ? '●' : '○';
969
- const pidColor = processStatus.processRunning ? UI.COLORS.green : UI.COLORS.red;
970
- pidText = `${pidColor}${pidIcon}${UI.COLORS.reset}${processStatus.pid}`.padEnd(7 + 9); // +9 for color codes
1022
+ else if (processStatus?.actualStatus === 'running') {
1023
+ statusColor = cyan;
971
1024
  }
972
- // Duration
973
- const duration = processStatus?.duration || status.duration;
974
- const timeText = this.formatDuration(duration).padEnd(8);
975
- // Tasks
976
- let tasksText = '-'.padEnd(6);
977
- if (typeof status.totalTasks === 'number') {
978
- tasksText = `${status.currentTask}/${status.totalTasks}`.padEnd(6);
1025
+ else if (status.status === 'completed') {
1026
+ statusColor = green;
1027
+ }
1028
+ else if (status.status === 'failed') {
1029
+ statusColor = red;
979
1030
  }
1031
+ // Progress
1032
+ const progressText = `${status.currentTask}/${status.totalTasks}`;
1033
+ // Duration
1034
+ const duration = this.formatDuration(processStatus?.duration || status.duration);
980
1035
  // Next action
981
1036
  let nextAction = '-';
982
- if (status.status === 'completed') {
1037
+ if (status.status === 'completed')
983
1038
  nextAction = '✓ Done';
984
- }
985
- else if (status.status === 'waiting') {
986
- if (status.waitingFor?.length > 0) {
987
- nextAction = `⏳ ${status.waitingFor.join(', ')}`;
988
- }
989
- else {
990
- nextAction = '⏳ waiting';
991
- }
992
- }
993
- else if (processStatus?.actualStatus === 'running') {
1039
+ else if (status.status === 'waiting')
1040
+ nextAction = `⏳ ${status.waitingFor?.join(', ') || 'waiting'}`;
1041
+ else if (processStatus?.actualStatus === 'running')
994
1042
  nextAction = '🚀 working...';
995
- }
996
- else if (processStatus?.isStale) {
997
- nextAction = '⚠️ died unexpectedly';
998
- }
999
- // Truncate next action
1000
- if (nextAction.length > 25)
1001
- nextAction = nextAction.substring(0, 22) + '...';
1002
- const prefix = isSelected ? ` ${UI.COLORS.cyan}▶${UI.COLORS.reset} ` : ' ';
1003
- const rowBg = isSelected ? UI.COLORS.bgGray : '';
1004
- const rowEnd = isSelected ? UI.COLORS.reset : '';
1005
- process.stdout.write(`${rowBg}${prefix}${lane.name.padEnd(maxNameLen)} ${statusColor}${statusText}${UI.COLORS.reset} ${pidText} ${timeText} ${tasksText} ${nextAction}${rowEnd}\n`);
1043
+ else if (processStatus?.isStale)
1044
+ nextAction = '⚠️ died';
1045
+ if (nextAction.length > 20)
1046
+ nextAction = nextAction.substring(0, 17) + '...';
1047
+ // Render row
1048
+ const prefix = isSelected ? `${cyan}▶${reset}` : ' ';
1049
+ const bg = isSelected ? bgGray : '';
1050
+ const endBg = isSelected ? reset : '';
1051
+ const num = String(i + 1).padStart(2);
1052
+ process.stdout.write(`${bg} ${prefix} ${num} ${lane.name.padEnd(maxNameLen)} ${statusColor}${statusIcon} ${displayStatus.padEnd(9)}${reset} ${progressText.padEnd(8)} │ ${duration.padEnd(8)} │ ${nextAction}${endBg}\n`);
1006
1053
  });
1007
- this.renderFooter([
1008
- '[↑↓] Select', '[→/Enter] Details', '[F] Flow', '[U] Unified Logs', '[M] All Flows', '[Q] Quit'
1009
- ]);
1054
+ if (this.lanes.length > visibleLanes.length) {
1055
+ process.stdout.write(` ${dim} ... and ${this.lanes.length - visibleLanes.length} more${reset}\n`);
1056
+ }
1010
1057
  }
1011
- renderLaneDetail() {
1012
- const lane = this.lanes.find(l => l.name === this.selectedLaneName);
1013
- if (!lane) {
1014
- this.view = View.LIST;
1015
- this.render();
1058
+ renderFlowList(maxLines) {
1059
+ const { cyan, reset, dim, gray, green, yellow, red, bgGray } = UI.COLORS;
1060
+ process.stdout.write(` ${dim}Total: ${this.allFlows.length} flows${reset}\n\n`);
1061
+ if (this.allFlows.length === 0) {
1062
+ process.stdout.write(` ${dim}No flows found.${reset}\n`);
1016
1063
  return;
1017
1064
  }
1018
- const status = this.getLaneStatus(lane.path, lane.name);
1019
- const processStatus = this.laneProcessStatuses.get(lane.name);
1020
- this.renderHeader('Lane Detail', [path.basename(this.runDir), lane.name]);
1021
- // Status grid
1022
- const statusColor = status.status === 'completed' ? UI.COLORS.green
1023
- : status.status === 'failed' ? UI.COLORS.red
1024
- : status.status === 'running' ? UI.COLORS.cyan : UI.COLORS.gray;
1025
- const actualStatus = processStatus?.actualStatus || status.status;
1026
- const isStale = processStatus?.isStale || false;
1027
- process.stdout.write(`\n`);
1028
- process.stdout.write(` ${UI.COLORS.dim}Status${UI.COLORS.reset} ${statusColor}${this.getStatusIcon(actualStatus)} ${actualStatus.toUpperCase()}${UI.COLORS.reset}`);
1029
- if (isStale)
1030
- process.stdout.write(` ${UI.COLORS.yellow}(stale)${UI.COLORS.reset}`);
1031
- process.stdout.write(`\n`);
1032
- const pidDisplay = processStatus?.pid
1033
- ? `${processStatus.processRunning ? UI.COLORS.green : UI.COLORS.red}${processStatus.pid}${UI.COLORS.reset}`
1034
- : '-';
1035
- process.stdout.write(` ${UI.COLORS.dim}PID${UI.COLORS.reset} ${pidDisplay}\n`);
1036
- process.stdout.write(` ${UI.COLORS.dim}Progress${UI.COLORS.reset} ${status.currentTask}/${status.totalTasks} tasks (${status.progress})\n`);
1037
- process.stdout.write(` ${UI.COLORS.dim}Duration${UI.COLORS.reset} ${this.formatDuration(processStatus?.duration || status.duration)}\n`);
1038
- process.stdout.write(` ${UI.COLORS.dim}Branch${UI.COLORS.reset} ${status.pipelineBranch}\n`);
1039
- if (status.waitingFor && status.waitingFor.length > 0) {
1040
- process.stdout.write(` ${UI.COLORS.yellow}Waiting${UI.COLORS.reset} ${status.waitingFor.join(', ')}\n`);
1041
- }
1042
- if (status.error) {
1043
- process.stdout.write(` ${UI.COLORS.red}Error${UI.COLORS.reset} ${status.error}\n`);
1044
- }
1045
- // Live terminal preview
1046
- this.renderSectionTitle('Live Terminal', 'last 10 lines');
1047
- const logPath = (0, path_1.safeJoin)(lane.path, 'terminal-readable.log');
1048
- if (fs.existsSync(logPath)) {
1049
- const content = fs.readFileSync(logPath, 'utf8');
1050
- const lines = content.split('\n').slice(-10);
1051
- for (const line of lines) {
1052
- const formatted = this.formatTerminalLine(line);
1053
- process.stdout.write(` ${UI.COLORS.dim}${formatted.substring(0, this.screenWidth - 4)}${UI.COLORS.reset}\n`);
1054
- }
1055
- }
1056
- else {
1057
- process.stdout.write(` ${UI.COLORS.dim}(No output yet)${UI.COLORS.reset}\n`);
1058
- }
1059
- // Conversation preview
1060
- this.renderSectionTitle('Conversation', `${this.currentLogs.length} messages`);
1061
- const maxVisible = 8;
1062
- if (this.selectedMessageIndex < this.scrollOffset) {
1063
- this.scrollOffset = this.selectedMessageIndex;
1064
- }
1065
- else if (this.selectedMessageIndex >= this.scrollOffset + maxVisible) {
1066
- this.scrollOffset = this.selectedMessageIndex - maxVisible + 1;
1067
- }
1068
- if (this.currentLogs.length === 0) {
1069
- process.stdout.write(` ${UI.COLORS.dim}(No messages yet)${UI.COLORS.reset}\n`);
1070
- }
1071
- else {
1072
- const visibleLogs = this.currentLogs.slice(this.scrollOffset, this.scrollOffset + maxVisible);
1073
- visibleLogs.forEach((log, i) => {
1074
- const actualIndex = i + this.scrollOffset;
1075
- const isSelected = actualIndex === this.selectedMessageIndex;
1076
- const roleColor = this.getRoleColor(log.role);
1077
- const role = log.role.toUpperCase().padEnd(10);
1078
- const ts = new Date(log.timestamp).toLocaleTimeString('en-US', { hour12: false });
1079
- const prefix = isSelected ? `${UI.COLORS.cyan}▶${UI.COLORS.reset}` : ' ';
1080
- const bg = isSelected ? UI.COLORS.bgGray : '';
1081
- const reset = isSelected ? UI.COLORS.reset : '';
1082
- const preview = log.fullText.replace(/\n/g, ' ').substring(0, 60);
1083
- process.stdout.write(`${bg}${prefix} ${roleColor}${role}${UI.COLORS.reset} ${UI.COLORS.dim}${ts}${UI.COLORS.reset} ${preview}...${reset}\n`);
1084
- });
1085
- if (this.currentLogs.length > maxVisible) {
1086
- process.stdout.write(` ${UI.COLORS.dim}(${this.currentLogs.length - maxVisible} more messages)${UI.COLORS.reset}\n`);
1087
- }
1065
+ // Header
1066
+ process.stdout.write(` ${dim} # Status │ Run ID │ Lanes │ Progress${reset}\n`);
1067
+ process.stdout.write(` ${dim}${''.repeat(80)}${reset}\n`);
1068
+ // List
1069
+ const visibleFlows = this.allFlows.slice(0, maxLines - 4);
1070
+ visibleFlows.forEach((flow, i) => {
1071
+ const isSelected = i === this.state.selectedFlowIndex;
1072
+ const isCurrent = flow.runDir === this.runDir;
1073
+ // Status
1074
+ let statusIcon = '⚪';
1075
+ if (flow.isAlive)
1076
+ statusIcon = '🟢';
1077
+ else if (flow.summary.completed === flow.summary.total && flow.summary.total > 0)
1078
+ statusIcon = '✅';
1079
+ else if (flow.summary.failed > 0)
1080
+ statusIcon = '🔴';
1081
+ // Lanes
1082
+ const lanesSummary = [
1083
+ flow.summary.running > 0 ? `${cyan}${flow.summary.running}R${reset}` : '',
1084
+ flow.summary.completed > 0 ? `${green}${flow.summary.completed}C${reset}` : '',
1085
+ flow.summary.failed > 0 ? `${red}${flow.summary.failed}F${reset}` : '',
1086
+ ].filter(Boolean).join('/') || '-';
1087
+ // Progress bar
1088
+ const total = flow.summary.total || 1;
1089
+ const ratio = flow.summary.completed / total;
1090
+ const barWidth = 10;
1091
+ const filled = Math.round(ratio * barWidth);
1092
+ const progressBar = `${green}${'█'.repeat(filled)}${reset}${dim}${'░'.repeat(barWidth - filled)}${reset}`;
1093
+ const pct = `${Math.round(ratio * 100)}%`;
1094
+ // Row
1095
+ const prefix = isSelected ? `${cyan}▶${reset}` : ' ';
1096
+ const bg = isSelected ? bgGray : '';
1097
+ const endBg = isSelected ? reset : '';
1098
+ const currentTag = isCurrent ? ` ${cyan}●${reset}` : '';
1099
+ const num = String(i + 1).padStart(2);
1100
+ const runIdDisplay = flow.runId.padEnd(32).substring(0, 32);
1101
+ process.stdout.write(`${bg} ${prefix} ${num} │ ${statusIcon} │ ${runIdDisplay} │ ${lanesSummary.padEnd(11 + 18)} │ ${progressBar} ${pct}${currentTag}${endBg}\n`);
1102
+ });
1103
+ if (this.allFlows.length > visibleFlows.length) {
1104
+ process.stdout.write(` ${dim} ... and ${this.allFlows.length - visibleFlows.length} more${reset}\n`);
1088
1105
  }
1089
- this.renderFooter([
1090
- '[↑↓] Scroll', '[→/Enter] Full Msg', '[T] Terminal', '[I] Intervene', '[K] Kill', '[←/Esc] Back'
1091
- ]);
1092
1106
  }
1093
- getRoleColor(role) {
1094
- const colors = {
1095
- user: UI.COLORS.yellow,
1096
- assistant: UI.COLORS.green,
1097
- intervention: UI.COLORS.red,
1098
- system: UI.COLORS.cyan,
1099
- };
1100
- return colors[role] || UI.COLORS.gray;
1101
- }
1102
- renderMessageDetail() {
1103
- const log = this.currentLogs[this.selectedMessageIndex];
1104
- if (!log) {
1105
- this.view = View.LANE_DETAIL;
1106
- this.render();
1107
+ renderUnifiedLog(maxLines) {
1108
+ const { cyan, reset, dim, gray, green, yellow } = UI.COLORS;
1109
+ // Status bar
1110
+ const filterLabel = this.state.laneFilter || 'All';
1111
+ const followLabel = this.state.unifiedLogFollowMode ? `${green}Follow ON${reset}` : `${yellow}Follow OFF${reset}`;
1112
+ const formatLabel = this.state.readableLogFormat ? `${green}Readable${reset}` : `${dim}Compact${reset}`;
1113
+ const totalEntries = this.unifiedLogBuffer?.getState().totalEntries || 0;
1114
+ process.stdout.write(` ${dim}Filter:${reset} ${cyan}${filterLabel}${reset} │ ${followLabel} │ ${yellow}[R]${reset} ${formatLabel} │ ${dim}Total: ${totalEntries}${reset}\n\n`);
1115
+ if (!this.unifiedLogBuffer) {
1116
+ process.stdout.write(` ${dim}No log buffer available${reset}\n`);
1107
1117
  return;
1108
1118
  }
1109
- this.renderHeader('Message Detail', [path.basename(this.runDir), this.selectedLaneName || '', log.role.toUpperCase()]);
1110
- const roleColor = this.getRoleColor(log.role);
1111
- const ts = new Date(log.timestamp).toLocaleString();
1112
- process.stdout.write(`\n`);
1113
- process.stdout.write(` ${UI.COLORS.dim}Role${UI.COLORS.reset} ${roleColor}${log.role.toUpperCase()}${UI.COLORS.reset}\n`);
1114
- process.stdout.write(` ${UI.COLORS.dim}Time${UI.COLORS.reset} ${ts}\n`);
1115
- if (log.model)
1116
- process.stdout.write(` ${UI.COLORS.dim}Model${UI.COLORS.reset} ${log.model}\n`);
1117
- if (log.task)
1118
- process.stdout.write(` ${UI.COLORS.dim}Task${UI.COLORS.reset} ${log.task}\n`);
1119
- this.renderSectionTitle('Content');
1120
- // Display message content with wrapping
1121
- const maxWidth = this.screenWidth - 4;
1122
- const lines = log.fullText.split('\n');
1123
- const maxLines = this.screenHeight - 16;
1124
- let lineCount = 0;
1125
- for (const line of lines) {
1126
- if (lineCount >= maxLines) {
1127
- process.stdout.write(` ${UI.COLORS.dim}... (truncated, ${lines.length - lineCount} more lines)${UI.COLORS.reset}\n`);
1128
- break;
1129
- }
1130
- // Word wrap long lines
1131
- if (line.length > maxWidth) {
1132
- const wrapped = this.wrapText(line, maxWidth);
1133
- for (const wl of wrapped) {
1134
- if (lineCount >= maxLines)
1135
- break;
1136
- process.stdout.write(` ${wl}\n`);
1137
- lineCount++;
1138
- }
1139
- }
1140
- else {
1141
- process.stdout.write(` ${line}\n`);
1142
- lineCount++;
1143
- }
1119
+ const entries = this.unifiedLogBuffer.getEntries({
1120
+ offset: this.state.unifiedLogScrollOffset,
1121
+ limit: maxLines - 2,
1122
+ filter: this.state.laneFilter ? { lane: this.state.laneFilter } : undefined,
1123
+ fromEnd: true,
1124
+ });
1125
+ if (entries.length === 0) {
1126
+ process.stdout.write(` ${dim}No log entries${reset}\n`);
1127
+ return;
1144
1128
  }
1145
- this.renderFooter(['[←/Esc] Back']);
1146
- }
1147
- /**
1148
- * Wrap text to specified width
1149
- */
1150
- wrapText(text, maxWidth) {
1151
- const words = text.split(' ');
1152
- const lines = [];
1153
- let currentLine = '';
1154
- for (const word of words) {
1155
- if (currentLine.length + word.length + 1 <= maxWidth) {
1156
- currentLine += (currentLine ? ' ' : '') + word;
1129
+ for (const entry of entries) {
1130
+ const ts = entry.timestamp.toLocaleTimeString('en-US', { hour12: false });
1131
+ const typeInfo = this.getLogTypeInfo(entry.type || 'info');
1132
+ if (this.state.readableLogFormat) {
1133
+ // Readable format: more context, wider lane name
1134
+ const lane = entry.laneName.substring(0, 12).padEnd(12);
1135
+ const preview = entry.message.replace(/\n/g, ' ').substring(0, this.screenWidth - 45);
1136
+ process.stdout.write(` ${dim}[${ts}]${reset} ${entry.laneColor}[${lane}]${reset} ${typeInfo.color}[${typeInfo.label}]${reset} ${preview}\n`);
1157
1137
  }
1158
1138
  else {
1159
- if (currentLine)
1160
- lines.push(currentLine);
1161
- currentLine = word;
1139
+ // Compact format: shorter, for quick scanning
1140
+ const lane = entry.laneName.substring(0, 8).padEnd(8);
1141
+ const typeShort = (entry.type || 'info').substring(0, 4).toUpperCase();
1142
+ const preview = entry.message.replace(/\n/g, ' ').substring(0, this.screenWidth - 35);
1143
+ process.stdout.write(` ${dim}${ts}${reset} ${entry.laneColor}${lane}${reset} ${typeInfo.color}${typeShort}${reset} ${preview}\n`);
1162
1144
  }
1163
1145
  }
1164
- if (currentLine)
1165
- lines.push(currentLine);
1166
- return lines;
1167
1146
  }
1168
- renderFlow() {
1169
- this.renderHeader('Dependency Flow', [path.basename(this.runDir), 'Flow']);
1170
- const laneMap = new Map();
1171
- this.lanes.forEach(lane => {
1172
- laneMap.set(lane.name, this.getLaneStatus(lane.path, lane.name));
1173
- });
1174
- process.stdout.write('\n');
1175
- // Group lanes by dependency level
1176
- const levels = this.calculateDependencyLevels();
1177
- const maxLevelWidth = Math.max(...levels.map(l => l.length));
1178
- for (let level = 0; level < levels.length; level++) {
1179
- const lanesAtLevel = levels[level];
1180
- // Level header
1181
- process.stdout.write(` ${UI.COLORS.dim}Level ${level}${UI.COLORS.reset}\n`);
1182
- for (const laneName of lanesAtLevel) {
1183
- const status = laneMap.get(laneName);
1184
- const statusIcon = this.getStatusIcon(status?.status || 'pending');
1185
- let statusColor = UI.COLORS.gray;
1186
- if (status?.status === 'completed')
1187
- statusColor = UI.COLORS.green;
1188
- else if (status?.status === 'running')
1189
- statusColor = UI.COLORS.cyan;
1190
- else if (status?.status === 'failed')
1191
- statusColor = UI.COLORS.red;
1192
- // Render the node
1193
- const nodeText = `${statusIcon} ${laneName}`;
1194
- process.stdout.write(` ${statusColor}${nodeText.padEnd(20)}${UI.COLORS.reset}`);
1195
- // Show task-level dependencies if waiting
1196
- if (status?.waitingFor?.length > 0) {
1197
- process.stdout.write(` ${UI.COLORS.dim}←${UI.COLORS.reset} ${UI.COLORS.yellow}${status.waitingFor.join(', ')}${UI.COLORS.reset}`);
1198
- }
1199
- process.stdout.write('\n');
1200
- }
1201
- if (level < levels.length - 1) {
1202
- process.stdout.write(` ${UI.COLORS.dim}│${UI.COLORS.reset}\n`);
1203
- process.stdout.write(` ${UI.COLORS.dim}▼${UI.COLORS.reset}\n`);
1204
- }
1205
- }
1206
- process.stdout.write(`\n ${UI.COLORS.dim}Tasks can wait for other tasks using task-level dependencies${UI.COLORS.reset}\n`);
1207
- this.renderFooter(['[←/Esc] Back']);
1208
- }
1209
- /**
1210
- * Calculate levels for visualization (all lanes run in parallel now)
1211
- */
1212
- calculateDependencyLevels() {
1213
- // Since lane-level dependencies are removed, all lanes can run in parallel
1214
- // Group them into a single level
1215
- if (this.lanes.length === 0) {
1216
- return [];
1217
- }
1218
- return [this.lanes.map(l => l.name)];
1219
- }
1220
- renderTerminal() {
1221
- const lane = this.lanes.find(l => l.name === this.selectedLaneName);
1147
+ renderDetail() {
1148
+ const w = this.screenWidth;
1149
+ const h = this.screenHeight;
1150
+ const { cyan, reset, bold, dim, gray, green, yellow, red } = UI.COLORS;
1151
+ const lane = this.lanes.find(l => l.name === this.state.selectedLaneName);
1222
1152
  if (!lane) {
1223
- this.view = View.LIST;
1153
+ this.state.level = Level.DASHBOARD;
1224
1154
  this.render();
1225
1155
  return;
1226
1156
  }
1227
- this.renderHeader('Live Terminal', [path.basename(this.runDir), lane.name, 'Terminal']);
1228
- // Get logs based on format mode
1229
- let logLines = [];
1230
- let totalLines = 0;
1231
- if (this.readableFormat) {
1232
- // Use JSONL for readable format
1233
- const jsonlPath = (0, path_1.safeJoin)(lane.path, 'terminal.jsonl');
1234
- logLines = this.getReadableLogLines(jsonlPath, lane.name);
1235
- totalLines = logLines.length;
1236
- }
1237
- else {
1238
- // Use readable log
1239
- const logPath = (0, path_1.safeJoin)(lane.path, 'terminal-readable.log');
1240
- if (fs.existsSync(logPath)) {
1241
- const content = fs.readFileSync(logPath, 'utf8');
1242
- logLines = content.split('\n');
1243
- totalLines = logLines.length;
1244
- }
1245
- }
1246
- const maxVisible = this.screenHeight - 10;
1247
- // Follow mode logic
1248
- if (this.followMode) {
1249
- this.terminalScrollOffset = 0;
1157
+ const status = this.getLaneStatus(lane.path, lane.name);
1158
+ const processStatus = this.laneProcessStatuses.get(lane.name);
1159
+ // Header
1160
+ const hLine = UI.CHARS.hLine.repeat(w);
1161
+ const statusColor = status.status === 'running' ? cyan : status.status === 'completed' ? green : status.status === 'failed' ? red : gray;
1162
+ const statusIcon = this.getStatusIcon(status.status);
1163
+ process.stdout.write(`${cyan}${hLine}${reset}\n`);
1164
+ process.stdout.write(` ${dim}←${reset} back │ ${bold}🔧 ${lane.name}${reset} │ ${statusColor}${statusIcon} ${status.status.toUpperCase()}${reset} │ ${status.currentTask}/${status.totalTasks} tasks │ ${this.formatDuration(processStatus?.duration || status.duration)}\n`);
1165
+ process.stdout.write(`${cyan}${hLine}${reset}\n`);
1166
+ // Split panel
1167
+ const panelWidth = Math.floor((w - 3) / 2);
1168
+ const contentHeight = h - 8;
1169
+ // Panel headers
1170
+ const leftActive = this.state.currentPanel === Panel.LEFT;
1171
+ const rightActive = this.state.currentPanel === Panel.RIGHT;
1172
+ const leftHeader = leftActive ? `${cyan}${UI.ICONS.arrow} Terminal Log${reset}` : `${dim} Terminal Log${reset}`;
1173
+ const rightHeader = rightActive ? `${cyan}${UI.ICONS.arrow} Conversation${reset}` : `${dim} Conversation${reset}`;
1174
+ process.stdout.write(`${leftHeader.padEnd(panelWidth + 10)} │ ${rightHeader}\n`);
1175
+ process.stdout.write(`${UI.CHARS.hLineLight.repeat(panelWidth)} ${UI.CHARS.tee.top} ${UI.CHARS.hLineLight.repeat(panelWidth)}\n`);
1176
+ // Get content
1177
+ const terminalLines = this.getTerminalLines(lane.path, contentHeight);
1178
+ const messageLines = this.getMessageLines(contentHeight);
1179
+ // Render side by side
1180
+ for (let i = 0; i < contentHeight; i++) {
1181
+ const termLine = (terminalLines[i] || '').substring(0, panelWidth - 2);
1182
+ const msgLine = (messageLines[i] || '').substring(0, panelWidth - 2);
1183
+ const termPadded = this.padWithAnsi(termLine, panelWidth - 1);
1184
+ const msgPadded = this.padWithAnsi(msgLine, panelWidth - 1);
1185
+ process.stdout.write(`${termPadded} ${dim}│${reset} ${msgPadded}\n`);
1186
+ }
1187
+ // Notification
1188
+ if (this.state.notification) {
1189
+ const nColor = this.state.notification.type === 'error' ? red
1190
+ : this.state.notification.type === 'success' ? green : cyan;
1191
+ process.stdout.write(`${nColor} 🔔 ${this.state.notification.message}${reset}\n`);
1250
1192
  }
1251
1193
  else {
1252
- if (this.lastTerminalTotalLines > 0 && totalLines > this.lastTerminalTotalLines) {
1253
- this.unseenLineCount += (totalLines - this.lastTerminalTotalLines);
1254
- this.terminalScrollOffset += (totalLines - this.lastTerminalTotalLines);
1255
- }
1194
+ process.stdout.write('\n');
1256
1195
  }
1257
- this.lastTerminalTotalLines = totalLines;
1258
- // Clamp scroll offset
1259
- const maxScroll = Math.max(0, totalLines - maxVisible);
1260
- if (this.terminalScrollOffset > maxScroll) {
1261
- this.terminalScrollOffset = maxScroll;
1262
- }
1263
- // Mode and status indicators
1264
- const formatMode = this.readableFormat
1265
- ? `${UI.COLORS.green}[R] Readable ✓${UI.COLORS.reset}`
1266
- : `${UI.COLORS.dim}[R] Raw${UI.COLORS.reset}`;
1267
- const followStatus = this.followMode
1268
- ? `${UI.COLORS.green}[F] Follow ✓${UI.COLORS.reset}`
1269
- : `${UI.COLORS.yellow}[F] Follow OFF${this.unseenLineCount > 0 ? ` (↓${this.unseenLineCount})` : ''}${UI.COLORS.reset}`;
1270
- process.stdout.write(` ${formatMode} ${followStatus} ${UI.COLORS.dim}Lines: ${totalLines}${UI.COLORS.reset}\n\n`);
1271
- // Slice based on scroll (0 means bottom, >0 means scrolled up)
1272
- const end = totalLines - this.terminalScrollOffset;
1273
- const start = Math.max(0, end - maxVisible);
1274
- const visibleLines = logLines.slice(start, end);
1275
- for (const line of visibleLines) {
1276
- const formatted = this.readableFormat ? line : this.formatTerminalLine(line);
1277
- // Truncate to screen width
1278
- const displayLine = formatted.length > this.screenWidth - 2
1279
- ? formatted.substring(0, this.screenWidth - 5) + '...'
1280
- : formatted;
1281
- process.stdout.write(` ${displayLine}\n`);
1282
- }
1283
- if (visibleLines.length === 0) {
1284
- process.stdout.write(` ${UI.COLORS.dim}(No output yet)${UI.COLORS.reset}\n`);
1285
- }
1286
- this.renderFooter([
1287
- '[↑↓] Scroll', '[F] Follow', '[R] Toggle Readable', '[I] Intervene', '[←/Esc] Back'
1288
- ]);
1196
+ // Footer
1197
+ process.stdout.write(`${cyan}${hLine}${reset}\n`);
1198
+ const followStatus = this.state.followMode ? `${green}ON${reset}` : `${yellow}OFF${reset}`;
1199
+ const formatStatus = this.state.readableFormat ? `${green}Readable${reset}` : `${dim}Raw${reset}`;
1200
+ process.stdout.write(` ${yellow}[←]${reset} Back ${yellow}[→]${reset} Panel ${yellow}[↑/↓]${reset} Scroll ${yellow}[Space]${reset} Follow:${followStatus} ${yellow}[R]${reset} ${formatStatus} ${yellow}[Enter]${reset} Action ${yellow}[?]${reset} Help\n`);
1289
1201
  }
1290
- /**
1291
- * Format a raw terminal line with syntax highlighting
1292
- */
1293
- formatTerminalLine(line) {
1294
- // Highlight patterns
1295
- if (line.includes('[HUMAN INTERVENTION]') || line.includes('Injecting intervention:')) {
1296
- return `${UI.COLORS.yellow}${UI.COLORS.bold}${line}${UI.COLORS.reset}`;
1297
- }
1298
- if (line.includes('Executing cursor-agent')) {
1299
- return `${UI.COLORS.cyan}${UI.COLORS.bold}${line}${UI.COLORS.reset}`;
1202
+ getTerminalLines(lanePath, maxLines) {
1203
+ const { dim, reset, cyan, green, yellow, red, gray } = UI.COLORS;
1204
+ // Choose log source based on format setting
1205
+ if (this.state.readableFormat) {
1206
+ // Try JSONL first for structured readable format
1207
+ const jsonlPath = (0, path_1.safeJoin)(lanePath, 'terminal.jsonl');
1208
+ if (fs.existsSync(jsonlPath)) {
1209
+ return this.getJsonlLogLines(jsonlPath, maxLines);
1210
+ }
1300
1211
  }
1301
- if (line.includes('=== Task:') || line.includes('Starting task:')) {
1302
- return `${UI.COLORS.green}${UI.COLORS.bold}${line}${UI.COLORS.reset}`;
1212
+ // Fallback to raw terminal log
1213
+ const logPath = (0, path_1.safeJoin)(lanePath, 'terminal-readable.log');
1214
+ if (!fs.existsSync(logPath)) {
1215
+ return [`${dim}(No output yet)${reset}`];
1303
1216
  }
1304
- if (line.toLowerCase().includes('error') || line.toLowerCase().includes('failed')) {
1305
- return `${UI.COLORS.red}${line}${UI.COLORS.reset}`;
1217
+ try {
1218
+ const content = fs.readFileSync(logPath, 'utf8');
1219
+ const allLines = content.split('\n');
1220
+ const totalLines = allLines.length;
1221
+ // Calculate visible range (from end, accounting for scroll offset)
1222
+ const end = Math.max(0, totalLines - this.state.terminalScrollOffset);
1223
+ const start = Math.max(0, end - maxLines);
1224
+ const visibleLines = allLines.slice(start, end);
1225
+ // Format lines with syntax highlighting
1226
+ return visibleLines.map(line => {
1227
+ if (line.includes('[HUMAN INTERVENTION]') || line.includes('Injecting intervention:')) {
1228
+ return `${yellow}${line}${reset}`;
1229
+ }
1230
+ if (line.includes('=== Task:') || line.includes('Starting task:')) {
1231
+ return `${green}${line}${reset}`;
1232
+ }
1233
+ if (line.includes('Executing cursor-agent') || line.includes('cursor-agent-v')) {
1234
+ return `${cyan}${line}${reset}`;
1235
+ }
1236
+ if (line.toLowerCase().includes('error') || line.toLowerCase().includes('failed')) {
1237
+ return `${red}${line}${reset}`;
1238
+ }
1239
+ if (line.toLowerCase().includes('success') || line.toLowerCase().includes('completed')) {
1240
+ return `${green}${line}${reset}`;
1241
+ }
1242
+ return line;
1243
+ });
1306
1244
  }
1307
- if (line.toLowerCase().includes('success') || line.toLowerCase().includes('completed')) {
1308
- return `${UI.COLORS.green}${line}${UI.COLORS.reset}`;
1245
+ catch {
1246
+ return [`${dim}(Error reading log)${reset}`];
1309
1247
  }
1310
- return line;
1311
1248
  }
1312
1249
  /**
1313
- * Get readable log lines from JSONL file
1250
+ * Get structured log lines from JSONL file
1314
1251
  */
1315
- getReadableLogLines(jsonlPath, laneName) {
1316
- if (!fs.existsSync(jsonlPath)) {
1317
- // Fallback: try to read raw log
1318
- const rawPath = jsonlPath.replace('.jsonl', '.log');
1319
- if (fs.existsSync(rawPath)) {
1320
- return fs.readFileSync(rawPath, 'utf8').split('\n').map(l => this.formatTerminalLine(l));
1321
- }
1322
- return [];
1323
- }
1252
+ getJsonlLogLines(jsonlPath, maxLines) {
1253
+ const { dim, reset, cyan, green, yellow, red, gray } = UI.COLORS;
1324
1254
  try {
1325
1255
  const content = fs.readFileSync(jsonlPath, 'utf8');
1326
- const lines = content.split('\n').filter(l => l.trim());
1327
- return lines.map(line => {
1256
+ const allLines = content.split('\n').filter(l => l.trim());
1257
+ const totalLines = allLines.length;
1258
+ // Calculate visible range
1259
+ const end = Math.max(0, totalLines - this.state.terminalScrollOffset);
1260
+ const start = Math.max(0, end - maxLines);
1261
+ const visibleLines = allLines.slice(start, end);
1262
+ return visibleLines.map(line => {
1328
1263
  try {
1329
1264
  const entry = JSON.parse(line);
1330
1265
  const ts = new Date(entry.timestamp || Date.now()).toLocaleTimeString('en-US', { hour12: false });
1331
1266
  const type = (entry.type || 'info').toLowerCase();
1332
- const content = entry.content || entry.message || '';
1333
- // Format based on type
1267
+ const content = (entry.content || entry.message || '').replace(/\n/g, ' ');
1334
1268
  const typeInfo = this.getLogTypeInfo(type);
1335
- const preview = content.replace(/\n/g, ' ').substring(0, 100);
1336
- return `${UI.COLORS.dim}[${ts}]${UI.COLORS.reset} ${typeInfo.color}[${typeInfo.label}]${UI.COLORS.reset} ${preview}`;
1269
+ return `${gray}[${ts}]${reset} ${typeInfo.color}[${typeInfo.label}]${reset} ${content}`;
1337
1270
  }
1338
1271
  catch {
1339
- return this.formatTerminalLine(line);
1272
+ return `${gray}${line}${reset}`;
1340
1273
  }
1341
1274
  });
1342
1275
  }
1343
1276
  catch {
1344
- return [];
1345
- }
1346
- }
1347
- /**
1348
- * Get log type display info
1349
- */
1350
- getLogTypeInfo(type) {
1351
- const typeMap = {
1352
- user: { label: 'USER ', color: UI.COLORS.cyan },
1353
- assistant: { label: 'ASST ', color: UI.COLORS.green },
1354
- tool: { label: 'TOOL ', color: UI.COLORS.yellow },
1355
- tool_result: { label: 'RESULT', color: UI.COLORS.gray },
1356
- result: { label: 'DONE ', color: UI.COLORS.green },
1357
- system: { label: 'SYSTEM', color: UI.COLORS.gray },
1358
- thinking: { label: 'THINK ', color: UI.COLORS.dim },
1359
- error: { label: 'ERROR ', color: UI.COLORS.red },
1360
- stderr: { label: 'STDERR', color: UI.COLORS.red },
1361
- stdout: { label: 'STDOUT', color: UI.COLORS.white },
1362
- };
1363
- return typeMap[type] || { label: type.toUpperCase().padEnd(6).substring(0, 6), color: UI.COLORS.gray };
1364
- }
1365
- renderIntervene() {
1366
- this.renderHeader('Human Intervention', [path.basename(this.runDir), this.selectedLaneName || '', 'Intervene']);
1367
- process.stdout.write(`\n`);
1368
- process.stdout.write(` ${UI.COLORS.yellow}Send a message directly to the agent.${UI.COLORS.reset}\n`);
1369
- process.stdout.write(` ${UI.COLORS.dim}This will interrupt the current flow and inject your instruction.${UI.COLORS.reset}\n\n`);
1370
- // Input box
1371
- const width = Math.min(this.screenWidth - 8, 80);
1372
- process.stdout.write(` ${UI.COLORS.cyan}┌${'─'.repeat(width)}┐${UI.COLORS.reset}\n`);
1373
- // Wrap input text
1374
- const inputLines = this.wrapText(this.interventionInput || ' ', width - 4);
1375
- for (const line of inputLines) {
1376
- process.stdout.write(` ${UI.COLORS.cyan}│${UI.COLORS.reset} ${line.padEnd(width - 2)} ${UI.COLORS.cyan}│${UI.COLORS.reset}\n`);
1377
- }
1378
- if (inputLines.length === 0 || inputLines[inputLines.length - 1] === ' ') {
1379
- 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`);
1380
- }
1381
- process.stdout.write(` ${UI.COLORS.cyan}└${'─'.repeat(width)}┘${UI.COLORS.reset}\n`);
1382
- this.renderFooter(['[Enter] Send', '[Esc] Cancel']);
1383
- }
1384
- renderTimeout() {
1385
- this.renderHeader('Update Timeout', [path.basename(this.runDir), this.selectedLaneName || '', 'Timeout']);
1386
- process.stdout.write(`\n`);
1387
- process.stdout.write(` ${UI.COLORS.yellow}Update the task timeout for this lane.${UI.COLORS.reset}\n`);
1388
- process.stdout.write(` ${UI.COLORS.dim}Enter timeout in milliseconds (e.g., 600000 = 10 minutes)${UI.COLORS.reset}\n\n`);
1389
- // Common presets
1390
- process.stdout.write(` ${UI.COLORS.dim}Presets: 300000 (5m) | 600000 (10m) | 1800000 (30m) | 3600000 (1h)${UI.COLORS.reset}\n\n`);
1391
- // Input box
1392
- const width = 40;
1393
- process.stdout.write(` ${UI.COLORS.cyan}┌${'─'.repeat(width)}┐${UI.COLORS.reset}\n`);
1394
- 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`);
1395
- process.stdout.write(` ${UI.COLORS.cyan}└${'─'.repeat(width)}┘${UI.COLORS.reset}\n`);
1396
- // Show human-readable interpretation
1397
- if (this.timeoutInput) {
1398
- const ms = parseInt(this.timeoutInput);
1399
- if (!isNaN(ms) && ms > 0) {
1400
- const formatted = this.formatDuration(ms);
1401
- process.stdout.write(`\n ${UI.COLORS.green}= ${formatted}${UI.COLORS.reset}\n`);
1402
- }
1277
+ return [`${dim}(Error reading log)${reset}`];
1403
1278
  }
1404
- this.renderFooter(['[Enter] Apply', '[Esc] Cancel']);
1405
1279
  }
1406
- /**
1407
- * Render unified log view - all lanes combined
1408
- */
1409
- renderUnifiedLog() {
1410
- this.renderHeader('Unified Logs', [path.basename(this.runDir), 'All Lanes']);
1411
- const bufferState = this.unifiedLogBuffer?.getState();
1412
- const totalEntries = bufferState?.totalEntries || 0;
1413
- const availableLanes = bufferState?.lanes || [];
1414
- // Status bar
1415
- const formatMode = this.readableFormat
1416
- ? `${UI.COLORS.green}[R] Readable ✓${UI.COLORS.reset}`
1417
- : `${UI.COLORS.dim}[R] Compact${UI.COLORS.reset}`;
1418
- const followStatus = this.unifiedLogFollowMode
1419
- ? `${UI.COLORS.green}[F] Follow ✓${UI.COLORS.reset}`
1420
- : `${UI.COLORS.yellow}[F] Follow OFF${UI.COLORS.reset}`;
1421
- const filterStatus = this.laneFilter
1422
- ? `${UI.COLORS.cyan}[L] ${this.laneFilter}${UI.COLORS.reset}`
1423
- : `${UI.COLORS.dim}[L] All Lanes${UI.COLORS.reset}`;
1424
- process.stdout.write(` ${formatMode} ${followStatus} ${filterStatus} ${UI.COLORS.dim}Total: ${totalEntries}${UI.COLORS.reset}\n`);
1425
- // Lane list for filtering hint
1426
- if (availableLanes.length > 1) {
1427
- process.stdout.write(` ${UI.COLORS.dim}Lanes: ${availableLanes.join(', ')}${UI.COLORS.reset}\n`);
1428
- }
1429
- process.stdout.write('\n');
1430
- if (!this.unifiedLogBuffer) {
1431
- process.stdout.write(` ${UI.COLORS.dim}(No log buffer available)${UI.COLORS.reset}\n`);
1432
- this.renderFooter(['[U/Esc] Back', '[Q] Quit']);
1433
- return;
1434
- }
1435
- const pageSize = this.screenHeight - 12;
1436
- const filter = this.laneFilter ? { lane: this.laneFilter } : undefined;
1437
- const entries = this.unifiedLogBuffer.getEntries({
1438
- offset: this.unifiedLogScrollOffset,
1439
- limit: pageSize,
1440
- filter,
1441
- fromEnd: true,
1442
- });
1443
- if (entries.length === 0) {
1444
- process.stdout.write(` ${UI.COLORS.dim}(No log entries yet)${UI.COLORS.reset}\n`);
1445
- }
1446
- else {
1447
- for (const entry of entries) {
1448
- const formatted = this.formatUnifiedLogEntry(entry);
1449
- const displayLine = formatted.length > this.screenWidth - 2
1450
- ? formatted.substring(0, this.screenWidth - 5) + '...'
1451
- : formatted;
1452
- process.stdout.write(` ${displayLine}\n`);
1453
- }
1454
- }
1455
- this.renderFooter([
1456
- '[↑↓/PgUp/PgDn] Scroll', '[F] Follow', '[R] Readable', '[L] Filter Lane', '[U/Esc] Back'
1457
- ]);
1458
- }
1459
- /**
1460
- * Format a unified log entry
1461
- */
1462
- formatUnifiedLogEntry(entry) {
1463
- const ts = entry.timestamp.toLocaleTimeString('en-US', { hour12: false });
1464
- const lane = entry.laneName.padEnd(12);
1465
- const typeInfo = this.getLogTypeInfo(entry.type || 'info');
1466
- if (this.readableFormat) {
1467
- // Readable format: show more context
1468
- const content = entry.message.replace(/\n/g, ' ');
1469
- return `${UI.COLORS.dim}[${ts}]${UI.COLORS.reset} ${entry.laneColor}[${lane}]${UI.COLORS.reset} ${typeInfo.color}[${typeInfo.label}]${UI.COLORS.reset} ${content}`;
1470
- }
1471
- else {
1472
- // Compact format
1473
- const preview = entry.message.replace(/\n/g, ' ').substring(0, 60);
1474
- 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}`;
1280
+ getMessageLines(maxLines) {
1281
+ const { dim, reset, cyan, green, yellow, gray } = UI.COLORS;
1282
+ if (this.currentLogs.length === 0) {
1283
+ return [`${dim}(No messages yet)${reset}`];
1475
1284
  }
1476
- }
1477
- /**
1478
- * Render multiple flows dashboard
1479
- */
1480
- renderFlowsDashboard() {
1481
- this.renderHeader('All Flows', ['Flows Dashboard']);
1482
- process.stdout.write(` ${UI.COLORS.dim}Total: ${this.allFlows.length} flows${UI.COLORS.reset}\n\n`);
1483
- if (this.allFlows.length === 0) {
1484
- process.stdout.write(` ${UI.COLORS.dim}No flow runs found.${UI.COLORS.reset}\n\n`);
1485
- process.stdout.write(` Run ${UI.COLORS.cyan}cursorflow run${UI.COLORS.reset} to start a new flow.\n`);
1486
- this.renderFooter(['[M/Esc] Back', '[Q] Quit']);
1487
- return;
1285
+ const lines = [];
1286
+ const start = this.state.messageScrollOffset;
1287
+ const visibleLogs = this.currentLogs.slice(start, start + Math.floor(maxLines / 3));
1288
+ for (const log of visibleLogs) {
1289
+ const roleColor = log.role === 'user' ? yellow : log.role === 'assistant' ? green : cyan;
1290
+ const ts = new Date(log.timestamp).toLocaleTimeString('en-US', { hour12: false });
1291
+ lines.push(`${roleColor}[${ts}] ${log.role.toUpperCase()}${reset}`);
1292
+ const preview = log.fullText.replace(/\n/g, ' ').substring(0, 60);
1293
+ lines.push(`${dim}${preview}...${reset}`);
1294
+ lines.push('');
1295
+ }
1296
+ if (this.currentLogs.length > visibleLogs.length + start) {
1297
+ lines.push(`${dim}... ${this.currentLogs.length - visibleLogs.length - start} more${reset}`);
1488
1298
  }
1489
- // Header
1490
- process.stdout.write(` ${'Status'.padEnd(8)} ${'Run ID'.padEnd(32)} ${'Lanes'.padEnd(12)} Progress\n`);
1491
- process.stdout.write(` ${'─'.repeat(8)} ${'─'.repeat(32)} ${'─'.repeat(12)} ${'─'.repeat(20)}\n`);
1492
- const maxVisible = this.screenHeight - 14;
1493
- const startIdx = Math.max(0, this.selectedFlowIndex - Math.floor(maxVisible / 2));
1494
- const endIdx = Math.min(this.allFlows.length, startIdx + maxVisible);
1495
- for (let i = startIdx; i < endIdx; i++) {
1496
- const flow = this.allFlows[i];
1497
- const isSelected = i === this.selectedFlowIndex;
1498
- const isCurrent = flow.runDir === this.runDir;
1499
- // Status icon based on flow state
1500
- let statusIcon = '⚪';
1501
- if (flow.isAlive) {
1502
- statusIcon = '🟢';
1503
- }
1504
- else if (flow.summary.completed === flow.summary.total && flow.summary.total > 0) {
1505
- statusIcon = '✅';
1506
- }
1507
- else if (flow.summary.failed > 0 || flow.summary.dead > 0) {
1508
- statusIcon = '🔴';
1509
- }
1510
- // Lanes summary
1511
- const lanesSummary = [
1512
- flow.summary.running > 0 ? `${UI.COLORS.cyan}${flow.summary.running}R${UI.COLORS.reset}` : '',
1513
- flow.summary.completed > 0 ? `${UI.COLORS.green}${flow.summary.completed}C${UI.COLORS.reset}` : '',
1514
- flow.summary.failed > 0 ? `${UI.COLORS.red}${flow.summary.failed}F${UI.COLORS.reset}` : '',
1515
- flow.summary.dead > 0 ? `${UI.COLORS.yellow}${flow.summary.dead}D${UI.COLORS.reset}` : '',
1516
- ].filter(Boolean).join('/') || '0';
1517
- // Progress bar
1518
- const total = flow.summary.total || 1;
1519
- const completed = flow.summary.completed;
1520
- const ratio = completed / total;
1521
- const barWidth = 12;
1522
- const filled = Math.round(ratio * barWidth);
1523
- const progressBar = `${UI.COLORS.green}${'█'.repeat(filled)}${UI.COLORS.reset}${UI.COLORS.gray}${'░'.repeat(barWidth - filled)}${UI.COLORS.reset}`;
1524
- const pct = `${Math.round(ratio * 100)}%`;
1525
- // Display
1526
- const prefix = isSelected ? ` ${UI.COLORS.cyan}▶${UI.COLORS.reset} ` : ' ';
1527
- const currentTag = isCurrent ? ` ${UI.COLORS.cyan}●${UI.COLORS.reset}` : '';
1528
- const bg = isSelected ? UI.COLORS.bgGray : '';
1529
- const resetBg = isSelected ? UI.COLORS.reset : '';
1530
- // Truncate run ID if needed
1531
- const runIdDisplay = flow.runId.length > 30 ? flow.runId.substring(0, 27) + '...' : flow.runId.padEnd(30);
1532
- process.stdout.write(`${bg}${prefix}${statusIcon} ${runIdDisplay} ${lanesSummary.padEnd(12 + 30)} ${progressBar} ${pct}${currentTag}${resetBg}\n`);
1533
- }
1534
- if (this.allFlows.length > maxVisible) {
1535
- process.stdout.write(`\n ${UI.COLORS.dim}(${this.allFlows.length - maxVisible} more flows, scroll to see)${UI.COLORS.reset}\n`);
1536
- }
1537
- this.renderFooter([
1538
- '[↑↓] Select', '[→/Enter] Switch', '[D] Delete', '[R] Refresh', '[M/Esc] Back', '[Q] Quit'
1539
- ]);
1299
+ return lines;
1540
1300
  }
1541
- listLanesWithDeps(runDir) {
1542
- const lanesDir = (0, path_1.safeJoin)(runDir, 'lanes');
1543
- if (!fs.existsSync(lanesDir))
1544
- return [];
1545
- const config = (0, config_1.loadConfig)();
1546
- const tasksDir = (0, path_1.safeJoin)(config.projectRoot, config.tasksDir);
1547
- const laneConfigs = this.listLaneFilesFromDir(tasksDir);
1548
- return fs.readdirSync(lanesDir)
1549
- .filter(d => fs.statSync((0, path_1.safeJoin)(lanesDir, d)).isDirectory())
1550
- .map(name => {
1551
- return {
1552
- name,
1553
- path: (0, path_1.safeJoin)(lanesDir, name),
1554
- };
1301
+ renderActionMenu() {
1302
+ const { cyan, reset, bold, dim, gray, bgGray, yellow, red } = UI.COLORS;
1303
+ const menuWidth = 36;
1304
+ const menuHeight = this.state.actionItems.length + 4;
1305
+ const startX = Math.floor((this.screenWidth - menuWidth) / 2);
1306
+ const startY = Math.floor((this.screenHeight - menuHeight) / 2);
1307
+ // Move cursor and draw menu
1308
+ const targetName = this.state.selectedLaneName || 'Item';
1309
+ // Top border
1310
+ process.stdout.write(`\x1b[${startY};${startX}H`);
1311
+ process.stdout.write(`${cyan}┌${'─'.repeat(menuWidth - 2)}┐${reset}`);
1312
+ // Title
1313
+ process.stdout.write(`\x1b[${startY + 1};${startX}H`);
1314
+ const title = ` 📋 Actions: ${targetName}`.substring(0, menuWidth - 4);
1315
+ process.stdout.write(`${cyan}│${reset}${bold}${title.padEnd(menuWidth - 2)}${reset}${cyan}│${reset}`);
1316
+ // Separator
1317
+ process.stdout.write(`\x1b[${startY + 2};${startX}H`);
1318
+ process.stdout.write(`${cyan}├${'─'.repeat(menuWidth - 2)}┤${reset}`);
1319
+ // Items
1320
+ this.state.actionItems.forEach((item, i) => {
1321
+ process.stdout.write(`\x1b[${startY + 3 + i};${startX}H`);
1322
+ const isSelected = i === this.state.selectedActionIndex;
1323
+ const prefix = isSelected ? `${cyan}▶${reset}` : ' ';
1324
+ const num = `${i + 1}.`;
1325
+ const bg = isSelected ? bgGray : '';
1326
+ const endBg = isSelected ? reset : '';
1327
+ const itemColor = item.disabled ? dim : reset;
1328
+ const label = `${item.icon} ${item.label}`;
1329
+ process.stdout.write(`${cyan}│${reset}${bg} ${prefix} ${num} ${itemColor}${label.padEnd(menuWidth - 9)}${reset}${endBg}${cyan}│${reset}`);
1555
1330
  });
1331
+ // Bottom border
1332
+ process.stdout.write(`\x1b[${startY + 3 + this.state.actionItems.length};${startX}H`);
1333
+ process.stdout.write(`${cyan}├${'─'.repeat(menuWidth - 2)}┤${reset}`);
1334
+ // Help
1335
+ process.stdout.write(`\x1b[${startY + 4 + this.state.actionItems.length};${startX}H`);
1336
+ process.stdout.write(`${cyan}│${reset}${dim} [↑/↓] Select [Enter] OK [Esc] Cancel${reset.padEnd(menuWidth - 41)}${cyan}│${reset}`);
1337
+ process.stdout.write(`\x1b[${startY + 5 + this.state.actionItems.length};${startX}H`);
1338
+ process.stdout.write(`${cyan}└${'─'.repeat(menuWidth - 2)}┘${reset}`);
1556
1339
  }
1557
- listLaneFilesFromDir(tasksDir) {
1558
- if (!fs.existsSync(tasksDir))
1559
- return [];
1560
- return fs.readdirSync(tasksDir)
1561
- .filter(f => f.endsWith('.json'))
1562
- .map(f => {
1563
- const filePath = (0, path_1.safeJoin)(tasksDir, f);
1564
- try {
1565
- return { name: path.basename(f, '.json') };
1566
- }
1567
- catch {
1568
- return { name: path.basename(f, '.json') };
1569
- }
1570
- });
1571
- }
1572
- getLaneStatus(lanePath, _laneName) {
1573
- const statePath = (0, path_1.safeJoin)(lanePath, 'state.json');
1574
- const state = (0, state_1.loadState)(statePath);
1575
- if (!state) {
1576
- return { status: 'pending', currentTask: 0, totalTasks: '?', progress: '0%', duration: 0, pipelineBranch: '-', chatId: '-' };
1577
- }
1578
- const progress = state.totalTasks > 0 ? Math.round((state.currentTaskIndex / state.totalTasks) * 100) : 0;
1579
- const duration = state.startTime ? (state.endTime
1580
- ? state.endTime - state.startTime
1581
- : (state.status === 'running' || state.status === 'reviewing' ? Date.now() - state.startTime : 0)) : 0;
1582
- return {
1583
- status: state.status || 'unknown',
1584
- currentTask: state.currentTaskIndex || 0,
1585
- totalTasks: state.totalTasks || '?',
1586
- progress: `${progress}%`,
1587
- pipelineBranch: state.pipelineBranch || '-',
1588
- chatId: state.chatId || '-',
1589
- duration,
1590
- error: state.error,
1591
- pid: state.pid,
1592
- waitingFor: state.waitingFor || [],
1593
- };
1340
+ renderInputOverlay() {
1341
+ const { cyan, reset, bold, dim, yellow } = UI.COLORS;
1342
+ const boxWidth = Math.min(70, this.screenWidth - 10);
1343
+ const startX = Math.floor((this.screenWidth - boxWidth) / 2);
1344
+ const startY = this.screenHeight - 6;
1345
+ const title = this.state.inputMode === 'message'
1346
+ ? `💬 Message to ${this.state.inputTarget}:`
1347
+ : `⏱️ Timeout (ms) for ${this.state.inputTarget}:`;
1348
+ const hint = this.state.inputMode === 'timeout'
1349
+ ? 'Presets: 300000 (5m) | 600000 (10m) | 1800000 (30m)'
1350
+ : 'Type your message and press Enter';
1351
+ // Background box
1352
+ process.stdout.write(`\x1b[${startY};${startX}H`);
1353
+ process.stdout.write(`${cyan}┌${'─'.repeat(boxWidth - 2)}┐${reset}`);
1354
+ process.stdout.write(`\x1b[${startY + 1};${startX}H`);
1355
+ process.stdout.write(`${cyan}│${reset} ${bold}${title.padEnd(boxWidth - 4)}${reset} ${cyan}│${reset}`);
1356
+ process.stdout.write(`\x1b[${startY + 2};${startX}H`);
1357
+ process.stdout.write(`${cyan}│${reset} ${dim}${hint.padEnd(boxWidth - 4)}${reset} ${cyan}│${reset}`);
1358
+ process.stdout.write(`\x1b[${startY + 3};${startX}H`);
1359
+ const inputDisplay = this.state.inputBuffer.substring(0, boxWidth - 6) + '';
1360
+ process.stdout.write(`${cyan}│${reset} ${yellow}${inputDisplay.padEnd(boxWidth - 4)}${reset} ${cyan}│${reset}`);
1361
+ process.stdout.write(`\x1b[${startY + 4};${startX}H`);
1362
+ process.stdout.write(`${cyan}│${reset}${dim} [Enter] Submit [Esc] Cancel${reset.padEnd(boxWidth - 32)} ${cyan}│${reset}`);
1363
+ process.stdout.write(`\x1b[${startY + 5};${startX}H`);
1364
+ process.stdout.write(`${cyan}└${''.repeat(boxWidth - 2)}┘${reset}`);
1594
1365
  }
1366
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1367
+ // Utility Methods
1368
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1595
1369
  formatDuration(ms) {
1596
1370
  if (ms <= 0)
1597
1371
  return '-';
@@ -1612,14 +1386,107 @@ class InteractiveMonitor {
1612
1386
  'failed': '❌',
1613
1387
  'blocked_dependency': '🚫',
1614
1388
  'pending': '⚪',
1615
- 'reviewing': '👀',
1616
1389
  };
1617
1390
  return icons[status] || '❓';
1618
1391
  }
1392
+ getLogTypeInfo(type) {
1393
+ const { cyan, green, yellow, gray, red, white, dim, magenta, reset } = UI.COLORS;
1394
+ const typeMap = {
1395
+ user: { label: 'USER ', color: cyan },
1396
+ assistant: { label: 'ASST ', color: green },
1397
+ tool: { label: 'TOOL ', color: yellow },
1398
+ tool_result: { label: 'RESULT', color: gray },
1399
+ result: { label: 'DONE ', color: green },
1400
+ system: { label: 'SYSTEM', color: gray },
1401
+ thinking: { label: 'THINK ', color: dim },
1402
+ error: { label: 'ERROR ', color: red },
1403
+ stderr: { label: 'STDERR', color: red },
1404
+ stdout: { label: 'STDOUT', color: white },
1405
+ };
1406
+ return typeMap[type] || { label: type.toUpperCase().padEnd(6).substring(0, 6), color: gray };
1407
+ }
1408
+ padWithAnsi(str, width) {
1409
+ const visibleLength = (0, formatter_1.stripAnsi)(str).length;
1410
+ const padding = Math.max(0, width - visibleLength);
1411
+ return str + ' '.repeat(padding);
1412
+ }
1413
+ /**
1414
+ * Safe string truncation that handles ANSI codes
1415
+ */
1416
+ safeSubstring(str, maxLen) {
1417
+ const stripped = (0, formatter_1.stripAnsi)(str);
1418
+ if (stripped.length <= maxLen)
1419
+ return str;
1420
+ // Simple approach: truncate stripped, find corresponding position in original
1421
+ let visibleCount = 0;
1422
+ let i = 0;
1423
+ while (i < str.length && visibleCount < maxLen - 3) {
1424
+ // Skip ANSI sequences
1425
+ if (str[i] === '\x1b') {
1426
+ const match = str.slice(i).match(/^\x1b\[[0-9;]*m/);
1427
+ if (match) {
1428
+ i += match[0].length;
1429
+ continue;
1430
+ }
1431
+ }
1432
+ visibleCount++;
1433
+ i++;
1434
+ }
1435
+ return str.slice(0, i) + '...';
1436
+ }
1437
+ /**
1438
+ * Render help overlay
1439
+ */
1440
+ renderHelpOverlay() {
1441
+ const { cyan, reset, bold, dim, yellow, gray } = UI.COLORS;
1442
+ const helpWidth = 60;
1443
+ const helpHeight = 20;
1444
+ const startX = Math.floor((this.screenWidth - helpWidth) / 2);
1445
+ const startY = Math.floor((this.screenHeight - helpHeight) / 2);
1446
+ const helpContent = [
1447
+ `${bold}📖 Keyboard Shortcuts${reset}`,
1448
+ '',
1449
+ `${yellow}Navigation${reset}`,
1450
+ ` ←/→ Tab switch / Enter detail / Panel switch`,
1451
+ ` ↑/↓ Select item / Scroll content`,
1452
+ ` Tab Quick tab switch`,
1453
+ ` Esc Go back / Close overlay`,
1454
+ '',
1455
+ `${yellow}Actions${reset}`,
1456
+ ` Enter Open action menu`,
1457
+ ` Space Toggle follow mode (in logs)`,
1458
+ ` R Toggle readable format`,
1459
+ ` ? Show/hide this help`,
1460
+ ` Q Quit`,
1461
+ '',
1462
+ `${yellow}Action Menu${reset}`,
1463
+ ` 1-9 Quick select action`,
1464
+ ` ↑/↓ Navigate actions`,
1465
+ ` Enter Execute selected action`,
1466
+ ];
1467
+ // Draw box
1468
+ process.stdout.write(`\x1b[${startY};${startX}H`);
1469
+ process.stdout.write(`${cyan}┌${'─'.repeat(helpWidth - 2)}┐${reset}`);
1470
+ for (let i = 0; i < helpContent.length; i++) {
1471
+ process.stdout.write(`\x1b[${startY + 1 + i};${startX}H`);
1472
+ const line = helpContent[i] || '';
1473
+ const paddedLine = this.padWithAnsi(line, helpWidth - 4);
1474
+ process.stdout.write(`${cyan}│${reset} ${paddedLine} ${cyan}│${reset}`);
1475
+ }
1476
+ // Fill remaining space
1477
+ for (let i = helpContent.length; i < helpHeight - 2; i++) {
1478
+ process.stdout.write(`\x1b[${startY + 1 + i};${startX}H`);
1479
+ process.stdout.write(`${cyan}│${reset}${' '.repeat(helpWidth - 2)}${cyan}│${reset}`);
1480
+ }
1481
+ process.stdout.write(`\x1b[${startY + helpHeight - 1};${startX}H`);
1482
+ process.stdout.write(`${cyan}│${reset}${dim} Press ? or Esc to close${reset}${' '.repeat(helpWidth - 27)}${cyan}│${reset}`);
1483
+ process.stdout.write(`\x1b[${startY + helpHeight};${startX}H`);
1484
+ process.stdout.write(`${cyan}└${'─'.repeat(helpWidth - 2)}┘${reset}`);
1485
+ }
1619
1486
  }
1620
- /**
1621
- * Find the latest run directory
1622
- */
1487
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1488
+ // Main Entry Point
1489
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1623
1490
  function findLatestRunDir(logsDir) {
1624
1491
  const runsDir = (0, path_1.safeJoin)(logsDir, 'runs');
1625
1492
  if (!fs.existsSync(runsDir))
@@ -1630,9 +1497,6 @@ function findLatestRunDir(logsDir) {
1630
1497
  .sort((a, b) => b.mtime - a.mtime);
1631
1498
  return runs.length > 0 ? runs[0].path : null;
1632
1499
  }
1633
- /**
1634
- * Monitor lanes
1635
- */
1636
1500
  async function monitor(args) {
1637
1501
  const help = args.includes('--help') || args.includes('-h');
1638
1502
  const list = args.includes('--list') || args.includes('-l');
@@ -1654,15 +1518,15 @@ async function monitor(args) {
1654
1518
  if (!runDir && !list)
1655
1519
  throw new Error('No run directories found');
1656
1520
  if (!runDir && list) {
1657
- // Create a dummy runDir if none exists but we want to see the list (dashboard will handle empty list)
1658
1521
  runDir = path.join((0, config_1.getLogsDir)(config), 'runs', 'empty');
1659
1522
  }
1660
1523
  }
1661
1524
  if (runDir && !fs.existsSync(runDir) && !list) {
1662
1525
  throw new Error(`Run directory not found: ${runDir}`);
1663
1526
  }
1664
- const monitor = new InteractiveMonitor(runDir, interval, undefined, list ? View.FLOWS_DASHBOARD : View.LIST);
1665
- await monitor.start();
1527
+ const initialTab = list ? Tab.ALL_FLOWS : Tab.CURRENT_FLOW;
1528
+ const mon = new InteractiveMonitor(runDir, interval, initialTab);
1529
+ await mon.start();
1666
1530
  }
1667
1531
  module.exports = monitor;
1668
1532
  //# sourceMappingURL=monitor.js.map