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