@litmers/cursorflow-orchestrator 0.1.6 → 0.1.9
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 +31 -0
- package/README.md +97 -321
- package/commands/cursorflow-doctor.md +28 -0
- package/commands/cursorflow-monitor.md +59 -101
- package/commands/cursorflow-prepare.md +25 -2
- package/commands/cursorflow-resume.md +11 -0
- package/commands/cursorflow-run.md +109 -100
- package/commands/cursorflow-signal.md +85 -14
- package/dist/cli/clean.d.ts +3 -1
- package/dist/cli/clean.js +122 -8
- package/dist/cli/clean.js.map +1 -1
- package/dist/cli/index.js +20 -20
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/monitor.d.ts +1 -1
- package/dist/cli/monitor.js +678 -145
- package/dist/cli/monitor.js.map +1 -1
- package/dist/cli/run.js +1 -0
- package/dist/cli/run.js.map +1 -1
- package/dist/core/orchestrator.d.ts +4 -2
- package/dist/core/orchestrator.js +92 -23
- package/dist/core/orchestrator.js.map +1 -1
- package/dist/core/runner.d.ts +14 -2
- package/dist/core/runner.js +244 -58
- package/dist/core/runner.js.map +1 -1
- package/dist/utils/types.d.ts +11 -0
- package/package.json +1 -1
- package/scripts/patches/test-cursor-agent.js +203 -0
- package/src/cli/clean.ts +129 -9
- package/src/cli/index.ts +20 -20
- package/src/cli/monitor.ts +732 -185
- package/src/cli/run.ts +1 -0
- package/src/core/orchestrator.ts +102 -27
- package/src/core/runner.ts +284 -66
- package/src/utils/types.ts +11 -0
package/src/cli/monitor.ts
CHANGED
|
@@ -1,229 +1,776 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* CursorFlow monitor command
|
|
2
|
+
* CursorFlow interactive monitor command
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import * as fs from 'fs';
|
|
6
6
|
import * as path from 'path';
|
|
7
|
+
import * as readline from 'readline';
|
|
7
8
|
import * as logger from '../utils/logger';
|
|
8
|
-
import { loadState } from '../utils/state';
|
|
9
|
-
import { LaneState } from '../utils/types';
|
|
9
|
+
import { loadState, readLog } from '../utils/state';
|
|
10
|
+
import { LaneState, ConversationEntry } from '../utils/types';
|
|
10
11
|
import { loadConfig } from '../utils/config';
|
|
11
12
|
|
|
13
|
+
interface LaneWithDeps {
|
|
14
|
+
name: string;
|
|
15
|
+
path: string;
|
|
16
|
+
dependsOn: string[];
|
|
17
|
+
}
|
|
18
|
+
|
|
12
19
|
interface MonitorOptions {
|
|
13
20
|
runDir?: string;
|
|
14
|
-
watch: boolean;
|
|
15
21
|
interval: number;
|
|
16
22
|
}
|
|
17
23
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
return {
|
|
27
|
-
runDir,
|
|
28
|
-
watch,
|
|
29
|
-
interval,
|
|
30
|
-
};
|
|
24
|
+
enum View {
|
|
25
|
+
LIST,
|
|
26
|
+
LANE_DETAIL,
|
|
27
|
+
MESSAGE_DETAIL,
|
|
28
|
+
FLOW,
|
|
29
|
+
TERMINAL,
|
|
30
|
+
INTERVENE
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
33
|
+
class InteractiveMonitor {
|
|
34
|
+
private runDir: string;
|
|
35
|
+
private interval: number;
|
|
36
|
+
private view: View = View.LIST;
|
|
37
|
+
private selectedLaneIndex: number = 0;
|
|
38
|
+
private selectedMessageIndex: number = 0;
|
|
39
|
+
private selectedLaneName: string | null = null;
|
|
40
|
+
private lanes: LaneWithDeps[] = [];
|
|
41
|
+
private currentLogs: ConversationEntry[] = [];
|
|
42
|
+
private timer: NodeJS.Timeout | null = null;
|
|
43
|
+
private scrollOffset: number = 0;
|
|
44
|
+
private terminalScrollOffset: number = 0;
|
|
45
|
+
private lastTerminalTotalLines: number = 0;
|
|
46
|
+
private interventionInput: string = '';
|
|
47
|
+
private notification: { message: string; type: 'info' | 'error' | 'success'; time: number } | null = null;
|
|
48
|
+
|
|
49
|
+
constructor(runDir: string, interval: number) {
|
|
50
|
+
this.runDir = runDir;
|
|
51
|
+
this.interval = interval;
|
|
41
52
|
}
|
|
42
|
-
|
|
43
|
-
const runs = fs.readdirSync(runsDir)
|
|
44
|
-
.filter(d => d.startsWith('run-'))
|
|
45
|
-
.map(d => ({
|
|
46
|
-
name: d,
|
|
47
|
-
path: path.join(runsDir, d),
|
|
48
|
-
mtime: fs.statSync(path.join(runsDir, d)).mtime.getTime(),
|
|
49
|
-
}))
|
|
50
|
-
.sort((a, b) => b.mtime - a.mtime);
|
|
51
|
-
|
|
52
|
-
return runs.length > 0 ? runs[0]!.path : null;
|
|
53
|
-
}
|
|
54
53
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
const lanesDir = path.join(runDir, 'lanes');
|
|
60
|
-
|
|
61
|
-
if (!fs.existsSync(lanesDir)) {
|
|
62
|
-
return [];
|
|
54
|
+
public async start() {
|
|
55
|
+
this.setupTerminal();
|
|
56
|
+
this.refresh();
|
|
57
|
+
this.timer = setInterval(() => this.refresh(), this.interval * 1000);
|
|
63
58
|
}
|
|
64
|
-
|
|
65
|
-
return fs.readdirSync(lanesDir)
|
|
66
|
-
.filter(d => {
|
|
67
|
-
const stat = fs.statSync(path.join(lanesDir, d));
|
|
68
|
-
return stat.isDirectory();
|
|
69
|
-
})
|
|
70
|
-
.map(name => ({
|
|
71
|
-
name,
|
|
72
|
-
path: path.join(lanesDir, name),
|
|
73
|
-
}));
|
|
74
|
-
}
|
|
75
59
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
60
|
+
private setupTerminal() {
|
|
61
|
+
if (process.stdin.isTTY) {
|
|
62
|
+
process.stdin.setRawMode(true);
|
|
63
|
+
}
|
|
64
|
+
readline.emitKeypressEvents(process.stdin);
|
|
65
|
+
process.stdin.on('keypress', (str, key) => {
|
|
66
|
+
// Handle Ctrl+C
|
|
67
|
+
if (key && key.ctrl && key.name === 'c') {
|
|
68
|
+
this.stop();
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Safeguard against missing key object
|
|
73
|
+
const keyName = key ? key.name : str;
|
|
74
|
+
|
|
75
|
+
if (this.view === View.LIST) {
|
|
76
|
+
this.handleListKey(keyName);
|
|
77
|
+
} else if (this.view === View.LANE_DETAIL) {
|
|
78
|
+
this.handleDetailKey(keyName);
|
|
79
|
+
} else if (this.view === View.FLOW) {
|
|
80
|
+
this.handleFlowKey(keyName);
|
|
81
|
+
} else if (this.view === View.TERMINAL) {
|
|
82
|
+
this.handleTerminalKey(keyName);
|
|
83
|
+
} else if (this.view === View.INTERVENE) {
|
|
84
|
+
this.handleInterveneKey(str, key);
|
|
85
|
+
} else {
|
|
86
|
+
this.handleMessageDetailKey(keyName);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// Hide cursor
|
|
91
|
+
process.stdout.write('\x1B[?25l');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
private stop() {
|
|
95
|
+
if (this.timer) clearInterval(this.timer);
|
|
96
|
+
// Show cursor and clear screen
|
|
97
|
+
process.stdout.write('\x1B[?25h');
|
|
98
|
+
process.stdout.write('\x1Bc');
|
|
99
|
+
console.log('\n👋 Monitoring stopped\n');
|
|
100
|
+
process.exit(0);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
private handleListKey(key: string) {
|
|
104
|
+
switch (key) {
|
|
105
|
+
case 'up':
|
|
106
|
+
this.selectedLaneIndex = Math.max(0, this.selectedLaneIndex - 1);
|
|
107
|
+
this.render();
|
|
108
|
+
break;
|
|
109
|
+
case 'down':
|
|
110
|
+
this.selectedLaneIndex = Math.min(this.lanes.length - 1, this.selectedLaneIndex + 1);
|
|
111
|
+
this.render();
|
|
112
|
+
break;
|
|
113
|
+
case 'right':
|
|
114
|
+
case 'return':
|
|
115
|
+
case 'enter':
|
|
116
|
+
if (this.lanes[this.selectedLaneIndex]) {
|
|
117
|
+
this.selectedLaneName = this.lanes[this.selectedLaneIndex]!.name;
|
|
118
|
+
this.view = View.LANE_DETAIL;
|
|
119
|
+
this.selectedMessageIndex = 0;
|
|
120
|
+
this.scrollOffset = 0;
|
|
121
|
+
this.refreshLogs();
|
|
122
|
+
this.render();
|
|
123
|
+
}
|
|
124
|
+
break;
|
|
125
|
+
case 'left':
|
|
126
|
+
case 'f':
|
|
127
|
+
this.view = View.FLOW;
|
|
128
|
+
this.render();
|
|
129
|
+
break;
|
|
130
|
+
case 'q':
|
|
131
|
+
this.stop();
|
|
132
|
+
break;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
private handleDetailKey(key: string) {
|
|
137
|
+
switch (key) {
|
|
138
|
+
case 'up':
|
|
139
|
+
this.selectedMessageIndex = Math.max(0, this.selectedMessageIndex - 1);
|
|
140
|
+
this.render();
|
|
141
|
+
break;
|
|
142
|
+
case 'down':
|
|
143
|
+
this.selectedMessageIndex = Math.min(this.currentLogs.length - 1, this.selectedMessageIndex + 1);
|
|
144
|
+
this.render();
|
|
145
|
+
break;
|
|
146
|
+
case 'right':
|
|
147
|
+
case 'return':
|
|
148
|
+
case 'enter':
|
|
149
|
+
if (this.currentLogs[this.selectedMessageIndex]) {
|
|
150
|
+
this.view = View.MESSAGE_DETAIL;
|
|
151
|
+
this.render();
|
|
152
|
+
}
|
|
153
|
+
break;
|
|
154
|
+
case 't':
|
|
155
|
+
this.view = View.TERMINAL;
|
|
156
|
+
this.terminalScrollOffset = 0;
|
|
157
|
+
this.render();
|
|
158
|
+
break;
|
|
159
|
+
case 'k':
|
|
160
|
+
this.killLane();
|
|
161
|
+
break;
|
|
162
|
+
case 'i':
|
|
163
|
+
const lane = this.lanes.find(l => l.name === this.selectedLaneName);
|
|
164
|
+
if (lane) {
|
|
165
|
+
const status = this.getLaneStatus(lane.path, lane.name);
|
|
166
|
+
if (status.status === 'running') {
|
|
167
|
+
this.view = View.INTERVENE;
|
|
168
|
+
this.interventionInput = '';
|
|
169
|
+
this.render();
|
|
170
|
+
} else {
|
|
171
|
+
// Show a temporary message that it's only for running lanes
|
|
172
|
+
console.log(`\n\x1b[31m Intervention is only available for RUNNING lanes.\x1b[0m`);
|
|
173
|
+
setTimeout(() => this.render(), 1500);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
break;
|
|
177
|
+
case 'escape':
|
|
178
|
+
case 'backspace':
|
|
179
|
+
case 'left':
|
|
180
|
+
this.view = View.LIST;
|
|
181
|
+
this.selectedLaneName = null;
|
|
182
|
+
this.render();
|
|
183
|
+
break;
|
|
184
|
+
case 'q':
|
|
185
|
+
this.stop();
|
|
186
|
+
break;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
private handleMessageDetailKey(key: string) {
|
|
191
|
+
switch (key) {
|
|
192
|
+
case 'escape':
|
|
193
|
+
case 'backspace':
|
|
194
|
+
case 'left':
|
|
195
|
+
this.view = View.LANE_DETAIL;
|
|
196
|
+
this.render();
|
|
197
|
+
break;
|
|
198
|
+
case 'q':
|
|
199
|
+
this.stop();
|
|
200
|
+
break;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
private handleTerminalKey(key: string) {
|
|
205
|
+
switch (key) {
|
|
206
|
+
case 'up':
|
|
207
|
+
this.terminalScrollOffset++;
|
|
208
|
+
this.render();
|
|
209
|
+
break;
|
|
210
|
+
case 'down':
|
|
211
|
+
this.terminalScrollOffset = Math.max(0, this.terminalScrollOffset - 1);
|
|
212
|
+
this.render();
|
|
213
|
+
break;
|
|
214
|
+
case 't':
|
|
215
|
+
case 'escape':
|
|
216
|
+
case 'backspace':
|
|
217
|
+
case 'left':
|
|
218
|
+
this.view = View.LANE_DETAIL;
|
|
219
|
+
this.render();
|
|
220
|
+
break;
|
|
221
|
+
case 'i':
|
|
222
|
+
this.view = View.INTERVENE;
|
|
223
|
+
this.interventionInput = '';
|
|
224
|
+
this.render();
|
|
225
|
+
break;
|
|
226
|
+
case 'q':
|
|
227
|
+
this.stop();
|
|
228
|
+
break;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
private handleInterveneKey(str: string, key: any) {
|
|
233
|
+
if (key.name === 'return' || key.name === 'enter') {
|
|
234
|
+
if (this.interventionInput.trim()) {
|
|
235
|
+
this.sendIntervention(this.interventionInput.trim());
|
|
236
|
+
}
|
|
237
|
+
this.view = View.LANE_DETAIL;
|
|
238
|
+
this.render();
|
|
239
|
+
} else if (key.name === 'escape') {
|
|
240
|
+
this.view = View.LANE_DETAIL;
|
|
241
|
+
this.render();
|
|
242
|
+
} else if (key.name === 'backspace') {
|
|
243
|
+
this.interventionInput = this.interventionInput.slice(0, -1);
|
|
244
|
+
this.render();
|
|
245
|
+
} else if (str && str.length === 1) {
|
|
246
|
+
this.interventionInput += str;
|
|
247
|
+
this.render();
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
private sendIntervention(message: string) {
|
|
252
|
+
if (!this.selectedLaneName) return;
|
|
253
|
+
const lane = this.lanes.find(l => l.name === this.selectedLaneName);
|
|
254
|
+
if (!lane) return;
|
|
255
|
+
|
|
256
|
+
// Write to intervention.txt which runner.ts is watching
|
|
257
|
+
const interventionPath = path.join(path.dirname(lane.path), 'intervention.txt');
|
|
258
|
+
fs.writeFileSync(interventionPath, message, 'utf8');
|
|
259
|
+
|
|
260
|
+
// Also log it to the conversation
|
|
261
|
+
const convoPath = path.join(lane.path, 'conversation.jsonl');
|
|
262
|
+
const entry = {
|
|
263
|
+
timestamp: new Date().toISOString(),
|
|
264
|
+
role: 'user',
|
|
265
|
+
task: 'INTERVENTION',
|
|
266
|
+
fullText: `[HUMAN INTERVENTION]: ${message}`,
|
|
267
|
+
textLength: message.length + 20,
|
|
268
|
+
model: 'manual'
|
|
96
269
|
};
|
|
270
|
+
fs.appendFileSync(convoPath, JSON.stringify(entry) + '\n', 'utf8');
|
|
97
271
|
}
|
|
98
|
-
|
|
99
|
-
const progress = state.totalTasks > 0
|
|
100
|
-
? Math.round((state.currentTaskIndex / state.totalTasks) * 100)
|
|
101
|
-
: 0;
|
|
102
|
-
|
|
103
|
-
return {
|
|
104
|
-
status: state.status || 'unknown',
|
|
105
|
-
currentTask: (state.currentTaskIndex || 0) + 1,
|
|
106
|
-
totalTasks: state.totalTasks || '?',
|
|
107
|
-
progress: `${progress}%`,
|
|
108
|
-
pipelineBranch: state.pipelineBranch || '-',
|
|
109
|
-
chatId: state.chatId || '-',
|
|
110
|
-
};
|
|
111
|
-
}
|
|
112
272
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
273
|
+
private handleFlowKey(key: string) {
|
|
274
|
+
switch (key) {
|
|
275
|
+
case 'f':
|
|
276
|
+
case 'escape':
|
|
277
|
+
case 'backspace':
|
|
278
|
+
case 'right':
|
|
279
|
+
case 'return':
|
|
280
|
+
case 'enter':
|
|
281
|
+
case 'left':
|
|
282
|
+
this.view = View.LIST;
|
|
283
|
+
this.render();
|
|
284
|
+
break;
|
|
285
|
+
case 'q':
|
|
286
|
+
this.stop();
|
|
287
|
+
break;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
127
290
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
return;
|
|
291
|
+
private refreshLogs() {
|
|
292
|
+
if (!this.selectedLaneName) return;
|
|
293
|
+
const lane = this.lanes.find(l => l.name === this.selectedLaneName);
|
|
294
|
+
if (!lane) return;
|
|
295
|
+
const convoPath = path.join(lane.path, 'conversation.jsonl');
|
|
296
|
+
this.currentLogs = readLog<ConversationEntry>(convoPath);
|
|
297
|
+
// Keep selection in bounds after refresh
|
|
298
|
+
if (this.selectedMessageIndex >= this.currentLogs.length) {
|
|
299
|
+
this.selectedMessageIndex = Math.max(0, this.currentLogs.length - 1);
|
|
300
|
+
}
|
|
139
301
|
}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
// Lanes
|
|
149
|
-
for (const lane of lanes) {
|
|
150
|
-
const status = getLaneStatus(lane.path);
|
|
151
|
-
const statusIcon = getStatusIcon(status.status);
|
|
152
|
-
const statusText = `${statusIcon} ${status.status}`.padEnd(18);
|
|
153
|
-
const progressText = status.progress.padEnd(8);
|
|
154
|
-
const tasksText = `${status.currentTask}/${status.totalTasks}`;
|
|
155
|
-
|
|
156
|
-
console.log(` ${lane.name.padEnd(maxNameLen)} ${statusText} ${progressText} ${tasksText}`);
|
|
302
|
+
|
|
303
|
+
private refresh() {
|
|
304
|
+
this.lanes = this.listLanesWithDeps(this.runDir);
|
|
305
|
+
if (this.view !== View.LIST) {
|
|
306
|
+
this.refreshLogs();
|
|
307
|
+
}
|
|
308
|
+
this.render();
|
|
157
309
|
}
|
|
158
|
-
|
|
159
|
-
console.log();
|
|
160
|
-
}
|
|
161
310
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
311
|
+
private killLane() {
|
|
312
|
+
if (!this.selectedLaneName) return;
|
|
313
|
+
const lane = this.lanes.find(l => l.name === this.selectedLaneName);
|
|
314
|
+
if (!lane) return;
|
|
315
|
+
|
|
316
|
+
const status = this.getLaneStatus(lane.path, lane.name);
|
|
317
|
+
if (status.pid && status.status === 'running') {
|
|
318
|
+
try {
|
|
319
|
+
process.kill(status.pid, 'SIGTERM');
|
|
320
|
+
this.showNotification(`Sent SIGTERM to PID ${status.pid}`, 'success');
|
|
321
|
+
} catch (e) {
|
|
322
|
+
this.showNotification(`Failed to kill PID ${status.pid}`, 'error');
|
|
323
|
+
}
|
|
324
|
+
} else {
|
|
325
|
+
this.showNotification(`No running process found for ${this.selectedLaneName}`, 'info');
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
private showNotification(message: string, type: 'info' | 'error' | 'success') {
|
|
330
|
+
this.notification = { message, type, time: Date.now() };
|
|
331
|
+
this.render();
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
private render() {
|
|
335
|
+
// Clear screen
|
|
336
|
+
process.stdout.write('\x1Bc');
|
|
176
337
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
338
|
+
// Clear old notifications
|
|
339
|
+
if (this.notification && Date.now() - this.notification.time > 3000) {
|
|
340
|
+
this.notification = null;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (this.notification) {
|
|
344
|
+
const color = this.notification.type === 'error' ? '\x1b[31m' : this.notification.type === 'success' ? '\x1b[32m' : '\x1b[36m';
|
|
345
|
+
console.log(`${color}🔔 ${this.notification.message}\x1b[0m\n`);
|
|
180
346
|
}
|
|
181
347
|
|
|
182
|
-
|
|
348
|
+
switch (this.view) {
|
|
349
|
+
case View.LIST:
|
|
350
|
+
this.renderList();
|
|
351
|
+
break;
|
|
352
|
+
case View.LANE_DETAIL:
|
|
353
|
+
this.renderLaneDetail();
|
|
354
|
+
break;
|
|
355
|
+
case View.MESSAGE_DETAIL:
|
|
356
|
+
this.renderMessageDetail();
|
|
357
|
+
break;
|
|
358
|
+
case View.FLOW:
|
|
359
|
+
this.renderFlow();
|
|
360
|
+
break;
|
|
361
|
+
case View.TERMINAL:
|
|
362
|
+
this.renderTerminal();
|
|
363
|
+
break;
|
|
364
|
+
case View.INTERVENE:
|
|
365
|
+
this.renderIntervene();
|
|
366
|
+
break;
|
|
367
|
+
}
|
|
183
368
|
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
369
|
+
|
|
370
|
+
private renderList() {
|
|
371
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
372
|
+
console.log(`📊 CursorFlow Monitor - Run: ${path.basename(this.runDir)}`);
|
|
373
|
+
console.log(`🕒 Updated: ${new Date().toLocaleTimeString()} | [↑/↓/→] Nav [←] Flow [Q] Quit`);
|
|
374
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
|
|
375
|
+
|
|
376
|
+
if (this.lanes.length === 0) {
|
|
377
|
+
console.log(' No lanes found\n');
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const laneStatuses: Record<string, any> = {};
|
|
382
|
+
this.lanes.forEach(l => laneStatuses[l.name] = this.getLaneStatus(l.path, l.name));
|
|
383
|
+
|
|
384
|
+
const maxNameLen = Math.max(...this.lanes.map(l => l.name.length), 15);
|
|
385
|
+
console.log(` ${'Lane'.padEnd(maxNameLen)} Status Progress Time Tasks Next Action`);
|
|
386
|
+
console.log(` ${'─'.repeat(maxNameLen)} ${'─'.repeat(18)} ${'─'.repeat(8)} ${'─'.repeat(8)} ${'─'.repeat(6)} ${'─'.repeat(20)}`);
|
|
387
|
+
|
|
388
|
+
this.lanes.forEach((lane, i) => {
|
|
389
|
+
const isSelected = i === this.selectedLaneIndex;
|
|
390
|
+
const status = laneStatuses[lane.name];
|
|
391
|
+
const statusIcon = this.getStatusIcon(status.status);
|
|
392
|
+
const statusText = `${statusIcon} ${status.status}`.padEnd(18);
|
|
393
|
+
const progressText = status.progress.padEnd(8);
|
|
394
|
+
const timeText = this.formatDuration(status.duration).padEnd(8);
|
|
395
|
+
|
|
396
|
+
let tasksDisplay = '-';
|
|
397
|
+
if (typeof status.totalTasks === 'number') {
|
|
398
|
+
tasksDisplay = `${status.currentTask}/${status.totalTasks}`;
|
|
399
|
+
}
|
|
400
|
+
const tasksText = tasksDisplay.padEnd(6);
|
|
401
|
+
|
|
402
|
+
// Determine "Next Action"
|
|
403
|
+
let nextAction = '-';
|
|
404
|
+
if (status.status === 'completed') {
|
|
405
|
+
const dependents = this.lanes.filter(l => laneStatuses[l.name].dependsOn.includes(lane.name));
|
|
406
|
+
if (dependents.length > 0) {
|
|
407
|
+
nextAction = `Unlock: ${dependents.map(d => d.name).join(', ')}`;
|
|
408
|
+
} else {
|
|
409
|
+
nextAction = '🏁 Done';
|
|
410
|
+
}
|
|
411
|
+
} else if (status.status === 'waiting') {
|
|
412
|
+
const missingDeps = status.dependsOn.filter((d: string) => laneStatuses[d] && laneStatuses[d].status !== 'completed');
|
|
413
|
+
nextAction = `Wait for: ${missingDeps.join(', ')}`;
|
|
414
|
+
} else if (status.status === 'running') {
|
|
415
|
+
nextAction = '🚀 Working...';
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const prefix = isSelected ? ' ▶ ' : ' ';
|
|
419
|
+
const line = `${prefix}${lane.name.padEnd(maxNameLen)} ${statusText} ${progressText} ${timeText} ${tasksText} ${nextAction}`;
|
|
420
|
+
|
|
421
|
+
if (isSelected) {
|
|
422
|
+
process.stdout.write(`\x1b[36m${line}\x1b[0m\n`);
|
|
423
|
+
} else {
|
|
424
|
+
process.stdout.write(`${line}\n`);
|
|
425
|
+
}
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
187
429
|
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
430
|
+
|
|
431
|
+
private renderLaneDetail() {
|
|
432
|
+
const lane = this.lanes.find(l => l.name === this.selectedLaneName);
|
|
433
|
+
if (!lane) {
|
|
434
|
+
this.view = View.LIST;
|
|
435
|
+
this.render();
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
const status = this.getLaneStatus(lane.path, lane.name);
|
|
440
|
+
const logPath = path.join(lane.path, 'terminal.log');
|
|
441
|
+
let liveLog = '(No live terminal output)';
|
|
442
|
+
if (fs.existsSync(logPath)) {
|
|
443
|
+
const content = fs.readFileSync(logPath, 'utf8');
|
|
444
|
+
liveLog = content.split('\n').slice(-15).join('\n');
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
448
|
+
console.log(`🔍 Lane: ${lane.name}`);
|
|
449
|
+
console.log(`🕒 Updated: ${new Date().toLocaleTimeString()} | [↑/↓/→] Browse History [Esc/←] Back`);
|
|
450
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
|
|
451
|
+
|
|
452
|
+
process.stdout.write(` Status: ${this.getStatusIcon(status.status)} ${status.status}\n`);
|
|
453
|
+
process.stdout.write(` PID: ${status.pid || '-'}\n`);
|
|
454
|
+
process.stdout.write(` Progress: ${status.progress} (${status.currentTask}/${status.totalTasks} tasks)\n`);
|
|
455
|
+
process.stdout.write(` Time: ${this.formatDuration(status.duration)}\n`);
|
|
456
|
+
process.stdout.write(` Branch: ${status.pipelineBranch}\n`);
|
|
457
|
+
process.stdout.write(` Chat ID: ${status.chatId}\n`);
|
|
458
|
+
process.stdout.write(` Depends: ${status.dependsOn.join(', ') || 'None'}\n`);
|
|
194
459
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
460
|
+
if (status.error) {
|
|
461
|
+
process.stdout.write(`\x1b[31m Error: ${status.error}\x1b[0m\n`);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
console.log('\n🖥️ Live Terminal Output (Last 15 lines):');
|
|
465
|
+
console.log('─'.repeat(80));
|
|
466
|
+
console.log(`\x1b[90m${liveLog}\x1b[0m`);
|
|
467
|
+
|
|
468
|
+
console.log('\n💬 Conversation History (Select to see full details):');
|
|
469
|
+
console.log('─'.repeat(80));
|
|
470
|
+
process.stdout.write(' [↑/↓] Browse | [→/Enter] Full Msg | [I] Intervene | [K] Kill | [T] Live Terminal | [Esc/←] Back\n\n');
|
|
471
|
+
|
|
472
|
+
if (this.currentLogs.length === 0) {
|
|
473
|
+
console.log(' (No messages yet)');
|
|
474
|
+
} else {
|
|
475
|
+
// Simple windowed view for long histories
|
|
476
|
+
const maxVisible = 15; // Number of messages to show
|
|
477
|
+
if (this.selectedMessageIndex < this.scrollOffset) {
|
|
478
|
+
this.scrollOffset = this.selectedMessageIndex;
|
|
479
|
+
} else if (this.selectedMessageIndex >= this.scrollOffset + maxVisible) {
|
|
480
|
+
this.scrollOffset = this.selectedMessageIndex - maxVisible + 1;
|
|
199
481
|
}
|
|
482
|
+
|
|
483
|
+
const visibleLogs = this.currentLogs.slice(this.scrollOffset, this.scrollOffset + maxVisible);
|
|
200
484
|
|
|
201
|
-
|
|
202
|
-
|
|
485
|
+
visibleLogs.forEach((log, i) => {
|
|
486
|
+
const actualIndex = i + this.scrollOffset;
|
|
487
|
+
const isSelected = actualIndex === this.selectedMessageIndex;
|
|
488
|
+
const roleColor = log.role === 'user' ? '\x1b[33m' : log.role === 'reviewer' ? '\x1b[35m' : '\x1b[32m';
|
|
489
|
+
const role = log.role.toUpperCase().padEnd(10);
|
|
490
|
+
|
|
491
|
+
const prefix = isSelected ? '▶ ' : ' ';
|
|
492
|
+
const header = `${prefix}${roleColor}${role}\x1b[0m [${new Date(log.timestamp).toLocaleTimeString()}]`;
|
|
493
|
+
|
|
494
|
+
if (isSelected) {
|
|
495
|
+
process.stdout.write(`\x1b[48;5;236m${header}\x1b[0m\n`);
|
|
496
|
+
} else {
|
|
497
|
+
process.stdout.write(`${header}\n`);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
const lines = log.fullText.split('\n').filter(l => l.trim());
|
|
501
|
+
const preview = lines[0]?.substring(0, 70) || '...';
|
|
502
|
+
process.stdout.write(` ${preview}${log.fullText.length > 70 ? '...' : ''}\n\n`);
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
if (this.currentLogs.length > maxVisible) {
|
|
506
|
+
console.log(` -- (${this.currentLogs.length - maxVisible} more messages, use ↑/↓ to scroll) --`);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
private renderMessageDetail() {
|
|
514
|
+
const log = this.currentLogs[this.selectedMessageIndex];
|
|
515
|
+
if (!log) {
|
|
516
|
+
this.view = View.LANE_DETAIL;
|
|
517
|
+
this.render();
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
522
|
+
console.log(`📄 Full Message Detail - ${log.role.toUpperCase()}`);
|
|
523
|
+
console.log(`🕒 ${new Date(log.timestamp).toLocaleString()} | [Esc/←] Back to History`);
|
|
524
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
|
|
525
|
+
|
|
526
|
+
const roleColor = log.role === 'user' ? '\x1b[33m' : log.role === 'reviewer' ? '\x1b[35m' : '\x1b[32m';
|
|
527
|
+
process.stdout.write(`${roleColor}ROLE: ${log.role.toUpperCase()}\x1b[0m\n`);
|
|
528
|
+
if (log.model) process.stdout.write(`MODEL: ${log.model}\n`);
|
|
529
|
+
if (log.task) process.stdout.write(`TASK: ${log.task}\n`);
|
|
530
|
+
console.log('─'.repeat(40));
|
|
531
|
+
console.log(log.fullText);
|
|
532
|
+
console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
private renderFlow() {
|
|
536
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
537
|
+
console.log(`⛓️ Task Dependency Flow`);
|
|
538
|
+
console.log(`🕒 Updated: ${new Date().toLocaleTimeString()} | [→/Enter/Esc] Back to List`);
|
|
539
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
|
|
540
|
+
|
|
541
|
+
const laneMap = new Map<string, any>();
|
|
542
|
+
this.lanes.forEach(lane => {
|
|
543
|
+
laneMap.set(lane.name, this.getLaneStatus(lane.path, lane.name));
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
// Enhanced visualization with box-like structure and clear connections
|
|
547
|
+
this.lanes.forEach(lane => {
|
|
548
|
+
const status = laneMap.get(lane.name);
|
|
549
|
+
const statusIcon = this.getStatusIcon(status.status);
|
|
203
550
|
|
|
204
|
-
|
|
205
|
-
|
|
551
|
+
let statusColor = '\x1b[90m'; // Grey for pending/waiting
|
|
552
|
+
if (status.status === 'completed') statusColor = '\x1b[32m'; // Green
|
|
553
|
+
if (status.status === 'running') statusColor = '\x1b[36m'; // Cyan
|
|
554
|
+
if (status.status === 'failed') statusColor = '\x1b[31m'; // Red
|
|
555
|
+
|
|
556
|
+
// Render the node
|
|
557
|
+
const nodeText = `[ ${statusIcon} ${lane.name.padEnd(18)} ]`;
|
|
558
|
+
process.stdout.write(` ${statusColor}${nodeText}\x1b[0m`);
|
|
559
|
+
|
|
560
|
+
// Render dependencies
|
|
561
|
+
if (status.dependsOn && status.dependsOn.length > 0) {
|
|
562
|
+
process.stdout.write(` \x1b[90m◀───\x1b[0m \x1b[33m( ${status.dependsOn.join(', ')} )\x1b[0m`);
|
|
563
|
+
}
|
|
564
|
+
process.stdout.write('\n');
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
console.log('\n\x1b[90m (Lanes wait for their dependencies to complete before starting)\x1b[0m');
|
|
568
|
+
console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
private renderTerminal() {
|
|
572
|
+
const lane = this.lanes.find(l => l.name === this.selectedLaneName);
|
|
573
|
+
if (!lane) {
|
|
574
|
+
this.view = View.LIST;
|
|
575
|
+
this.render();
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
const logPath = path.join(lane.path, 'terminal.log');
|
|
580
|
+
let logLines: string[] = [];
|
|
581
|
+
if (fs.existsSync(logPath)) {
|
|
582
|
+
const content = fs.readFileSync(logPath, 'utf8');
|
|
583
|
+
logLines = content.split('\n');
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
const maxVisible = 40;
|
|
587
|
+
const totalLines = logLines.length;
|
|
588
|
+
|
|
589
|
+
// Sticky scroll logic: if new lines arrived and we are already scrolled up,
|
|
590
|
+
// increase the offset to stay on the same content.
|
|
591
|
+
if (this.terminalScrollOffset > 0 && this.lastTerminalTotalLines > 0 && totalLines > this.lastTerminalTotalLines) {
|
|
592
|
+
this.terminalScrollOffset += (totalLines - this.lastTerminalTotalLines);
|
|
593
|
+
}
|
|
594
|
+
this.lastTerminalTotalLines = totalLines;
|
|
206
595
|
|
|
207
|
-
//
|
|
208
|
-
|
|
596
|
+
// Clamp scroll offset
|
|
597
|
+
const maxScroll = Math.max(0, totalLines - maxVisible);
|
|
598
|
+
if (this.terminalScrollOffset > maxScroll) {
|
|
599
|
+
this.terminalScrollOffset = maxScroll;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// Slice based on scroll (0 means bottom, >0 means scrolled up)
|
|
603
|
+
const end = totalLines - this.terminalScrollOffset;
|
|
604
|
+
const start = Math.max(0, end - maxVisible);
|
|
605
|
+
const visibleLines = logLines.slice(start, end);
|
|
606
|
+
|
|
607
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
608
|
+
console.log(`🖥️ Full Live Terminal: ${lane.name}`);
|
|
609
|
+
console.log(`🕒 Streaming... | [↑/↓] Scroll (${this.terminalScrollOffset > 0 ? `Scrolled Up ${this.terminalScrollOffset}` : 'Bottom'}) | [I] Intervene | [T/Esc/←] Back`);
|
|
610
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
|
|
611
|
+
|
|
612
|
+
visibleLines.forEach(line => {
|
|
613
|
+
let formattedLine = line;
|
|
614
|
+
// Highlight human intervention
|
|
615
|
+
if (line.includes('[HUMAN INTERVENTION]') || line.includes('Injecting intervention:')) {
|
|
616
|
+
formattedLine = `\x1b[33m\x1b[1m${line}\x1b[0m`;
|
|
617
|
+
}
|
|
618
|
+
// Highlight agent execution starts
|
|
619
|
+
else if (line.includes('Executing cursor-agent')) {
|
|
620
|
+
formattedLine = `\x1b[36m\x1b[1m${line}\x1b[0m`;
|
|
621
|
+
}
|
|
622
|
+
// Highlight task headers
|
|
623
|
+
else if (line.includes('=== Task:')) {
|
|
624
|
+
formattedLine = `\x1b[32m\x1b[1m${line}\x1b[0m`;
|
|
625
|
+
}
|
|
626
|
+
// Highlight errors
|
|
627
|
+
else if (line.toLowerCase().includes('error') || line.toLowerCase().includes('failed')) {
|
|
628
|
+
formattedLine = `\x1b[31m${line}\x1b[0m`;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
process.stdout.write(` ${formattedLine}\n`);
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
private renderIntervene() {
|
|
638
|
+
console.clear();
|
|
639
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
640
|
+
console.log(`🙋 HUMAN INTERVENTION: ${this.selectedLaneName}`);
|
|
641
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
642
|
+
console.log('\n Type your message to the agent. This will be sent as a direct prompt.');
|
|
643
|
+
console.log(` Press \x1b[1mENTER\x1b[0m to send, \x1b[1mESC\x1b[0m to cancel.\n`);
|
|
644
|
+
console.log(`\x1b[33m > \x1b[0m${this.interventionInput}\x1b[37m█\x1b[0m`);
|
|
645
|
+
console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
private listLanesWithDeps(runDir: string): LaneWithDeps[] {
|
|
649
|
+
const lanesDir = path.join(runDir, 'lanes');
|
|
650
|
+
if (!fs.existsSync(lanesDir)) return [];
|
|
209
651
|
|
|
210
|
-
|
|
211
|
-
const
|
|
652
|
+
const config = loadConfig();
|
|
653
|
+
const tasksDir = path.join(config.projectRoot, config.tasksDir);
|
|
212
654
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
655
|
+
const laneConfigs = this.listLaneFilesFromDir(tasksDir);
|
|
656
|
+
|
|
657
|
+
return fs.readdirSync(lanesDir)
|
|
658
|
+
.filter(d => fs.statSync(path.join(lanesDir, d)).isDirectory())
|
|
659
|
+
.map(name => {
|
|
660
|
+
const config = laneConfigs.find(c => c.name === name);
|
|
661
|
+
return {
|
|
662
|
+
name,
|
|
663
|
+
path: path.join(lanesDir, name),
|
|
664
|
+
dependsOn: config?.dependsOn || [],
|
|
665
|
+
};
|
|
219
666
|
});
|
|
220
|
-
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
private listLaneFilesFromDir(tasksDir: string): { name: string; dependsOn: string[] }[] {
|
|
670
|
+
if (!fs.existsSync(tasksDir)) return [];
|
|
671
|
+
return fs.readdirSync(tasksDir)
|
|
672
|
+
.filter(f => f.endsWith('.json'))
|
|
673
|
+
.map(f => {
|
|
674
|
+
const filePath = path.join(tasksDir, f);
|
|
675
|
+
try {
|
|
676
|
+
const config = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
677
|
+
return { name: path.basename(f, '.json'), dependsOn: config.dependsOn || [] };
|
|
678
|
+
} catch {
|
|
679
|
+
return { name: path.basename(f, '.json'), dependsOn: [] };
|
|
680
|
+
}
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
private getLaneStatus(lanePath: string, laneName: string) {
|
|
685
|
+
const statePath = path.join(lanePath, 'state.json');
|
|
686
|
+
const state = loadState<LaneState & { chatId?: string }>(statePath);
|
|
221
687
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
688
|
+
const laneInfo = this.lanes.find(l => l.name === laneName);
|
|
689
|
+
const dependsOn = state?.dependsOn || laneInfo?.dependsOn || [];
|
|
690
|
+
|
|
691
|
+
if (!state) {
|
|
692
|
+
return { status: 'pending', currentTask: 0, totalTasks: '?', progress: '0%', dependsOn, duration: 0, pipelineBranch: '-', chatId: '-' };
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
const progress = state.totalTasks > 0 ? Math.round((state.currentTaskIndex / state.totalTasks) * 100) : 0;
|
|
696
|
+
|
|
697
|
+
const duration = state.startTime ? (state.endTime
|
|
698
|
+
? state.endTime - state.startTime
|
|
699
|
+
: (state.status === 'running' || state.status === 'reviewing' ? Date.now() - state.startTime : 0)) : 0;
|
|
700
|
+
|
|
701
|
+
return {
|
|
702
|
+
status: state.status || 'unknown',
|
|
703
|
+
currentTask: state.currentTaskIndex || 0,
|
|
704
|
+
totalTasks: state.totalTasks || '?',
|
|
705
|
+
progress: `${progress}%`,
|
|
706
|
+
pipelineBranch: state.pipelineBranch || '-',
|
|
707
|
+
chatId: state.chatId || '-',
|
|
708
|
+
dependsOn,
|
|
709
|
+
duration,
|
|
710
|
+
error: state.error,
|
|
711
|
+
pid: state.pid
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
private formatDuration(ms: number): string {
|
|
716
|
+
if (ms <= 0) return '-';
|
|
717
|
+
const seconds = Math.floor((ms / 1000) % 60);
|
|
718
|
+
const minutes = Math.floor((ms / (1000 * 60)) % 60);
|
|
719
|
+
const hours = Math.floor(ms / (1000 * 60 * 60));
|
|
720
|
+
|
|
721
|
+
if (hours > 0) return `${hours}h ${minutes}m`;
|
|
722
|
+
if (minutes > 0) return `${minutes}m ${seconds}s`;
|
|
723
|
+
return `${seconds}s`;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
private getStatusIcon(status: string): string {
|
|
727
|
+
const icons: Record<string, string> = {
|
|
728
|
+
'running': '🔄',
|
|
729
|
+
'waiting': '⏳',
|
|
730
|
+
'completed': '✅',
|
|
731
|
+
'failed': '❌',
|
|
732
|
+
'blocked_dependency': '🚫',
|
|
733
|
+
'pending': '⚪',
|
|
734
|
+
'reviewing': '👀',
|
|
735
|
+
};
|
|
736
|
+
return icons[status] || '❓';
|
|
226
737
|
}
|
|
227
738
|
}
|
|
228
739
|
|
|
740
|
+
/**
|
|
741
|
+
* Find the latest run directory
|
|
742
|
+
*/
|
|
743
|
+
function findLatestRunDir(logsDir: string): string | null {
|
|
744
|
+
const runsDir = path.join(logsDir, 'runs');
|
|
745
|
+
if (!fs.existsSync(runsDir)) return null;
|
|
746
|
+
const runs = fs.readdirSync(runsDir)
|
|
747
|
+
.filter(d => d.startsWith('run-'))
|
|
748
|
+
.map(d => ({ name: d, path: path.join(runsDir, d), mtime: fs.statSync(path.join(runsDir, d)).mtime.getTime() }))
|
|
749
|
+
.sort((a, b) => b.mtime - a.mtime);
|
|
750
|
+
return runs.length > 0 ? runs[0]!.path : null;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
/**
|
|
754
|
+
* Monitor lanes
|
|
755
|
+
*/
|
|
756
|
+
async function monitor(args: string[]): Promise<void> {
|
|
757
|
+
const watchIdx = args.indexOf('--watch');
|
|
758
|
+
const intervalIdx = args.indexOf('--interval');
|
|
759
|
+
const interval = intervalIdx >= 0 ? parseInt(args[intervalIdx + 1] || '2') || 2 : 2;
|
|
760
|
+
|
|
761
|
+
const runDirArg = args.find(arg => !arg.startsWith('--') && args.indexOf(arg) !== intervalIdx + 1);
|
|
762
|
+
const config = loadConfig();
|
|
763
|
+
|
|
764
|
+
let runDir = runDirArg;
|
|
765
|
+
if (!runDir || runDir === 'latest') {
|
|
766
|
+
runDir = findLatestRunDir(config.logsDir) || undefined;
|
|
767
|
+
if (!runDir) throw new Error('No run directories found');
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
if (!fs.existsSync(runDir)) throw new Error(`Run directory not found: ${runDir}`);
|
|
771
|
+
|
|
772
|
+
const monitor = new InteractiveMonitor(runDir, interval);
|
|
773
|
+
await monitor.start();
|
|
774
|
+
}
|
|
775
|
+
|
|
229
776
|
export = monitor;
|