@litmers/cursorflow-orchestrator 0.1.39 → 0.2.2

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