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