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