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