@litmers/cursorflow-orchestrator 0.1.18 → 0.1.26
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 +25 -0
- package/README.md +25 -7
- package/commands/cursorflow-clean.md +19 -0
- package/commands/cursorflow-runs.md +59 -0
- package/commands/cursorflow-stop.md +55 -0
- package/dist/cli/clean.js +178 -6
- package/dist/cli/clean.js.map +1 -1
- package/dist/cli/index.js +12 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/init.js +8 -7
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/logs.js +126 -77
- package/dist/cli/logs.js.map +1 -1
- package/dist/cli/monitor.d.ts +7 -0
- package/dist/cli/monitor.js +1021 -202
- package/dist/cli/monitor.js.map +1 -1
- package/dist/cli/prepare.js +39 -21
- package/dist/cli/prepare.js.map +1 -1
- package/dist/cli/resume.js +268 -163
- package/dist/cli/resume.js.map +1 -1
- package/dist/cli/run.js +11 -5
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/runs.d.ts +5 -0
- package/dist/cli/runs.js +214 -0
- package/dist/cli/runs.js.map +1 -0
- package/dist/cli/setup-commands.js +0 -0
- package/dist/cli/signal.js +8 -8
- package/dist/cli/signal.js.map +1 -1
- package/dist/cli/stop.d.ts +5 -0
- package/dist/cli/stop.js +215 -0
- package/dist/cli/stop.js.map +1 -0
- package/dist/cli/tasks.d.ts +10 -0
- package/dist/cli/tasks.js +165 -0
- package/dist/cli/tasks.js.map +1 -0
- package/dist/core/auto-recovery.d.ts +212 -0
- package/dist/core/auto-recovery.js +737 -0
- package/dist/core/auto-recovery.js.map +1 -0
- package/dist/core/failure-policy.d.ts +156 -0
- package/dist/core/failure-policy.js +488 -0
- package/dist/core/failure-policy.js.map +1 -0
- package/dist/core/orchestrator.d.ts +16 -2
- package/dist/core/orchestrator.js +439 -105
- package/dist/core/orchestrator.js.map +1 -1
- package/dist/core/reviewer.d.ts +2 -0
- package/dist/core/reviewer.js +2 -0
- package/dist/core/reviewer.js.map +1 -1
- package/dist/core/runner.d.ts +33 -10
- package/dist/core/runner.js +374 -164
- package/dist/core/runner.js.map +1 -1
- package/dist/services/logging/buffer.d.ts +67 -0
- package/dist/services/logging/buffer.js +309 -0
- package/dist/services/logging/buffer.js.map +1 -0
- package/dist/services/logging/console.d.ts +89 -0
- package/dist/services/logging/console.js +169 -0
- package/dist/services/logging/console.js.map +1 -0
- package/dist/services/logging/file-writer.d.ts +71 -0
- package/dist/services/logging/file-writer.js +516 -0
- package/dist/services/logging/file-writer.js.map +1 -0
- package/dist/services/logging/formatter.d.ts +39 -0
- package/dist/services/logging/formatter.js +227 -0
- package/dist/services/logging/formatter.js.map +1 -0
- package/dist/services/logging/index.d.ts +11 -0
- package/dist/services/logging/index.js +30 -0
- package/dist/services/logging/index.js.map +1 -0
- package/dist/services/logging/parser.d.ts +31 -0
- package/dist/services/logging/parser.js +222 -0
- package/dist/services/logging/parser.js.map +1 -0
- package/dist/services/process/index.d.ts +59 -0
- package/dist/services/process/index.js +257 -0
- package/dist/services/process/index.js.map +1 -0
- package/dist/types/agent.d.ts +20 -0
- package/dist/types/agent.js +6 -0
- package/dist/types/agent.js.map +1 -0
- package/dist/types/config.d.ts +65 -0
- package/dist/types/config.js +6 -0
- package/dist/types/config.js.map +1 -0
- package/dist/types/events.d.ts +125 -0
- package/dist/types/events.js +6 -0
- package/dist/types/events.js.map +1 -0
- package/dist/types/index.d.ts +12 -0
- package/dist/types/index.js +37 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/lane.d.ts +43 -0
- package/dist/types/lane.js +6 -0
- package/dist/types/lane.js.map +1 -0
- package/dist/types/logging.d.ts +71 -0
- package/dist/types/logging.js +16 -0
- package/dist/types/logging.js.map +1 -0
- package/dist/types/review.d.ts +17 -0
- package/dist/types/review.js +6 -0
- package/dist/types/review.js.map +1 -0
- package/dist/types/run.d.ts +32 -0
- package/dist/types/run.js +6 -0
- package/dist/types/run.js.map +1 -0
- package/dist/types/task.d.ts +71 -0
- package/dist/types/task.js +6 -0
- package/dist/types/task.js.map +1 -0
- package/dist/ui/components.d.ts +134 -0
- package/dist/ui/components.js +389 -0
- package/dist/ui/components.js.map +1 -0
- package/dist/ui/log-viewer.d.ts +49 -0
- package/dist/ui/log-viewer.js +449 -0
- package/dist/ui/log-viewer.js.map +1 -0
- package/dist/utils/checkpoint.d.ts +87 -0
- package/dist/utils/checkpoint.js +317 -0
- package/dist/utils/checkpoint.js.map +1 -0
- package/dist/utils/config.d.ts +4 -0
- package/dist/utils/config.js +18 -8
- package/dist/utils/config.js.map +1 -1
- package/dist/utils/cursor-agent.js.map +1 -1
- package/dist/utils/dependency.d.ts +74 -0
- package/dist/utils/dependency.js +420 -0
- package/dist/utils/dependency.js.map +1 -0
- package/dist/utils/doctor.js +17 -11
- package/dist/utils/doctor.js.map +1 -1
- package/dist/utils/enhanced-logger.d.ts +10 -33
- package/dist/utils/enhanced-logger.js +108 -20
- package/dist/utils/enhanced-logger.js.map +1 -1
- package/dist/utils/git.d.ts +121 -0
- package/dist/utils/git.js +484 -11
- package/dist/utils/git.js.map +1 -1
- package/dist/utils/health.d.ts +91 -0
- package/dist/utils/health.js +556 -0
- package/dist/utils/health.js.map +1 -0
- package/dist/utils/lock.d.ts +95 -0
- package/dist/utils/lock.js +332 -0
- package/dist/utils/lock.js.map +1 -0
- package/dist/utils/log-buffer.d.ts +17 -0
- package/dist/utils/log-buffer.js +14 -0
- package/dist/utils/log-buffer.js.map +1 -0
- package/dist/utils/log-constants.d.ts +23 -0
- package/dist/utils/log-constants.js +28 -0
- package/dist/utils/log-constants.js.map +1 -0
- package/dist/utils/log-formatter.d.ts +25 -0
- package/dist/utils/log-formatter.js +237 -0
- package/dist/utils/log-formatter.js.map +1 -0
- package/dist/utils/log-service.d.ts +19 -0
- package/dist/utils/log-service.js +47 -0
- package/dist/utils/log-service.js.map +1 -0
- package/dist/utils/logger.d.ts +46 -27
- package/dist/utils/logger.js +82 -60
- package/dist/utils/logger.js.map +1 -1
- package/dist/utils/path.d.ts +19 -0
- package/dist/utils/path.js +77 -0
- package/dist/utils/path.js.map +1 -0
- package/dist/utils/process-manager.d.ts +21 -0
- package/dist/utils/process-manager.js +138 -0
- package/dist/utils/process-manager.js.map +1 -0
- package/dist/utils/retry.d.ts +121 -0
- package/dist/utils/retry.js +374 -0
- package/dist/utils/retry.js.map +1 -0
- package/dist/utils/run-service.d.ts +88 -0
- package/dist/utils/run-service.js +412 -0
- package/dist/utils/run-service.js.map +1 -0
- package/dist/utils/state.d.ts +62 -3
- package/dist/utils/state.js +317 -11
- package/dist/utils/state.js.map +1 -1
- package/dist/utils/task-service.d.ts +82 -0
- package/dist/utils/task-service.js +348 -0
- package/dist/utils/task-service.js.map +1 -0
- package/dist/utils/template.d.ts +14 -0
- package/dist/utils/template.js +122 -0
- package/dist/utils/template.js.map +1 -0
- package/dist/utils/types.d.ts +2 -271
- package/dist/utils/types.js +16 -0
- package/dist/utils/types.js.map +1 -1
- package/package.json +38 -23
- package/scripts/ai-security-check.js +0 -1
- package/scripts/local-security-gate.sh +0 -0
- package/scripts/monitor-lanes.sh +94 -0
- package/scripts/patches/test-cursor-agent.js +0 -1
- package/scripts/release.sh +0 -0
- package/scripts/setup-security.sh +0 -0
- package/scripts/stream-logs.sh +72 -0
- package/scripts/verify-and-fix.sh +0 -0
- package/src/cli/clean.ts +187 -6
- package/src/cli/index.ts +12 -1
- package/src/cli/init.ts +8 -7
- package/src/cli/logs.ts +124 -77
- package/src/cli/monitor.ts +1815 -898
- package/src/cli/prepare.ts +41 -21
- package/src/cli/resume.ts +753 -626
- package/src/cli/run.ts +12 -5
- package/src/cli/runs.ts +212 -0
- package/src/cli/setup-commands.ts +0 -0
- package/src/cli/signal.ts +8 -7
- package/src/cli/stop.ts +209 -0
- package/src/cli/tasks.ts +154 -0
- package/src/core/auto-recovery.ts +909 -0
- package/src/core/failure-policy.ts +592 -0
- package/src/core/orchestrator.ts +1131 -704
- package/src/core/reviewer.ts +4 -0
- package/src/core/runner.ts +444 -180
- package/src/services/logging/buffer.ts +326 -0
- package/src/services/logging/console.ts +193 -0
- package/src/services/logging/file-writer.ts +526 -0
- package/src/services/logging/formatter.ts +268 -0
- package/src/services/logging/index.ts +16 -0
- package/src/services/logging/parser.ts +232 -0
- package/src/services/process/index.ts +261 -0
- package/src/types/agent.ts +24 -0
- package/src/types/config.ts +79 -0
- package/src/types/events.ts +156 -0
- package/src/types/index.ts +29 -0
- package/src/types/lane.ts +56 -0
- package/src/types/logging.ts +96 -0
- package/src/types/review.ts +20 -0
- package/src/types/run.ts +37 -0
- package/src/types/task.ts +79 -0
- package/src/ui/components.ts +430 -0
- package/src/ui/log-viewer.ts +485 -0
- package/src/utils/checkpoint.ts +374 -0
- package/src/utils/config.ts +18 -8
- package/src/utils/cursor-agent.ts +1 -1
- package/src/utils/dependency.ts +482 -0
- package/src/utils/doctor.ts +18 -11
- package/src/utils/enhanced-logger.ts +122 -60
- package/src/utils/git.ts +517 -11
- package/src/utils/health.ts +596 -0
- package/src/utils/lock.ts +346 -0
- package/src/utils/log-buffer.ts +28 -0
- package/src/utils/log-constants.ts +26 -0
- package/src/utils/log-formatter.ts +245 -0
- package/src/utils/log-service.ts +49 -0
- package/src/utils/logger.ts +100 -51
- package/src/utils/path.ts +45 -0
- package/src/utils/process-manager.ts +100 -0
- package/src/utils/retry.ts +413 -0
- package/src/utils/run-service.ts +433 -0
- package/src/utils/state.ts +385 -11
- package/src/utils/task-service.ts +370 -0
- package/src/utils/template.ts +92 -0
- package/src/utils/types.ts +2 -314
- package/templates/basic.json +21 -0
package/dist/cli/monitor.js
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
3
|
* CursorFlow interactive monitor command
|
|
4
|
+
*
|
|
5
|
+
* Features:
|
|
6
|
+
* - Lane dashboard with accurate process status
|
|
7
|
+
* - Unified log view for all lanes
|
|
8
|
+
* - Readable log format support
|
|
9
|
+
* - Multiple flows dashboard
|
|
10
|
+
* - Consistent layout across all views
|
|
4
11
|
*/
|
|
5
12
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
6
13
|
if (k2 === undefined) k2 = k;
|
|
@@ -40,16 +47,50 @@ const path = __importStar(require("path"));
|
|
|
40
47
|
const readline = __importStar(require("readline"));
|
|
41
48
|
const state_1 = require("../utils/state");
|
|
42
49
|
const config_1 = require("../utils/config");
|
|
50
|
+
const path_1 = require("../utils/path");
|
|
51
|
+
const process_1 = require("../services/process");
|
|
52
|
+
const buffer_1 = require("../services/logging/buffer");
|
|
53
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
54
|
+
// UI Constants
|
|
55
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
56
|
+
const UI = {
|
|
57
|
+
COLORS: {
|
|
58
|
+
reset: '\x1b[0m',
|
|
59
|
+
bold: '\x1b[1m',
|
|
60
|
+
dim: '\x1b[2m',
|
|
61
|
+
cyan: '\x1b[36m',
|
|
62
|
+
green: '\x1b[32m',
|
|
63
|
+
yellow: '\x1b[33m',
|
|
64
|
+
red: '\x1b[31m',
|
|
65
|
+
magenta: '\x1b[35m',
|
|
66
|
+
gray: '\x1b[90m',
|
|
67
|
+
white: '\x1b[37m',
|
|
68
|
+
bgGray: '\x1b[48;5;236m',
|
|
69
|
+
bgCyan: '\x1b[46m',
|
|
70
|
+
},
|
|
71
|
+
CHARS: {
|
|
72
|
+
hLine: '━',
|
|
73
|
+
vLine: '│',
|
|
74
|
+
corner: {
|
|
75
|
+
tl: '┌', tr: '┐', bl: '└', br: '┘'
|
|
76
|
+
},
|
|
77
|
+
arrow: {
|
|
78
|
+
right: '▶', left: '◀', up: '▲', down: '▼'
|
|
79
|
+
},
|
|
80
|
+
bullet: '•',
|
|
81
|
+
check: '✓',
|
|
82
|
+
},
|
|
83
|
+
};
|
|
43
84
|
function printHelp() {
|
|
44
|
-
console.log(`
|
|
45
|
-
Usage: cursorflow monitor [run-dir] [options]
|
|
46
|
-
|
|
47
|
-
Interactive lane dashboard to track progress and dependencies.
|
|
48
|
-
|
|
49
|
-
Options:
|
|
50
|
-
[run-dir] Run directory to monitor (default: latest)
|
|
51
|
-
--interval <seconds> Refresh interval (default: 2)
|
|
52
|
-
--help, -h Show help
|
|
85
|
+
console.log(`
|
|
86
|
+
Usage: cursorflow monitor [run-dir] [options]
|
|
87
|
+
|
|
88
|
+
Interactive lane dashboard to track progress and dependencies.
|
|
89
|
+
|
|
90
|
+
Options:
|
|
91
|
+
[run-dir] Run directory to monitor (default: latest)
|
|
92
|
+
--interval <seconds> Refresh interval (default: 2)
|
|
93
|
+
--help, -h Show help
|
|
53
94
|
`);
|
|
54
95
|
}
|
|
55
96
|
var View;
|
|
@@ -61,6 +102,8 @@ var View;
|
|
|
61
102
|
View[View["TERMINAL"] = 4] = "TERMINAL";
|
|
62
103
|
View[View["INTERVENE"] = 5] = "INTERVENE";
|
|
63
104
|
View[View["TIMEOUT"] = 6] = "TIMEOUT";
|
|
105
|
+
View[View["UNIFIED_LOG"] = 7] = "UNIFIED_LOG";
|
|
106
|
+
View[View["FLOWS_DASHBOARD"] = 8] = "FLOWS_DASHBOARD";
|
|
64
107
|
})(View || (View = {}));
|
|
65
108
|
class InteractiveMonitor {
|
|
66
109
|
runDir;
|
|
@@ -74,19 +117,92 @@ class InteractiveMonitor {
|
|
|
74
117
|
timer = null;
|
|
75
118
|
scrollOffset = 0;
|
|
76
119
|
terminalScrollOffset = 0;
|
|
120
|
+
followMode = true;
|
|
121
|
+
unseenLineCount = 0;
|
|
77
122
|
lastTerminalTotalLines = 0;
|
|
78
123
|
interventionInput = '';
|
|
79
124
|
timeoutInput = '';
|
|
80
125
|
notification = null;
|
|
81
|
-
|
|
126
|
+
// Process status tracking
|
|
127
|
+
laneProcessStatuses = new Map();
|
|
128
|
+
// Unified log buffer for all lanes
|
|
129
|
+
unifiedLogBuffer = null;
|
|
130
|
+
unifiedLogScrollOffset = 0;
|
|
131
|
+
unifiedLogFollowMode = true;
|
|
132
|
+
// Multiple flows support
|
|
133
|
+
allFlows = [];
|
|
134
|
+
selectedFlowIndex = 0;
|
|
135
|
+
logsDir = '';
|
|
136
|
+
// NEW: UX improvements
|
|
137
|
+
readableFormat = true; // Toggle readable log format
|
|
138
|
+
laneFilter = null; // Filter by lane name
|
|
139
|
+
confirmAction = null;
|
|
140
|
+
// Screen dimensions
|
|
141
|
+
get screenWidth() {
|
|
142
|
+
return process.stdout.columns || 120;
|
|
143
|
+
}
|
|
144
|
+
get screenHeight() {
|
|
145
|
+
return process.stdout.rows || 24;
|
|
146
|
+
}
|
|
147
|
+
constructor(runDir, interval, logsDir) {
|
|
82
148
|
this.runDir = runDir;
|
|
83
149
|
this.interval = interval;
|
|
150
|
+
// Set logs directory for multiple flows discovery
|
|
151
|
+
if (logsDir) {
|
|
152
|
+
this.logsDir = logsDir;
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
const config = (0, config_1.loadConfig)();
|
|
156
|
+
this.logsDir = (0, path_1.safeJoin)(config.logsDir, 'runs');
|
|
157
|
+
}
|
|
158
|
+
// Initialize unified log buffer
|
|
159
|
+
this.unifiedLogBuffer = new buffer_1.LogBufferService(runDir);
|
|
84
160
|
}
|
|
85
161
|
async start() {
|
|
86
162
|
this.setupTerminal();
|
|
163
|
+
// Start unified log streaming
|
|
164
|
+
if (this.unifiedLogBuffer) {
|
|
165
|
+
this.unifiedLogBuffer.startStreaming();
|
|
166
|
+
this.unifiedLogBuffer.on('update', () => {
|
|
167
|
+
if (this.view === View.UNIFIED_LOG && this.unifiedLogFollowMode) {
|
|
168
|
+
this.render();
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
// Discover all flows
|
|
173
|
+
this.discoverFlows();
|
|
87
174
|
this.refresh();
|
|
88
175
|
this.timer = setInterval(() => this.refresh(), this.interval * 1000);
|
|
89
176
|
}
|
|
177
|
+
/**
|
|
178
|
+
* Discover all run directories (flows) for multi-flow view
|
|
179
|
+
*/
|
|
180
|
+
discoverFlows() {
|
|
181
|
+
try {
|
|
182
|
+
if (!fs.existsSync(this.logsDir))
|
|
183
|
+
return;
|
|
184
|
+
const runs = fs.readdirSync(this.logsDir)
|
|
185
|
+
.filter(d => d.startsWith('run-'))
|
|
186
|
+
.map(d => {
|
|
187
|
+
const runDir = (0, path_1.safeJoin)(this.logsDir, d);
|
|
188
|
+
const summary = (0, process_1.getFlowSummary)(runDir);
|
|
189
|
+
return {
|
|
190
|
+
runDir,
|
|
191
|
+
runId: d,
|
|
192
|
+
isAlive: summary.isAlive,
|
|
193
|
+
summary,
|
|
194
|
+
};
|
|
195
|
+
})
|
|
196
|
+
.sort((a, b) => {
|
|
197
|
+
// Sort by run ID (timestamp-based) descending
|
|
198
|
+
return b.runId.localeCompare(a.runId);
|
|
199
|
+
});
|
|
200
|
+
this.allFlows = runs;
|
|
201
|
+
}
|
|
202
|
+
catch {
|
|
203
|
+
// Ignore errors
|
|
204
|
+
}
|
|
205
|
+
}
|
|
90
206
|
setupTerminal() {
|
|
91
207
|
if (process.stdin.isTTY) {
|
|
92
208
|
process.stdin.setRawMode(true);
|
|
@@ -121,6 +237,12 @@ class InteractiveMonitor {
|
|
|
121
237
|
else if (this.view === View.MESSAGE_DETAIL) {
|
|
122
238
|
this.handleMessageDetailKey(keyName);
|
|
123
239
|
}
|
|
240
|
+
else if (this.view === View.UNIFIED_LOG) {
|
|
241
|
+
this.handleUnifiedLogKey(keyName);
|
|
242
|
+
}
|
|
243
|
+
else if (this.view === View.FLOWS_DASHBOARD) {
|
|
244
|
+
this.handleFlowsDashboardKey(keyName);
|
|
245
|
+
}
|
|
124
246
|
});
|
|
125
247
|
// Hide cursor
|
|
126
248
|
process.stdout.write('\x1B[?25l');
|
|
@@ -128,6 +250,10 @@ class InteractiveMonitor {
|
|
|
128
250
|
stop() {
|
|
129
251
|
if (this.timer)
|
|
130
252
|
clearInterval(this.timer);
|
|
253
|
+
// Stop unified log streaming
|
|
254
|
+
if (this.unifiedLogBuffer) {
|
|
255
|
+
this.unifiedLogBuffer.stopStreaming();
|
|
256
|
+
}
|
|
131
257
|
// Show cursor and clear screen
|
|
132
258
|
process.stdout.write('\x1B[?25h');
|
|
133
259
|
process.stdout.write('\x1Bc');
|
|
@@ -161,6 +287,19 @@ class InteractiveMonitor {
|
|
|
161
287
|
this.view = View.FLOW;
|
|
162
288
|
this.render();
|
|
163
289
|
break;
|
|
290
|
+
case 'u':
|
|
291
|
+
// Unified log view
|
|
292
|
+
this.view = View.UNIFIED_LOG;
|
|
293
|
+
this.unifiedLogScrollOffset = 0;
|
|
294
|
+
this.unifiedLogFollowMode = true;
|
|
295
|
+
this.render();
|
|
296
|
+
break;
|
|
297
|
+
case 'm':
|
|
298
|
+
// Multiple flows dashboard
|
|
299
|
+
this.discoverFlows();
|
|
300
|
+
this.view = View.FLOWS_DASHBOARD;
|
|
301
|
+
this.render();
|
|
302
|
+
break;
|
|
164
303
|
case 'q':
|
|
165
304
|
this.stop();
|
|
166
305
|
break;
|
|
@@ -248,11 +387,29 @@ class InteractiveMonitor {
|
|
|
248
387
|
handleTerminalKey(key) {
|
|
249
388
|
switch (key) {
|
|
250
389
|
case 'up':
|
|
390
|
+
this.followMode = false;
|
|
251
391
|
this.terminalScrollOffset++;
|
|
252
392
|
this.render();
|
|
253
393
|
break;
|
|
254
394
|
case 'down':
|
|
255
395
|
this.terminalScrollOffset = Math.max(0, this.terminalScrollOffset - 1);
|
|
396
|
+
if (this.terminalScrollOffset === 0) {
|
|
397
|
+
this.followMode = true;
|
|
398
|
+
this.unseenLineCount = 0;
|
|
399
|
+
}
|
|
400
|
+
this.render();
|
|
401
|
+
break;
|
|
402
|
+
case 'f':
|
|
403
|
+
this.followMode = true;
|
|
404
|
+
this.terminalScrollOffset = 0;
|
|
405
|
+
this.unseenLineCount = 0;
|
|
406
|
+
this.render();
|
|
407
|
+
break;
|
|
408
|
+
case 'r':
|
|
409
|
+
// Toggle readable log format
|
|
410
|
+
this.readableFormat = !this.readableFormat;
|
|
411
|
+
this.terminalScrollOffset = 0;
|
|
412
|
+
this.lastTerminalTotalLines = 0;
|
|
256
413
|
this.render();
|
|
257
414
|
break;
|
|
258
415
|
case 't':
|
|
@@ -338,6 +495,197 @@ class InteractiveMonitor {
|
|
|
338
495
|
break;
|
|
339
496
|
}
|
|
340
497
|
}
|
|
498
|
+
handleUnifiedLogKey(key) {
|
|
499
|
+
const pageSize = Math.max(10, this.screenHeight - 12);
|
|
500
|
+
switch (key) {
|
|
501
|
+
case 'up':
|
|
502
|
+
this.unifiedLogFollowMode = false;
|
|
503
|
+
this.unifiedLogScrollOffset++;
|
|
504
|
+
this.render();
|
|
505
|
+
break;
|
|
506
|
+
case 'down':
|
|
507
|
+
this.unifiedLogScrollOffset = Math.max(0, this.unifiedLogScrollOffset - 1);
|
|
508
|
+
if (this.unifiedLogScrollOffset === 0) {
|
|
509
|
+
this.unifiedLogFollowMode = true;
|
|
510
|
+
}
|
|
511
|
+
this.render();
|
|
512
|
+
break;
|
|
513
|
+
case 'pageup':
|
|
514
|
+
this.unifiedLogFollowMode = false;
|
|
515
|
+
this.unifiedLogScrollOffset += pageSize;
|
|
516
|
+
this.render();
|
|
517
|
+
break;
|
|
518
|
+
case 'pagedown':
|
|
519
|
+
this.unifiedLogScrollOffset = Math.max(0, this.unifiedLogScrollOffset - pageSize);
|
|
520
|
+
if (this.unifiedLogScrollOffset === 0) {
|
|
521
|
+
this.unifiedLogFollowMode = true;
|
|
522
|
+
}
|
|
523
|
+
this.render();
|
|
524
|
+
break;
|
|
525
|
+
case 'f':
|
|
526
|
+
this.unifiedLogFollowMode = true;
|
|
527
|
+
this.unifiedLogScrollOffset = 0;
|
|
528
|
+
this.render();
|
|
529
|
+
break;
|
|
530
|
+
case 'r':
|
|
531
|
+
// Toggle readable format
|
|
532
|
+
this.readableFormat = !this.readableFormat;
|
|
533
|
+
this.render();
|
|
534
|
+
break;
|
|
535
|
+
case 'l':
|
|
536
|
+
// Cycle through lane filter
|
|
537
|
+
this.cycleLaneFilter();
|
|
538
|
+
this.unifiedLogScrollOffset = 0;
|
|
539
|
+
this.render();
|
|
540
|
+
break;
|
|
541
|
+
case 'escape':
|
|
542
|
+
case 'backspace':
|
|
543
|
+
case 'u':
|
|
544
|
+
this.view = View.LIST;
|
|
545
|
+
this.render();
|
|
546
|
+
break;
|
|
547
|
+
case 'q':
|
|
548
|
+
this.stop();
|
|
549
|
+
break;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
/**
|
|
553
|
+
* Cycle through available lanes for filtering
|
|
554
|
+
*/
|
|
555
|
+
cycleLaneFilter() {
|
|
556
|
+
const lanes = this.unifiedLogBuffer?.getLanes() || [];
|
|
557
|
+
if (lanes.length === 0) {
|
|
558
|
+
this.laneFilter = null;
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
if (this.laneFilter === null) {
|
|
562
|
+
// Show first lane
|
|
563
|
+
this.laneFilter = lanes[0];
|
|
564
|
+
}
|
|
565
|
+
else {
|
|
566
|
+
const currentIndex = lanes.indexOf(this.laneFilter);
|
|
567
|
+
if (currentIndex === -1 || currentIndex === lanes.length - 1) {
|
|
568
|
+
// Reset to all lanes
|
|
569
|
+
this.laneFilter = null;
|
|
570
|
+
}
|
|
571
|
+
else {
|
|
572
|
+
// Next lane
|
|
573
|
+
this.laneFilter = lanes[currentIndex + 1];
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
handleFlowsDashboardKey(key) {
|
|
578
|
+
// Handle confirmation dialog first
|
|
579
|
+
if (this.confirmAction) {
|
|
580
|
+
if (key === 'y') {
|
|
581
|
+
this.executeConfirmedAction();
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
else if (key === 'n' || key === 'escape') {
|
|
585
|
+
this.confirmAction = null;
|
|
586
|
+
this.render();
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
// Other keys cancel confirmation
|
|
590
|
+
this.confirmAction = null;
|
|
591
|
+
this.render();
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
switch (key) {
|
|
595
|
+
case 'up':
|
|
596
|
+
this.selectedFlowIndex = Math.max(0, this.selectedFlowIndex - 1);
|
|
597
|
+
this.render();
|
|
598
|
+
break;
|
|
599
|
+
case 'down':
|
|
600
|
+
this.selectedFlowIndex = Math.min(this.allFlows.length - 1, this.selectedFlowIndex + 1);
|
|
601
|
+
this.render();
|
|
602
|
+
break;
|
|
603
|
+
case 'right':
|
|
604
|
+
case 'return':
|
|
605
|
+
case 'enter':
|
|
606
|
+
// Switch to selected flow
|
|
607
|
+
if (this.allFlows[this.selectedFlowIndex]) {
|
|
608
|
+
const flow = this.allFlows[this.selectedFlowIndex];
|
|
609
|
+
this.runDir = flow.runDir;
|
|
610
|
+
// Restart log buffer for new run
|
|
611
|
+
if (this.unifiedLogBuffer) {
|
|
612
|
+
this.unifiedLogBuffer.stopStreaming();
|
|
613
|
+
}
|
|
614
|
+
this.unifiedLogBuffer = new buffer_1.LogBufferService(this.runDir);
|
|
615
|
+
this.unifiedLogBuffer.startStreaming();
|
|
616
|
+
this.lanes = [];
|
|
617
|
+
this.laneProcessStatuses.clear();
|
|
618
|
+
this.view = View.LIST;
|
|
619
|
+
this.showNotification(`Switched to flow: ${flow.runId}`, 'info');
|
|
620
|
+
this.refresh();
|
|
621
|
+
}
|
|
622
|
+
break;
|
|
623
|
+
case 'd':
|
|
624
|
+
// Delete flow (with confirmation)
|
|
625
|
+
if (this.allFlows[this.selectedFlowIndex]) {
|
|
626
|
+
const flow = this.allFlows[this.selectedFlowIndex];
|
|
627
|
+
if (flow.isAlive) {
|
|
628
|
+
this.showNotification('Cannot delete a running flow. Stop it first.', 'error');
|
|
629
|
+
}
|
|
630
|
+
else if (flow.runDir === this.runDir) {
|
|
631
|
+
this.showNotification('Cannot delete the currently viewed flow.', 'error');
|
|
632
|
+
}
|
|
633
|
+
else {
|
|
634
|
+
this.confirmAction = {
|
|
635
|
+
type: 'delete-flow',
|
|
636
|
+
target: flow.runId,
|
|
637
|
+
time: Date.now(),
|
|
638
|
+
};
|
|
639
|
+
this.render();
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
break;
|
|
643
|
+
case 'r':
|
|
644
|
+
// Refresh flows
|
|
645
|
+
this.discoverFlows();
|
|
646
|
+
this.showNotification('Flows refreshed', 'info');
|
|
647
|
+
this.render();
|
|
648
|
+
break;
|
|
649
|
+
case 'escape':
|
|
650
|
+
case 'backspace':
|
|
651
|
+
case 'm':
|
|
652
|
+
this.view = View.LIST;
|
|
653
|
+
this.render();
|
|
654
|
+
break;
|
|
655
|
+
case 'q':
|
|
656
|
+
this.stop();
|
|
657
|
+
break;
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
/**
|
|
661
|
+
* Execute a confirmed action (delete flow, kill process, etc.)
|
|
662
|
+
*/
|
|
663
|
+
executeConfirmedAction() {
|
|
664
|
+
if (!this.confirmAction)
|
|
665
|
+
return;
|
|
666
|
+
const { type, target } = this.confirmAction;
|
|
667
|
+
this.confirmAction = null;
|
|
668
|
+
if (type === 'delete-flow') {
|
|
669
|
+
const flow = this.allFlows.find(f => f.runId === target);
|
|
670
|
+
if (flow) {
|
|
671
|
+
try {
|
|
672
|
+
// Delete the flow directory
|
|
673
|
+
fs.rmSync(flow.runDir, { recursive: true, force: true });
|
|
674
|
+
this.showNotification(`Deleted flow: ${target}`, 'success');
|
|
675
|
+
// Refresh the list
|
|
676
|
+
this.discoverFlows();
|
|
677
|
+
// Adjust selection if needed
|
|
678
|
+
if (this.selectedFlowIndex >= this.allFlows.length) {
|
|
679
|
+
this.selectedFlowIndex = Math.max(0, this.allFlows.length - 1);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
catch (err) {
|
|
683
|
+
this.showNotification(`Failed to delete flow: ${err}`, 'error');
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
this.render();
|
|
688
|
+
}
|
|
341
689
|
sendIntervention(message) {
|
|
342
690
|
if (!this.selectedLaneName)
|
|
343
691
|
return;
|
|
@@ -345,13 +693,13 @@ class InteractiveMonitor {
|
|
|
345
693
|
if (!lane)
|
|
346
694
|
return;
|
|
347
695
|
try {
|
|
348
|
-
const interventionPath =
|
|
696
|
+
const interventionPath = (0, path_1.safeJoin)(lane.path, 'intervention.txt');
|
|
349
697
|
fs.writeFileSync(interventionPath, message, 'utf8');
|
|
350
698
|
// Also log it to the conversation
|
|
351
|
-
const convoPath =
|
|
699
|
+
const convoPath = (0, path_1.safeJoin)(lane.path, 'conversation.jsonl');
|
|
352
700
|
const entry = {
|
|
353
701
|
timestamp: new Date().toISOString(),
|
|
354
|
-
role: '
|
|
702
|
+
role: 'intervention',
|
|
355
703
|
task: 'INTERVENTION',
|
|
356
704
|
fullText: `[HUMAN INTERVENTION]: ${message}`,
|
|
357
705
|
textLength: message.length + 20,
|
|
@@ -376,7 +724,7 @@ class InteractiveMonitor {
|
|
|
376
724
|
this.showNotification('Invalid timeout value', 'error');
|
|
377
725
|
return;
|
|
378
726
|
}
|
|
379
|
-
const timeoutPath =
|
|
727
|
+
const timeoutPath = (0, path_1.safeJoin)(lane.path, 'timeout.txt');
|
|
380
728
|
fs.writeFileSync(timeoutPath, String(timeoutMs), 'utf8');
|
|
381
729
|
this.showNotification(`Timeout updated to ${Math.round(timeoutMs / 1000)}s`, 'success');
|
|
382
730
|
}
|
|
@@ -390,7 +738,7 @@ class InteractiveMonitor {
|
|
|
390
738
|
const lane = this.lanes.find(l => l.name === this.selectedLaneName);
|
|
391
739
|
if (!lane)
|
|
392
740
|
return;
|
|
393
|
-
const convoPath =
|
|
741
|
+
const convoPath = (0, path_1.safeJoin)(lane.path, 'conversation.jsonl');
|
|
394
742
|
this.currentLogs = (0, state_1.readLog)(convoPath);
|
|
395
743
|
// Keep selection in bounds after refresh
|
|
396
744
|
if (this.selectedMessageIndex >= this.currentLogs.length) {
|
|
@@ -399,11 +747,29 @@ class InteractiveMonitor {
|
|
|
399
747
|
}
|
|
400
748
|
refresh() {
|
|
401
749
|
this.lanes = this.listLanesWithDeps(this.runDir);
|
|
402
|
-
|
|
750
|
+
// Update process statuses for accurate display
|
|
751
|
+
this.updateProcessStatuses();
|
|
752
|
+
if (this.view !== View.LIST && this.view !== View.UNIFIED_LOG && this.view !== View.FLOWS_DASHBOARD) {
|
|
403
753
|
this.refreshLogs();
|
|
404
754
|
}
|
|
755
|
+
// Refresh flows list periodically
|
|
756
|
+
if (this.view === View.FLOWS_DASHBOARD) {
|
|
757
|
+
this.discoverFlows();
|
|
758
|
+
}
|
|
405
759
|
this.render();
|
|
406
760
|
}
|
|
761
|
+
/**
|
|
762
|
+
* Update process statuses for all lanes
|
|
763
|
+
*/
|
|
764
|
+
updateProcessStatuses() {
|
|
765
|
+
const lanesDir = (0, path_1.safeJoin)(this.runDir, 'lanes');
|
|
766
|
+
if (!fs.existsSync(lanesDir))
|
|
767
|
+
return;
|
|
768
|
+
for (const lane of this.lanes) {
|
|
769
|
+
const status = (0, process_1.getLaneProcessStatus)(lane.path, lane.name);
|
|
770
|
+
this.laneProcessStatuses.set(lane.name, status);
|
|
771
|
+
}
|
|
772
|
+
}
|
|
407
773
|
killLane() {
|
|
408
774
|
if (!this.selectedLaneName)
|
|
409
775
|
return;
|
|
@@ -428,6 +794,55 @@ class InteractiveMonitor {
|
|
|
428
794
|
this.notification = { message, type, time: Date.now() };
|
|
429
795
|
this.render();
|
|
430
796
|
}
|
|
797
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
798
|
+
// UI Layout Helpers - Consistent header/footer across all views
|
|
799
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
800
|
+
renderHeader(title, breadcrumb = []) {
|
|
801
|
+
const width = Math.min(this.screenWidth, 120);
|
|
802
|
+
const line = UI.CHARS.hLine.repeat(width);
|
|
803
|
+
// Flow status
|
|
804
|
+
const flowSummary = (0, process_1.getFlowSummary)(this.runDir);
|
|
805
|
+
const flowStatusIcon = flowSummary.isAlive ? '🟢' : (flowSummary.completed === flowSummary.total && flowSummary.total > 0 ? '✅' : '🔴');
|
|
806
|
+
// Breadcrumb
|
|
807
|
+
const crumbs = ['CursorFlow', ...breadcrumb].join(` ${UI.COLORS.gray}›${UI.COLORS.reset} `);
|
|
808
|
+
// Time
|
|
809
|
+
const timeStr = new Date().toLocaleTimeString('en-US', { hour12: false });
|
|
810
|
+
process.stdout.write(`${UI.COLORS.cyan}${line}${UI.COLORS.reset}\n`);
|
|
811
|
+
process.stdout.write(`${UI.COLORS.bold}${crumbs}${UI.COLORS.reset} ${flowStatusIcon} `);
|
|
812
|
+
process.stdout.write(`${UI.COLORS.dim}${timeStr}${UI.COLORS.reset}\n`);
|
|
813
|
+
process.stdout.write(`${UI.COLORS.cyan}${line}${UI.COLORS.reset}\n`);
|
|
814
|
+
}
|
|
815
|
+
renderFooter(actions) {
|
|
816
|
+
const width = Math.min(this.screenWidth, 120);
|
|
817
|
+
const line = UI.CHARS.hLine.repeat(width);
|
|
818
|
+
// Notification area
|
|
819
|
+
if (this.notification && Date.now() - this.notification.time < 3000) {
|
|
820
|
+
const nColor = this.notification.type === 'error' ? UI.COLORS.red
|
|
821
|
+
: this.notification.type === 'success' ? UI.COLORS.green
|
|
822
|
+
: UI.COLORS.cyan;
|
|
823
|
+
process.stdout.write(`\n${nColor}🔔 ${this.notification.message}${UI.COLORS.reset}\n`);
|
|
824
|
+
}
|
|
825
|
+
// Confirmation dialog area
|
|
826
|
+
if (this.confirmAction && Date.now() - this.confirmAction.time < 10000) {
|
|
827
|
+
const actionName = this.confirmAction.type === 'delete-flow' ? 'DELETE FLOW' : 'KILL PROCESS';
|
|
828
|
+
process.stdout.write(`\n${UI.COLORS.yellow}⚠️ Confirm ${actionName}: ${this.confirmAction.target}? [Y] Yes / [N] No${UI.COLORS.reset}\n`);
|
|
829
|
+
}
|
|
830
|
+
process.stdout.write(`\n${UI.COLORS.cyan}${line}${UI.COLORS.reset}\n`);
|
|
831
|
+
const formattedActions = actions.map(a => {
|
|
832
|
+
const parts = a.split('] ');
|
|
833
|
+
if (parts.length === 2) {
|
|
834
|
+
// Use regex with global flag to replace all occurrences
|
|
835
|
+
return `${UI.COLORS.yellow}[${parts[0].replace(/\[/g, '')}]${UI.COLORS.reset} ${parts[1]}`;
|
|
836
|
+
}
|
|
837
|
+
return a;
|
|
838
|
+
});
|
|
839
|
+
process.stdout.write(` ${formattedActions.join(' ')}\n`);
|
|
840
|
+
}
|
|
841
|
+
renderSectionTitle(title, extra) {
|
|
842
|
+
const extraStr = extra ? ` ${UI.COLORS.dim}${extra}${UI.COLORS.reset}` : '';
|
|
843
|
+
process.stdout.write(`\n${UI.COLORS.bold}${title}${UI.COLORS.reset}${extraStr}\n`);
|
|
844
|
+
process.stdout.write(`${UI.COLORS.gray}${'─'.repeat(40)}${UI.COLORS.reset}\n`);
|
|
845
|
+
}
|
|
431
846
|
render() {
|
|
432
847
|
// Clear screen
|
|
433
848
|
process.stdout.write('\x1Bc');
|
|
@@ -435,9 +850,9 @@ class InteractiveMonitor {
|
|
|
435
850
|
if (this.notification && Date.now() - this.notification.time > 3000) {
|
|
436
851
|
this.notification = null;
|
|
437
852
|
}
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
853
|
+
// Clear old confirmation
|
|
854
|
+
if (this.confirmAction && Date.now() - this.confirmAction.time > 10000) {
|
|
855
|
+
this.confirmAction = null;
|
|
441
856
|
}
|
|
442
857
|
switch (this.view) {
|
|
443
858
|
case View.LIST:
|
|
@@ -461,67 +876,113 @@ class InteractiveMonitor {
|
|
|
461
876
|
case View.TIMEOUT:
|
|
462
877
|
this.renderTimeout();
|
|
463
878
|
break;
|
|
879
|
+
case View.UNIFIED_LOG:
|
|
880
|
+
this.renderUnifiedLog();
|
|
881
|
+
break;
|
|
882
|
+
case View.FLOWS_DASHBOARD:
|
|
883
|
+
this.renderFlowsDashboard();
|
|
884
|
+
break;
|
|
464
885
|
}
|
|
465
886
|
}
|
|
466
887
|
renderList() {
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
888
|
+
const flowSummary = (0, process_1.getFlowSummary)(this.runDir);
|
|
889
|
+
const runId = path.basename(this.runDir);
|
|
890
|
+
this.renderHeader('Lane Dashboard', [runId]);
|
|
891
|
+
// Summary line
|
|
892
|
+
const summaryParts = [
|
|
893
|
+
`${flowSummary.running} ${UI.COLORS.cyan}running${UI.COLORS.reset}`,
|
|
894
|
+
`${flowSummary.completed} ${UI.COLORS.green}done${UI.COLORS.reset}`,
|
|
895
|
+
`${flowSummary.failed} ${UI.COLORS.red}failed${UI.COLORS.reset}`,
|
|
896
|
+
`${flowSummary.dead} ${UI.COLORS.yellow}stale${UI.COLORS.reset}`,
|
|
897
|
+
];
|
|
898
|
+
process.stdout.write(` ${UI.COLORS.dim}Lanes:${UI.COLORS.reset} ${summaryParts.join(' │ ')}\n`);
|
|
471
899
|
if (this.lanes.length === 0) {
|
|
472
|
-
|
|
900
|
+
process.stdout.write(`\n ${UI.COLORS.dim}No lanes found${UI.COLORS.reset}\n`);
|
|
901
|
+
this.renderFooter(['[Q] Quit', '[M] All Flows']);
|
|
473
902
|
return;
|
|
474
903
|
}
|
|
475
904
|
const laneStatuses = {};
|
|
476
905
|
this.lanes.forEach(l => laneStatuses[l.name] = this.getLaneStatus(l.path, l.name));
|
|
477
|
-
const maxNameLen = Math.max(...this.lanes.map(l => l.name.length),
|
|
478
|
-
|
|
479
|
-
|
|
906
|
+
const maxNameLen = Math.max(...this.lanes.map(l => l.name.length), 12);
|
|
907
|
+
process.stdout.write(`\n ${'Lane'.padEnd(maxNameLen)} ${'Status'.padEnd(12)} ${'PID'.padEnd(7)} ${'Time'.padEnd(8)} ${'Tasks'.padEnd(6)} Next\n`);
|
|
908
|
+
process.stdout.write(` ${'─'.repeat(maxNameLen)} ${'─'.repeat(12)} ${'─'.repeat(7)} ${'─'.repeat(8)} ${'─'.repeat(6)} ${'─'.repeat(25)}\n`);
|
|
480
909
|
this.lanes.forEach((lane, i) => {
|
|
481
910
|
const isSelected = i === this.selectedLaneIndex;
|
|
482
911
|
const status = laneStatuses[lane.name];
|
|
483
|
-
const
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
let
|
|
912
|
+
const processStatus = this.laneProcessStatuses.get(lane.name);
|
|
913
|
+
// Determine the accurate status based on process detection
|
|
914
|
+
let displayStatus = status.status;
|
|
915
|
+
let statusColor = UI.COLORS.gray;
|
|
916
|
+
let statusIcon = this.getStatusIcon(status.status);
|
|
917
|
+
if (processStatus) {
|
|
918
|
+
if (processStatus.isStale) {
|
|
919
|
+
displayStatus = 'STALE';
|
|
920
|
+
statusIcon = '💀';
|
|
921
|
+
statusColor = UI.COLORS.yellow;
|
|
922
|
+
}
|
|
923
|
+
else if (processStatus.actualStatus === 'dead' && status.status === 'running') {
|
|
924
|
+
displayStatus = 'DEAD';
|
|
925
|
+
statusIcon = '☠️';
|
|
926
|
+
statusColor = UI.COLORS.red;
|
|
927
|
+
}
|
|
928
|
+
else if (processStatus.actualStatus === 'running') {
|
|
929
|
+
statusColor = UI.COLORS.cyan;
|
|
930
|
+
}
|
|
931
|
+
else if (status.status === 'completed') {
|
|
932
|
+
statusColor = UI.COLORS.green;
|
|
933
|
+
}
|
|
934
|
+
else if (status.status === 'failed') {
|
|
935
|
+
statusColor = UI.COLORS.red;
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
const statusText = `${statusIcon} ${displayStatus}`.padEnd(12);
|
|
939
|
+
// Process indicator
|
|
940
|
+
let pidText = '-'.padEnd(7);
|
|
941
|
+
if (processStatus?.pid) {
|
|
942
|
+
const pidIcon = processStatus.processRunning ? '●' : '○';
|
|
943
|
+
const pidColor = processStatus.processRunning ? UI.COLORS.green : UI.COLORS.red;
|
|
944
|
+
pidText = `${pidColor}${pidIcon}${UI.COLORS.reset}${processStatus.pid}`.padEnd(7 + 9); // +9 for color codes
|
|
945
|
+
}
|
|
946
|
+
// Duration
|
|
947
|
+
const duration = processStatus?.duration || status.duration;
|
|
948
|
+
const timeText = this.formatDuration(duration).padEnd(8);
|
|
949
|
+
// Tasks
|
|
950
|
+
let tasksText = '-'.padEnd(6);
|
|
488
951
|
if (typeof status.totalTasks === 'number') {
|
|
489
|
-
|
|
952
|
+
tasksText = `${status.currentTask}/${status.totalTasks}`.padEnd(6);
|
|
490
953
|
}
|
|
491
|
-
|
|
492
|
-
// Determine "Next Action"
|
|
954
|
+
// Next action
|
|
493
955
|
let nextAction = '-';
|
|
494
956
|
if (status.status === 'completed') {
|
|
495
|
-
const dependents = this.lanes.filter(l => laneStatuses[l.name]
|
|
496
|
-
|
|
497
|
-
nextAction = `Unlock: ${dependents.map(d => d.name).join(', ')}`;
|
|
498
|
-
}
|
|
499
|
-
else {
|
|
500
|
-
nextAction = '🏁 Done';
|
|
501
|
-
}
|
|
957
|
+
const dependents = this.lanes.filter(l => laneStatuses[l.name]?.dependsOn?.includes(lane.name));
|
|
958
|
+
nextAction = dependents.length > 0 ? `→ ${dependents.map(d => d.name).join(', ')}` : '✓ Done';
|
|
502
959
|
}
|
|
503
960
|
else if (status.status === 'waiting') {
|
|
504
|
-
if (status.waitingFor
|
|
505
|
-
nextAction =
|
|
961
|
+
if (status.waitingFor?.length > 0) {
|
|
962
|
+
nextAction = `⏳ ${status.waitingFor.join(', ')}`;
|
|
506
963
|
}
|
|
507
964
|
else {
|
|
508
|
-
const missingDeps = status.dependsOn.filter((d) => laneStatuses[d]
|
|
509
|
-
nextAction =
|
|
965
|
+
const missingDeps = status.dependsOn.filter((d) => laneStatuses[d]?.status !== 'completed');
|
|
966
|
+
nextAction = missingDeps.length > 0 ? `⏳ ${missingDeps.join(', ')}` : '⏳ waiting';
|
|
510
967
|
}
|
|
511
968
|
}
|
|
512
|
-
else if (
|
|
513
|
-
nextAction = '🚀
|
|
514
|
-
}
|
|
515
|
-
const prefix = isSelected ? ' ▶ ' : ' ';
|
|
516
|
-
const line = `${prefix}${lane.name.padEnd(maxNameLen)} ${statusText} ${progressText} ${timeText} ${tasksText} ${nextAction}`;
|
|
517
|
-
if (isSelected) {
|
|
518
|
-
process.stdout.write(`\x1b[36m${line}\x1b[0m\n`);
|
|
969
|
+
else if (processStatus?.actualStatus === 'running') {
|
|
970
|
+
nextAction = '🚀 working...';
|
|
519
971
|
}
|
|
520
|
-
else {
|
|
521
|
-
|
|
972
|
+
else if (processStatus?.isStale) {
|
|
973
|
+
nextAction = '⚠️ died unexpectedly';
|
|
522
974
|
}
|
|
975
|
+
// Truncate next action
|
|
976
|
+
if (nextAction.length > 25)
|
|
977
|
+
nextAction = nextAction.substring(0, 22) + '...';
|
|
978
|
+
const prefix = isSelected ? ` ${UI.COLORS.cyan}▶${UI.COLORS.reset} ` : ' ';
|
|
979
|
+
const rowBg = isSelected ? UI.COLORS.bgGray : '';
|
|
980
|
+
const rowEnd = isSelected ? UI.COLORS.reset : '';
|
|
981
|
+
process.stdout.write(`${rowBg}${prefix}${lane.name.padEnd(maxNameLen)} ${statusColor}${statusText}${UI.COLORS.reset} ${pidText} ${timeText} ${tasksText} ${nextAction}${rowEnd}\n`);
|
|
523
982
|
});
|
|
524
|
-
|
|
983
|
+
this.renderFooter([
|
|
984
|
+
'[↑↓] Select', '[→/Enter] Details', '[F] Flow', '[U] Unified Logs', '[M] All Flows', '[Q] Quit'
|
|
985
|
+
]);
|
|
525
986
|
}
|
|
526
987
|
renderLaneDetail() {
|
|
527
988
|
const lane = this.lanes.find(l => l.name === this.selectedLaneName);
|
|
@@ -531,70 +992,92 @@ class InteractiveMonitor {
|
|
|
531
992
|
return;
|
|
532
993
|
}
|
|
533
994
|
const status = this.getLaneStatus(lane.path, lane.name);
|
|
534
|
-
const
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
process.stdout.write(
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
process.stdout.write(`
|
|
995
|
+
const processStatus = this.laneProcessStatuses.get(lane.name);
|
|
996
|
+
this.renderHeader('Lane Detail', [path.basename(this.runDir), lane.name]);
|
|
997
|
+
// Status grid
|
|
998
|
+
const statusColor = status.status === 'completed' ? UI.COLORS.green
|
|
999
|
+
: status.status === 'failed' ? UI.COLORS.red
|
|
1000
|
+
: status.status === 'running' ? UI.COLORS.cyan : UI.COLORS.gray;
|
|
1001
|
+
const actualStatus = processStatus?.actualStatus || status.status;
|
|
1002
|
+
const isStale = processStatus?.isStale || false;
|
|
1003
|
+
process.stdout.write(`\n`);
|
|
1004
|
+
process.stdout.write(` ${UI.COLORS.dim}Status${UI.COLORS.reset} ${statusColor}${this.getStatusIcon(actualStatus)} ${actualStatus.toUpperCase()}${UI.COLORS.reset}`);
|
|
1005
|
+
if (isStale)
|
|
1006
|
+
process.stdout.write(` ${UI.COLORS.yellow}(stale)${UI.COLORS.reset}`);
|
|
1007
|
+
process.stdout.write(`\n`);
|
|
1008
|
+
const pidDisplay = processStatus?.pid
|
|
1009
|
+
? `${processStatus.processRunning ? UI.COLORS.green : UI.COLORS.red}${processStatus.pid}${UI.COLORS.reset}`
|
|
1010
|
+
: '-';
|
|
1011
|
+
process.stdout.write(` ${UI.COLORS.dim}PID${UI.COLORS.reset} ${pidDisplay}\n`);
|
|
1012
|
+
process.stdout.write(` ${UI.COLORS.dim}Progress${UI.COLORS.reset} ${status.currentTask}/${status.totalTasks} tasks (${status.progress})\n`);
|
|
1013
|
+
process.stdout.write(` ${UI.COLORS.dim}Duration${UI.COLORS.reset} ${this.formatDuration(processStatus?.duration || status.duration)}\n`);
|
|
1014
|
+
process.stdout.write(` ${UI.COLORS.dim}Branch${UI.COLORS.reset} ${status.pipelineBranch}\n`);
|
|
1015
|
+
if (status.dependsOn && status.dependsOn.length > 0) {
|
|
1016
|
+
process.stdout.write(` ${UI.COLORS.dim}Depends${UI.COLORS.reset} ${status.dependsOn.join(', ')}\n`);
|
|
1017
|
+
}
|
|
551
1018
|
if (status.waitingFor && status.waitingFor.length > 0) {
|
|
552
|
-
process.stdout.write(
|
|
1019
|
+
process.stdout.write(` ${UI.COLORS.yellow}Waiting${UI.COLORS.reset} ${status.waitingFor.join(', ')}\n`);
|
|
553
1020
|
}
|
|
554
1021
|
if (status.error) {
|
|
555
|
-
process.stdout.write(
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
1022
|
+
process.stdout.write(` ${UI.COLORS.red}Error${UI.COLORS.reset} ${status.error}\n`);
|
|
1023
|
+
}
|
|
1024
|
+
// Live terminal preview
|
|
1025
|
+
this.renderSectionTitle('Live Terminal', 'last 10 lines');
|
|
1026
|
+
const logPath = (0, path_1.safeJoin)(lane.path, 'terminal.log');
|
|
1027
|
+
if (fs.existsSync(logPath)) {
|
|
1028
|
+
const content = fs.readFileSync(logPath, 'utf8');
|
|
1029
|
+
const lines = content.split('\n').slice(-10);
|
|
1030
|
+
for (const line of lines) {
|
|
1031
|
+
const formatted = this.formatTerminalLine(line);
|
|
1032
|
+
process.stdout.write(` ${UI.COLORS.dim}${formatted.substring(0, this.screenWidth - 4)}${UI.COLORS.reset}\n`);
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
else {
|
|
1036
|
+
process.stdout.write(` ${UI.COLORS.dim}(No output yet)${UI.COLORS.reset}\n`);
|
|
1037
|
+
}
|
|
1038
|
+
// Conversation preview
|
|
1039
|
+
this.renderSectionTitle('Conversation', `${this.currentLogs.length} messages`);
|
|
1040
|
+
const maxVisible = 8;
|
|
1041
|
+
if (this.selectedMessageIndex < this.scrollOffset) {
|
|
1042
|
+
this.scrollOffset = this.selectedMessageIndex;
|
|
1043
|
+
}
|
|
1044
|
+
else if (this.selectedMessageIndex >= this.scrollOffset + maxVisible) {
|
|
1045
|
+
this.scrollOffset = this.selectedMessageIndex - maxVisible + 1;
|
|
1046
|
+
}
|
|
563
1047
|
if (this.currentLogs.length === 0) {
|
|
564
|
-
|
|
1048
|
+
process.stdout.write(` ${UI.COLORS.dim}(No messages yet)${UI.COLORS.reset}\n`);
|
|
565
1049
|
}
|
|
566
1050
|
else {
|
|
567
|
-
// Simple windowed view for long histories
|
|
568
|
-
const maxVisible = 15; // Number of messages to show
|
|
569
|
-
if (this.selectedMessageIndex < this.scrollOffset) {
|
|
570
|
-
this.scrollOffset = this.selectedMessageIndex;
|
|
571
|
-
}
|
|
572
|
-
else if (this.selectedMessageIndex >= this.scrollOffset + maxVisible) {
|
|
573
|
-
this.scrollOffset = this.selectedMessageIndex - maxVisible + 1;
|
|
574
|
-
}
|
|
575
1051
|
const visibleLogs = this.currentLogs.slice(this.scrollOffset, this.scrollOffset + maxVisible);
|
|
576
1052
|
visibleLogs.forEach((log, i) => {
|
|
577
1053
|
const actualIndex = i + this.scrollOffset;
|
|
578
1054
|
const isSelected = actualIndex === this.selectedMessageIndex;
|
|
579
|
-
const roleColor =
|
|
1055
|
+
const roleColor = this.getRoleColor(log.role);
|
|
580
1056
|
const role = log.role.toUpperCase().padEnd(10);
|
|
581
|
-
const
|
|
582
|
-
const
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
process.stdout.write(`${header}\n`);
|
|
588
|
-
}
|
|
589
|
-
const lines = log.fullText.split('\n').filter(l => l.trim());
|
|
590
|
-
const preview = lines[0]?.substring(0, 70) || '...';
|
|
591
|
-
process.stdout.write(` ${preview}${log.fullText.length > 70 ? '...' : ''}\n\n`);
|
|
1057
|
+
const ts = new Date(log.timestamp).toLocaleTimeString('en-US', { hour12: false });
|
|
1058
|
+
const prefix = isSelected ? `${UI.COLORS.cyan}▶${UI.COLORS.reset}` : ' ';
|
|
1059
|
+
const bg = isSelected ? UI.COLORS.bgGray : '';
|
|
1060
|
+
const reset = isSelected ? UI.COLORS.reset : '';
|
|
1061
|
+
const preview = log.fullText.replace(/\n/g, ' ').substring(0, 60);
|
|
1062
|
+
process.stdout.write(`${bg}${prefix} ${roleColor}${role}${UI.COLORS.reset} ${UI.COLORS.dim}${ts}${UI.COLORS.reset} ${preview}...${reset}\n`);
|
|
592
1063
|
});
|
|
593
1064
|
if (this.currentLogs.length > maxVisible) {
|
|
594
|
-
|
|
1065
|
+
process.stdout.write(` ${UI.COLORS.dim}(${this.currentLogs.length - maxVisible} more messages)${UI.COLORS.reset}\n`);
|
|
595
1066
|
}
|
|
596
1067
|
}
|
|
597
|
-
|
|
1068
|
+
this.renderFooter([
|
|
1069
|
+
'[↑↓] Scroll', '[→/Enter] Full Msg', '[T] Terminal', '[I] Intervene', '[K] Kill', '[←/Esc] Back'
|
|
1070
|
+
]);
|
|
1071
|
+
}
|
|
1072
|
+
getRoleColor(role) {
|
|
1073
|
+
const colors = {
|
|
1074
|
+
user: UI.COLORS.yellow,
|
|
1075
|
+
assistant: UI.COLORS.green,
|
|
1076
|
+
reviewer: UI.COLORS.magenta,
|
|
1077
|
+
intervention: UI.COLORS.red,
|
|
1078
|
+
system: UI.COLORS.cyan,
|
|
1079
|
+
};
|
|
1080
|
+
return colors[role] || UI.COLORS.gray;
|
|
598
1081
|
}
|
|
599
1082
|
renderMessageDetail() {
|
|
600
1083
|
const log = this.currentLogs[this.selectedMessageIndex];
|
|
@@ -603,51 +1086,143 @@ class InteractiveMonitor {
|
|
|
603
1086
|
this.render();
|
|
604
1087
|
return;
|
|
605
1088
|
}
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
process.stdout.write(
|
|
1089
|
+
this.renderHeader('Message Detail', [path.basename(this.runDir), this.selectedLaneName || '', log.role.toUpperCase()]);
|
|
1090
|
+
const roleColor = this.getRoleColor(log.role);
|
|
1091
|
+
const ts = new Date(log.timestamp).toLocaleString();
|
|
1092
|
+
process.stdout.write(`\n`);
|
|
1093
|
+
process.stdout.write(` ${UI.COLORS.dim}Role${UI.COLORS.reset} ${roleColor}${log.role.toUpperCase()}${UI.COLORS.reset}\n`);
|
|
1094
|
+
process.stdout.write(` ${UI.COLORS.dim}Time${UI.COLORS.reset} ${ts}\n`);
|
|
612
1095
|
if (log.model)
|
|
613
|
-
process.stdout.write(`
|
|
1096
|
+
process.stdout.write(` ${UI.COLORS.dim}Model${UI.COLORS.reset} ${log.model}\n`);
|
|
614
1097
|
if (log.task)
|
|
615
|
-
process.stdout.write(`
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
1098
|
+
process.stdout.write(` ${UI.COLORS.dim}Task${UI.COLORS.reset} ${log.task}\n`);
|
|
1099
|
+
this.renderSectionTitle('Content');
|
|
1100
|
+
// Display message content with wrapping
|
|
1101
|
+
const maxWidth = this.screenWidth - 4;
|
|
1102
|
+
const lines = log.fullText.split('\n');
|
|
1103
|
+
const maxLines = this.screenHeight - 16;
|
|
1104
|
+
let lineCount = 0;
|
|
1105
|
+
for (const line of lines) {
|
|
1106
|
+
if (lineCount >= maxLines) {
|
|
1107
|
+
process.stdout.write(` ${UI.COLORS.dim}... (truncated, ${lines.length - lineCount} more lines)${UI.COLORS.reset}\n`);
|
|
1108
|
+
break;
|
|
1109
|
+
}
|
|
1110
|
+
// Word wrap long lines
|
|
1111
|
+
if (line.length > maxWidth) {
|
|
1112
|
+
const wrapped = this.wrapText(line, maxWidth);
|
|
1113
|
+
for (const wl of wrapped) {
|
|
1114
|
+
if (lineCount >= maxLines)
|
|
1115
|
+
break;
|
|
1116
|
+
process.stdout.write(` ${wl}\n`);
|
|
1117
|
+
lineCount++;
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
else {
|
|
1121
|
+
process.stdout.write(` ${line}\n`);
|
|
1122
|
+
lineCount++;
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
this.renderFooter(['[←/Esc] Back']);
|
|
1126
|
+
}
|
|
1127
|
+
/**
|
|
1128
|
+
* Wrap text to specified width
|
|
1129
|
+
*/
|
|
1130
|
+
wrapText(text, maxWidth) {
|
|
1131
|
+
const words = text.split(' ');
|
|
1132
|
+
const lines = [];
|
|
1133
|
+
let currentLine = '';
|
|
1134
|
+
for (const word of words) {
|
|
1135
|
+
if (currentLine.length + word.length + 1 <= maxWidth) {
|
|
1136
|
+
currentLine += (currentLine ? ' ' : '') + word;
|
|
1137
|
+
}
|
|
1138
|
+
else {
|
|
1139
|
+
if (currentLine)
|
|
1140
|
+
lines.push(currentLine);
|
|
1141
|
+
currentLine = word;
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
if (currentLine)
|
|
1145
|
+
lines.push(currentLine);
|
|
1146
|
+
return lines;
|
|
619
1147
|
}
|
|
620
1148
|
renderFlow() {
|
|
621
|
-
|
|
622
|
-
console.log(`⛓️ Task Dependency Flow`);
|
|
623
|
-
console.log(`🕒 Updated: ${new Date().toLocaleTimeString()} | [→/Enter/Esc] Back to List`);
|
|
624
|
-
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
|
|
1149
|
+
this.renderHeader('Dependency Flow', [path.basename(this.runDir), 'Flow']);
|
|
625
1150
|
const laneMap = new Map();
|
|
626
1151
|
this.lanes.forEach(lane => {
|
|
627
1152
|
laneMap.set(lane.name, this.getLaneStatus(lane.path, lane.name));
|
|
628
1153
|
});
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
1154
|
+
process.stdout.write('\n');
|
|
1155
|
+
// Group lanes by dependency level
|
|
1156
|
+
const levels = this.calculateDependencyLevels();
|
|
1157
|
+
const maxLevelWidth = Math.max(...levels.map(l => l.length));
|
|
1158
|
+
for (let level = 0; level < levels.length; level++) {
|
|
1159
|
+
const lanesAtLevel = levels[level];
|
|
1160
|
+
// Level header
|
|
1161
|
+
process.stdout.write(` ${UI.COLORS.dim}Level ${level}${UI.COLORS.reset}\n`);
|
|
1162
|
+
for (const laneName of lanesAtLevel) {
|
|
1163
|
+
const status = laneMap.get(laneName);
|
|
1164
|
+
const statusIcon = this.getStatusIcon(status?.status || 'pending');
|
|
1165
|
+
let statusColor = UI.COLORS.gray;
|
|
1166
|
+
if (status?.status === 'completed')
|
|
1167
|
+
statusColor = UI.COLORS.green;
|
|
1168
|
+
else if (status?.status === 'running')
|
|
1169
|
+
statusColor = UI.COLORS.cyan;
|
|
1170
|
+
else if (status?.status === 'failed')
|
|
1171
|
+
statusColor = UI.COLORS.red;
|
|
1172
|
+
// Render the node
|
|
1173
|
+
const nodeText = `${statusIcon} ${laneName}`;
|
|
1174
|
+
process.stdout.write(` ${statusColor}${nodeText.padEnd(20)}${UI.COLORS.reset}`);
|
|
1175
|
+
// Render dependencies
|
|
1176
|
+
if (status?.dependsOn?.length > 0) {
|
|
1177
|
+
process.stdout.write(` ${UI.COLORS.dim}←${UI.COLORS.reset} ${UI.COLORS.yellow}${status.dependsOn.join(', ')}${UI.COLORS.reset}`);
|
|
1178
|
+
}
|
|
1179
|
+
process.stdout.write('\n');
|
|
1180
|
+
}
|
|
1181
|
+
if (level < levels.length - 1) {
|
|
1182
|
+
process.stdout.write(` ${UI.COLORS.dim}│${UI.COLORS.reset}\n`);
|
|
1183
|
+
process.stdout.write(` ${UI.COLORS.dim}▼${UI.COLORS.reset}\n`);
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
process.stdout.write(`\n ${UI.COLORS.dim}Lanes wait for dependencies to complete before starting${UI.COLORS.reset}\n`);
|
|
1187
|
+
this.renderFooter(['[←/Esc] Back']);
|
|
1188
|
+
}
|
|
1189
|
+
/**
|
|
1190
|
+
* Calculate dependency levels for visualization
|
|
1191
|
+
*/
|
|
1192
|
+
calculateDependencyLevels() {
|
|
1193
|
+
const levels = [];
|
|
1194
|
+
const assigned = new Set();
|
|
1195
|
+
// First, find lanes with no dependencies
|
|
1196
|
+
const noDeps = this.lanes.filter(l => !l.dependsOn || l.dependsOn.length === 0);
|
|
1197
|
+
if (noDeps.length > 0) {
|
|
1198
|
+
levels.push(noDeps.map(l => l.name));
|
|
1199
|
+
noDeps.forEach(l => assigned.add(l.name));
|
|
1200
|
+
}
|
|
1201
|
+
// Then assign remaining lanes by dependency completion
|
|
1202
|
+
let maxIterations = 10;
|
|
1203
|
+
while (assigned.size < this.lanes.length && maxIterations-- > 0) {
|
|
1204
|
+
const nextLevel = [];
|
|
1205
|
+
for (const lane of this.lanes) {
|
|
1206
|
+
if (assigned.has(lane.name))
|
|
1207
|
+
continue;
|
|
1208
|
+
// Check if all dependencies are assigned
|
|
1209
|
+
const allDepsAssigned = lane.dependsOn.every(d => assigned.has(d));
|
|
1210
|
+
if (allDepsAssigned) {
|
|
1211
|
+
nextLevel.push(lane.name);
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
if (nextLevel.length === 0) {
|
|
1215
|
+
// Remaining lanes have circular deps or missing deps
|
|
1216
|
+
const remaining = this.lanes.filter(l => !assigned.has(l.name)).map(l => l.name);
|
|
1217
|
+
if (remaining.length > 0) {
|
|
1218
|
+
levels.push(remaining);
|
|
1219
|
+
}
|
|
1220
|
+
break;
|
|
1221
|
+
}
|
|
1222
|
+
levels.push(nextLevel);
|
|
1223
|
+
nextLevel.forEach(n => assigned.add(n));
|
|
1224
|
+
}
|
|
1225
|
+
return levels;
|
|
651
1226
|
}
|
|
652
1227
|
renderTerminal() {
|
|
653
1228
|
const lane = this.lanes.find(l => l.name === this.selectedLaneName);
|
|
@@ -656,18 +1231,35 @@ class InteractiveMonitor {
|
|
|
656
1231
|
this.render();
|
|
657
1232
|
return;
|
|
658
1233
|
}
|
|
659
|
-
|
|
1234
|
+
this.renderHeader('Live Terminal', [path.basename(this.runDir), lane.name, 'Terminal']);
|
|
1235
|
+
// Get logs based on format mode
|
|
660
1236
|
let logLines = [];
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
1237
|
+
let totalLines = 0;
|
|
1238
|
+
if (this.readableFormat) {
|
|
1239
|
+
// Use JSONL for readable format
|
|
1240
|
+
const jsonlPath = (0, path_1.safeJoin)(lane.path, 'terminal.jsonl');
|
|
1241
|
+
logLines = this.getReadableLogLines(jsonlPath, lane.name);
|
|
1242
|
+
totalLines = logLines.length;
|
|
664
1243
|
}
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
1244
|
+
else {
|
|
1245
|
+
// Use raw log
|
|
1246
|
+
const logPath = (0, path_1.safeJoin)(lane.path, 'terminal.log');
|
|
1247
|
+
if (fs.existsSync(logPath)) {
|
|
1248
|
+
const content = fs.readFileSync(logPath, 'utf8');
|
|
1249
|
+
logLines = content.split('\n');
|
|
1250
|
+
totalLines = logLines.length;
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
const maxVisible = this.screenHeight - 10;
|
|
1254
|
+
// Follow mode logic
|
|
1255
|
+
if (this.followMode) {
|
|
1256
|
+
this.terminalScrollOffset = 0;
|
|
1257
|
+
}
|
|
1258
|
+
else {
|
|
1259
|
+
if (this.lastTerminalTotalLines > 0 && totalLines > this.lastTerminalTotalLines) {
|
|
1260
|
+
this.unseenLineCount += (totalLines - this.lastTerminalTotalLines);
|
|
1261
|
+
this.terminalScrollOffset += (totalLines - this.lastTerminalTotalLines);
|
|
1262
|
+
}
|
|
671
1263
|
}
|
|
672
1264
|
this.lastTerminalTotalLines = totalLines;
|
|
673
1265
|
// Clamp scroll offset
|
|
@@ -675,70 +1267,298 @@ class InteractiveMonitor {
|
|
|
675
1267
|
if (this.terminalScrollOffset > maxScroll) {
|
|
676
1268
|
this.terminalScrollOffset = maxScroll;
|
|
677
1269
|
}
|
|
1270
|
+
// Mode and status indicators
|
|
1271
|
+
const formatMode = this.readableFormat
|
|
1272
|
+
? `${UI.COLORS.green}[R] Readable ✓${UI.COLORS.reset}`
|
|
1273
|
+
: `${UI.COLORS.dim}[R] Raw${UI.COLORS.reset}`;
|
|
1274
|
+
const followStatus = this.followMode
|
|
1275
|
+
? `${UI.COLORS.green}[F] Follow ✓${UI.COLORS.reset}`
|
|
1276
|
+
: `${UI.COLORS.yellow}[F] Follow OFF${this.unseenLineCount > 0 ? ` (↓${this.unseenLineCount})` : ''}${UI.COLORS.reset}`;
|
|
1277
|
+
process.stdout.write(` ${formatMode} ${followStatus} ${UI.COLORS.dim}Lines: ${totalLines}${UI.COLORS.reset}\n\n`);
|
|
678
1278
|
// Slice based on scroll (0 means bottom, >0 means scrolled up)
|
|
679
1279
|
const end = totalLines - this.terminalScrollOffset;
|
|
680
1280
|
const start = Math.max(0, end - maxVisible);
|
|
681
1281
|
const visibleLines = logLines.slice(start, end);
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
1282
|
+
for (const line of visibleLines) {
|
|
1283
|
+
const formatted = this.readableFormat ? line : this.formatTerminalLine(line);
|
|
1284
|
+
// Truncate to screen width
|
|
1285
|
+
const displayLine = formatted.length > this.screenWidth - 2
|
|
1286
|
+
? formatted.substring(0, this.screenWidth - 5) + '...'
|
|
1287
|
+
: formatted;
|
|
1288
|
+
process.stdout.write(` ${displayLine}\n`);
|
|
1289
|
+
}
|
|
1290
|
+
if (visibleLines.length === 0) {
|
|
1291
|
+
process.stdout.write(` ${UI.COLORS.dim}(No output yet)${UI.COLORS.reset}\n`);
|
|
1292
|
+
}
|
|
1293
|
+
this.renderFooter([
|
|
1294
|
+
'[↑↓] Scroll', '[F] Follow', '[R] Toggle Readable', '[I] Intervene', '[←/Esc] Back'
|
|
1295
|
+
]);
|
|
1296
|
+
}
|
|
1297
|
+
/**
|
|
1298
|
+
* Format a raw terminal line with syntax highlighting
|
|
1299
|
+
*/
|
|
1300
|
+
formatTerminalLine(line) {
|
|
1301
|
+
// Highlight patterns
|
|
1302
|
+
if (line.includes('[HUMAN INTERVENTION]') || line.includes('Injecting intervention:')) {
|
|
1303
|
+
return `${UI.COLORS.yellow}${UI.COLORS.bold}${line}${UI.COLORS.reset}`;
|
|
1304
|
+
}
|
|
1305
|
+
if (line.includes('Executing cursor-agent')) {
|
|
1306
|
+
return `${UI.COLORS.cyan}${UI.COLORS.bold}${line}${UI.COLORS.reset}`;
|
|
1307
|
+
}
|
|
1308
|
+
if (line.includes('=== Task:') || line.includes('Starting task:')) {
|
|
1309
|
+
return `${UI.COLORS.green}${UI.COLORS.bold}${line}${UI.COLORS.reset}`;
|
|
1310
|
+
}
|
|
1311
|
+
if (line.toLowerCase().includes('error') || line.toLowerCase().includes('failed')) {
|
|
1312
|
+
return `${UI.COLORS.red}${line}${UI.COLORS.reset}`;
|
|
1313
|
+
}
|
|
1314
|
+
if (line.toLowerCase().includes('success') || line.toLowerCase().includes('completed')) {
|
|
1315
|
+
return `${UI.COLORS.green}${line}${UI.COLORS.reset}`;
|
|
1316
|
+
}
|
|
1317
|
+
return line;
|
|
1318
|
+
}
|
|
1319
|
+
/**
|
|
1320
|
+
* Get readable log lines from JSONL file
|
|
1321
|
+
*/
|
|
1322
|
+
getReadableLogLines(jsonlPath, laneName) {
|
|
1323
|
+
if (!fs.existsSync(jsonlPath)) {
|
|
1324
|
+
// Fallback: try to read raw log
|
|
1325
|
+
const rawPath = jsonlPath.replace('.jsonl', '.log');
|
|
1326
|
+
if (fs.existsSync(rawPath)) {
|
|
1327
|
+
return fs.readFileSync(rawPath, 'utf8').split('\n').map(l => this.formatTerminalLine(l));
|
|
1328
|
+
}
|
|
1329
|
+
return [];
|
|
1330
|
+
}
|
|
1331
|
+
try {
|
|
1332
|
+
const content = fs.readFileSync(jsonlPath, 'utf8');
|
|
1333
|
+
const lines = content.split('\n').filter(l => l.trim());
|
|
1334
|
+
return lines.map(line => {
|
|
1335
|
+
try {
|
|
1336
|
+
const entry = JSON.parse(line);
|
|
1337
|
+
const ts = new Date(entry.timestamp || Date.now()).toLocaleTimeString('en-US', { hour12: false });
|
|
1338
|
+
const type = (entry.type || 'info').toLowerCase();
|
|
1339
|
+
const content = entry.content || entry.message || '';
|
|
1340
|
+
// Format based on type
|
|
1341
|
+
const typeInfo = this.getLogTypeInfo(type);
|
|
1342
|
+
const preview = content.replace(/\n/g, ' ').substring(0, 100);
|
|
1343
|
+
return `${UI.COLORS.dim}[${ts}]${UI.COLORS.reset} ${typeInfo.color}[${typeInfo.label}]${UI.COLORS.reset} ${preview}`;
|
|
1344
|
+
}
|
|
1345
|
+
catch {
|
|
1346
|
+
return this.formatTerminalLine(line);
|
|
1347
|
+
}
|
|
1348
|
+
});
|
|
1349
|
+
}
|
|
1350
|
+
catch {
|
|
1351
|
+
return [];
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
/**
|
|
1355
|
+
* Get log type display info
|
|
1356
|
+
*/
|
|
1357
|
+
getLogTypeInfo(type) {
|
|
1358
|
+
const typeMap = {
|
|
1359
|
+
user: { label: 'USER ', color: UI.COLORS.cyan },
|
|
1360
|
+
assistant: { label: 'ASST ', color: UI.COLORS.green },
|
|
1361
|
+
tool: { label: 'TOOL ', color: UI.COLORS.yellow },
|
|
1362
|
+
tool_result: { label: 'RESULT', color: UI.COLORS.gray },
|
|
1363
|
+
result: { label: 'DONE ', color: UI.COLORS.green },
|
|
1364
|
+
system: { label: 'SYSTEM', color: UI.COLORS.gray },
|
|
1365
|
+
thinking: { label: 'THINK ', color: UI.COLORS.dim },
|
|
1366
|
+
error: { label: 'ERROR ', color: UI.COLORS.red },
|
|
1367
|
+
stderr: { label: 'STDERR', color: UI.COLORS.red },
|
|
1368
|
+
stdout: { label: 'STDOUT', color: UI.COLORS.white },
|
|
1369
|
+
};
|
|
1370
|
+
return typeMap[type] || { label: type.toUpperCase().padEnd(6).substring(0, 6), color: UI.COLORS.gray };
|
|
707
1371
|
}
|
|
708
1372
|
renderIntervene() {
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
1373
|
+
this.renderHeader('Human Intervention', [path.basename(this.runDir), this.selectedLaneName || '', 'Intervene']);
|
|
1374
|
+
process.stdout.write(`\n`);
|
|
1375
|
+
process.stdout.write(` ${UI.COLORS.yellow}Send a message directly to the agent.${UI.COLORS.reset}\n`);
|
|
1376
|
+
process.stdout.write(` ${UI.COLORS.dim}This will interrupt the current flow and inject your instruction.${UI.COLORS.reset}\n\n`);
|
|
1377
|
+
// Input box
|
|
1378
|
+
const width = Math.min(this.screenWidth - 8, 80);
|
|
1379
|
+
process.stdout.write(` ${UI.COLORS.cyan}┌${'─'.repeat(width)}┐${UI.COLORS.reset}\n`);
|
|
1380
|
+
// Wrap input text
|
|
1381
|
+
const inputLines = this.wrapText(this.interventionInput || ' ', width - 4);
|
|
1382
|
+
for (const line of inputLines) {
|
|
1383
|
+
process.stdout.write(` ${UI.COLORS.cyan}│${UI.COLORS.reset} ${line.padEnd(width - 2)} ${UI.COLORS.cyan}│${UI.COLORS.reset}\n`);
|
|
1384
|
+
}
|
|
1385
|
+
if (inputLines.length === 0 || inputLines[inputLines.length - 1] === ' ') {
|
|
1386
|
+
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`);
|
|
1387
|
+
}
|
|
1388
|
+
process.stdout.write(` ${UI.COLORS.cyan}└${'─'.repeat(width)}┘${UI.COLORS.reset}\n`);
|
|
1389
|
+
this.renderFooter(['[Enter] Send', '[Esc] Cancel']);
|
|
717
1390
|
}
|
|
718
1391
|
renderTimeout() {
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
1392
|
+
this.renderHeader('Update Timeout', [path.basename(this.runDir), this.selectedLaneName || '', 'Timeout']);
|
|
1393
|
+
process.stdout.write(`\n`);
|
|
1394
|
+
process.stdout.write(` ${UI.COLORS.yellow}Update the task timeout for this lane.${UI.COLORS.reset}\n`);
|
|
1395
|
+
process.stdout.write(` ${UI.COLORS.dim}Enter timeout in milliseconds (e.g., 600000 = 10 minutes)${UI.COLORS.reset}\n\n`);
|
|
1396
|
+
// Common presets
|
|
1397
|
+
process.stdout.write(` ${UI.COLORS.dim}Presets: 300000 (5m) | 600000 (10m) | 1800000 (30m) | 3600000 (1h)${UI.COLORS.reset}\n\n`);
|
|
1398
|
+
// Input box
|
|
1399
|
+
const width = 40;
|
|
1400
|
+
process.stdout.write(` ${UI.COLORS.cyan}┌${'─'.repeat(width)}┐${UI.COLORS.reset}\n`);
|
|
1401
|
+
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`);
|
|
1402
|
+
process.stdout.write(` ${UI.COLORS.cyan}└${'─'.repeat(width)}┘${UI.COLORS.reset}\n`);
|
|
1403
|
+
// Show human-readable interpretation
|
|
1404
|
+
if (this.timeoutInput) {
|
|
1405
|
+
const ms = parseInt(this.timeoutInput);
|
|
1406
|
+
if (!isNaN(ms) && ms > 0) {
|
|
1407
|
+
const formatted = this.formatDuration(ms);
|
|
1408
|
+
process.stdout.write(`\n ${UI.COLORS.green}= ${formatted}${UI.COLORS.reset}\n`);
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
this.renderFooter(['[Enter] Apply', '[Esc] Cancel']);
|
|
1412
|
+
}
|
|
1413
|
+
/**
|
|
1414
|
+
* Render unified log view - all lanes combined
|
|
1415
|
+
*/
|
|
1416
|
+
renderUnifiedLog() {
|
|
1417
|
+
this.renderHeader('Unified Logs', [path.basename(this.runDir), 'All Lanes']);
|
|
1418
|
+
const bufferState = this.unifiedLogBuffer?.getState();
|
|
1419
|
+
const totalEntries = bufferState?.totalEntries || 0;
|
|
1420
|
+
const availableLanes = bufferState?.lanes || [];
|
|
1421
|
+
// Status bar
|
|
1422
|
+
const formatMode = this.readableFormat
|
|
1423
|
+
? `${UI.COLORS.green}[R] Readable ✓${UI.COLORS.reset}`
|
|
1424
|
+
: `${UI.COLORS.dim}[R] Compact${UI.COLORS.reset}`;
|
|
1425
|
+
const followStatus = this.unifiedLogFollowMode
|
|
1426
|
+
? `${UI.COLORS.green}[F] Follow ✓${UI.COLORS.reset}`
|
|
1427
|
+
: `${UI.COLORS.yellow}[F] Follow OFF${UI.COLORS.reset}`;
|
|
1428
|
+
const filterStatus = this.laneFilter
|
|
1429
|
+
? `${UI.COLORS.cyan}[L] ${this.laneFilter}${UI.COLORS.reset}`
|
|
1430
|
+
: `${UI.COLORS.dim}[L] All Lanes${UI.COLORS.reset}`;
|
|
1431
|
+
process.stdout.write(` ${formatMode} ${followStatus} ${filterStatus} ${UI.COLORS.dim}Total: ${totalEntries}${UI.COLORS.reset}\n`);
|
|
1432
|
+
// Lane list for filtering hint
|
|
1433
|
+
if (availableLanes.length > 1) {
|
|
1434
|
+
process.stdout.write(` ${UI.COLORS.dim}Lanes: ${availableLanes.join(', ')}${UI.COLORS.reset}\n`);
|
|
1435
|
+
}
|
|
1436
|
+
process.stdout.write('\n');
|
|
1437
|
+
if (!this.unifiedLogBuffer) {
|
|
1438
|
+
process.stdout.write(` ${UI.COLORS.dim}(No log buffer available)${UI.COLORS.reset}\n`);
|
|
1439
|
+
this.renderFooter(['[U/Esc] Back', '[Q] Quit']);
|
|
1440
|
+
return;
|
|
1441
|
+
}
|
|
1442
|
+
const pageSize = this.screenHeight - 12;
|
|
1443
|
+
const filter = this.laneFilter ? { lane: this.laneFilter } : undefined;
|
|
1444
|
+
const entries = this.unifiedLogBuffer.getEntries({
|
|
1445
|
+
offset: this.unifiedLogScrollOffset,
|
|
1446
|
+
limit: pageSize,
|
|
1447
|
+
filter,
|
|
1448
|
+
fromEnd: true,
|
|
1449
|
+
});
|
|
1450
|
+
if (entries.length === 0) {
|
|
1451
|
+
process.stdout.write(` ${UI.COLORS.dim}(No log entries yet)${UI.COLORS.reset}\n`);
|
|
1452
|
+
}
|
|
1453
|
+
else {
|
|
1454
|
+
for (const entry of entries) {
|
|
1455
|
+
const formatted = this.formatUnifiedLogEntry(entry);
|
|
1456
|
+
const displayLine = formatted.length > this.screenWidth - 2
|
|
1457
|
+
? formatted.substring(0, this.screenWidth - 5) + '...'
|
|
1458
|
+
: formatted;
|
|
1459
|
+
process.stdout.write(` ${displayLine}\n`);
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
this.renderFooter([
|
|
1463
|
+
'[↑↓/PgUp/PgDn] Scroll', '[F] Follow', '[R] Readable', '[L] Filter Lane', '[U/Esc] Back'
|
|
1464
|
+
]);
|
|
1465
|
+
}
|
|
1466
|
+
/**
|
|
1467
|
+
* Format a unified log entry
|
|
1468
|
+
*/
|
|
1469
|
+
formatUnifiedLogEntry(entry) {
|
|
1470
|
+
const ts = entry.timestamp.toLocaleTimeString('en-US', { hour12: false });
|
|
1471
|
+
const lane = entry.laneName.padEnd(12);
|
|
1472
|
+
const typeInfo = this.getLogTypeInfo(entry.type || 'info');
|
|
1473
|
+
if (this.readableFormat) {
|
|
1474
|
+
// Readable format: show more context
|
|
1475
|
+
const content = entry.message.replace(/\n/g, ' ');
|
|
1476
|
+
return `${UI.COLORS.dim}[${ts}]${UI.COLORS.reset} ${entry.laneColor}[${lane}]${UI.COLORS.reset} ${typeInfo.color}[${typeInfo.label}]${UI.COLORS.reset} ${content}`;
|
|
1477
|
+
}
|
|
1478
|
+
else {
|
|
1479
|
+
// Compact format
|
|
1480
|
+
const preview = entry.message.replace(/\n/g, ' ').substring(0, 60);
|
|
1481
|
+
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}`;
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
/**
|
|
1485
|
+
* Render multiple flows dashboard
|
|
1486
|
+
*/
|
|
1487
|
+
renderFlowsDashboard() {
|
|
1488
|
+
this.renderHeader('All Flows', ['Flows Dashboard']);
|
|
1489
|
+
process.stdout.write(` ${UI.COLORS.dim}Total: ${this.allFlows.length} flows${UI.COLORS.reset}\n\n`);
|
|
1490
|
+
if (this.allFlows.length === 0) {
|
|
1491
|
+
process.stdout.write(` ${UI.COLORS.dim}No flow runs found.${UI.COLORS.reset}\n\n`);
|
|
1492
|
+
process.stdout.write(` Run ${UI.COLORS.cyan}cursorflow run${UI.COLORS.reset} to start a new flow.\n`);
|
|
1493
|
+
this.renderFooter(['[M/Esc] Back', '[Q] Quit']);
|
|
1494
|
+
return;
|
|
1495
|
+
}
|
|
1496
|
+
// Header
|
|
1497
|
+
process.stdout.write(` ${'Status'.padEnd(8)} ${'Run ID'.padEnd(32)} ${'Lanes'.padEnd(12)} Progress\n`);
|
|
1498
|
+
process.stdout.write(` ${'─'.repeat(8)} ${'─'.repeat(32)} ${'─'.repeat(12)} ${'─'.repeat(20)}\n`);
|
|
1499
|
+
const maxVisible = this.screenHeight - 14;
|
|
1500
|
+
const startIdx = Math.max(0, this.selectedFlowIndex - Math.floor(maxVisible / 2));
|
|
1501
|
+
const endIdx = Math.min(this.allFlows.length, startIdx + maxVisible);
|
|
1502
|
+
for (let i = startIdx; i < endIdx; i++) {
|
|
1503
|
+
const flow = this.allFlows[i];
|
|
1504
|
+
const isSelected = i === this.selectedFlowIndex;
|
|
1505
|
+
const isCurrent = flow.runDir === this.runDir;
|
|
1506
|
+
// Status icon based on flow state
|
|
1507
|
+
let statusIcon = '⚪';
|
|
1508
|
+
if (flow.isAlive) {
|
|
1509
|
+
statusIcon = '🟢';
|
|
1510
|
+
}
|
|
1511
|
+
else if (flow.summary.completed === flow.summary.total && flow.summary.total > 0) {
|
|
1512
|
+
statusIcon = '✅';
|
|
1513
|
+
}
|
|
1514
|
+
else if (flow.summary.failed > 0 || flow.summary.dead > 0) {
|
|
1515
|
+
statusIcon = '🔴';
|
|
1516
|
+
}
|
|
1517
|
+
// Lanes summary
|
|
1518
|
+
const lanesSummary = [
|
|
1519
|
+
flow.summary.running > 0 ? `${UI.COLORS.cyan}${flow.summary.running}R${UI.COLORS.reset}` : '',
|
|
1520
|
+
flow.summary.completed > 0 ? `${UI.COLORS.green}${flow.summary.completed}C${UI.COLORS.reset}` : '',
|
|
1521
|
+
flow.summary.failed > 0 ? `${UI.COLORS.red}${flow.summary.failed}F${UI.COLORS.reset}` : '',
|
|
1522
|
+
flow.summary.dead > 0 ? `${UI.COLORS.yellow}${flow.summary.dead}D${UI.COLORS.reset}` : '',
|
|
1523
|
+
].filter(Boolean).join('/') || '0';
|
|
1524
|
+
// Progress bar
|
|
1525
|
+
const total = flow.summary.total || 1;
|
|
1526
|
+
const completed = flow.summary.completed;
|
|
1527
|
+
const ratio = completed / total;
|
|
1528
|
+
const barWidth = 12;
|
|
1529
|
+
const filled = Math.round(ratio * barWidth);
|
|
1530
|
+
const progressBar = `${UI.COLORS.green}${'█'.repeat(filled)}${UI.COLORS.reset}${UI.COLORS.gray}${'░'.repeat(barWidth - filled)}${UI.COLORS.reset}`;
|
|
1531
|
+
const pct = `${Math.round(ratio * 100)}%`;
|
|
1532
|
+
// Display
|
|
1533
|
+
const prefix = isSelected ? ` ${UI.COLORS.cyan}▶${UI.COLORS.reset} ` : ' ';
|
|
1534
|
+
const currentTag = isCurrent ? ` ${UI.COLORS.cyan}●${UI.COLORS.reset}` : '';
|
|
1535
|
+
const bg = isSelected ? UI.COLORS.bgGray : '';
|
|
1536
|
+
const resetBg = isSelected ? UI.COLORS.reset : '';
|
|
1537
|
+
// Truncate run ID if needed
|
|
1538
|
+
const runIdDisplay = flow.runId.length > 30 ? flow.runId.substring(0, 27) + '...' : flow.runId.padEnd(30);
|
|
1539
|
+
process.stdout.write(`${bg}${prefix}${statusIcon} ${runIdDisplay} ${lanesSummary.padEnd(12 + 30)} ${progressBar} ${pct}${currentTag}${resetBg}\n`);
|
|
1540
|
+
}
|
|
1541
|
+
if (this.allFlows.length > maxVisible) {
|
|
1542
|
+
process.stdout.write(`\n ${UI.COLORS.dim}(${this.allFlows.length - maxVisible} more flows, scroll to see)${UI.COLORS.reset}\n`);
|
|
1543
|
+
}
|
|
1544
|
+
this.renderFooter([
|
|
1545
|
+
'[↑↓] Select', '[→/Enter] Switch', '[D] Delete', '[R] Refresh', '[M/Esc] Back', '[Q] Quit'
|
|
1546
|
+
]);
|
|
727
1547
|
}
|
|
728
1548
|
listLanesWithDeps(runDir) {
|
|
729
|
-
const lanesDir =
|
|
1549
|
+
const lanesDir = (0, path_1.safeJoin)(runDir, 'lanes');
|
|
730
1550
|
if (!fs.existsSync(lanesDir))
|
|
731
1551
|
return [];
|
|
732
1552
|
const config = (0, config_1.loadConfig)();
|
|
733
|
-
const tasksDir =
|
|
1553
|
+
const tasksDir = (0, path_1.safeJoin)(config.projectRoot, config.tasksDir);
|
|
734
1554
|
const laneConfigs = this.listLaneFilesFromDir(tasksDir);
|
|
735
1555
|
return fs.readdirSync(lanesDir)
|
|
736
|
-
.filter(d => fs.statSync(
|
|
1556
|
+
.filter(d => fs.statSync((0, path_1.safeJoin)(lanesDir, d)).isDirectory())
|
|
737
1557
|
.map(name => {
|
|
738
1558
|
const config = laneConfigs.find(c => c.name === name);
|
|
739
1559
|
return {
|
|
740
1560
|
name,
|
|
741
|
-
path:
|
|
1561
|
+
path: (0, path_1.safeJoin)(lanesDir, name),
|
|
742
1562
|
dependsOn: config?.dependsOn || [],
|
|
743
1563
|
};
|
|
744
1564
|
});
|
|
@@ -749,7 +1569,7 @@ class InteractiveMonitor {
|
|
|
749
1569
|
return fs.readdirSync(tasksDir)
|
|
750
1570
|
.filter(f => f.endsWith('.json'))
|
|
751
1571
|
.map(f => {
|
|
752
|
-
const filePath =
|
|
1572
|
+
const filePath = (0, path_1.safeJoin)(tasksDir, f);
|
|
753
1573
|
try {
|
|
754
1574
|
const config = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
755
1575
|
return { name: path.basename(f, '.json'), dependsOn: config.dependsOn || [] };
|
|
@@ -760,7 +1580,7 @@ class InteractiveMonitor {
|
|
|
760
1580
|
});
|
|
761
1581
|
}
|
|
762
1582
|
getLaneStatus(lanePath, laneName) {
|
|
763
|
-
const statePath =
|
|
1583
|
+
const statePath = (0, path_1.safeJoin)(lanePath, 'state.json');
|
|
764
1584
|
const state = (0, state_1.loadState)(statePath);
|
|
765
1585
|
const laneInfo = this.lanes.find(l => l.name === laneName);
|
|
766
1586
|
const dependsOn = state?.dependsOn || laneInfo?.dependsOn || [];
|
|
@@ -814,12 +1634,12 @@ class InteractiveMonitor {
|
|
|
814
1634
|
* Find the latest run directory
|
|
815
1635
|
*/
|
|
816
1636
|
function findLatestRunDir(logsDir) {
|
|
817
|
-
const runsDir =
|
|
1637
|
+
const runsDir = (0, path_1.safeJoin)(logsDir, 'runs');
|
|
818
1638
|
if (!fs.existsSync(runsDir))
|
|
819
1639
|
return null;
|
|
820
1640
|
const runs = fs.readdirSync(runsDir)
|
|
821
1641
|
.filter(d => d.startsWith('run-'))
|
|
822
|
-
.map(d => ({ name: d, path:
|
|
1642
|
+
.map(d => ({ name: d, path: (0, path_1.safeJoin)(runsDir, d), mtime: fs.statSync((0, path_1.safeJoin)(runsDir, d)).mtime.getTime() }))
|
|
823
1643
|
.sort((a, b) => b.mtime - a.mtime);
|
|
824
1644
|
return runs.length > 0 ? runs[0].path : null;
|
|
825
1645
|
}
|
|
@@ -832,7 +1652,6 @@ async function monitor(args) {
|
|
|
832
1652
|
printHelp();
|
|
833
1653
|
return;
|
|
834
1654
|
}
|
|
835
|
-
const watchIdx = args.indexOf('--watch');
|
|
836
1655
|
const intervalIdx = args.indexOf('--interval');
|
|
837
1656
|
const interval = intervalIdx >= 0 ? parseInt(args[intervalIdx + 1] || '2') || 2 : 2;
|
|
838
1657
|
const runDirArg = args.find(arg => !arg.startsWith('--') && args.indexOf(arg) !== intervalIdx + 1);
|