@litmers/cursorflow-orchestrator 0.1.40 → 0.2.3

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