@litmers/cursorflow-orchestrator 0.1.39 → 0.2.2

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