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