agileflow 2.89.3 → 2.90.0
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 +5 -0
- package/README.md +3 -3
- package/lib/placeholder-registry.js +617 -0
- package/lib/smart-json-file.js +205 -1
- package/lib/table-formatter.js +504 -0
- package/lib/transient-status.js +374 -0
- package/lib/ui-manager.js +612 -0
- package/lib/validate-args.js +213 -0
- package/lib/validate-names.js +143 -0
- package/lib/validate-paths.js +434 -0
- package/lib/validate.js +37 -737
- package/package.json +4 -1
- package/scripts/check-update.js +16 -3
- package/scripts/lib/sessionRegistry.js +682 -0
- package/scripts/session-manager.js +77 -10
- package/scripts/tui/App.js +176 -0
- package/scripts/tui/index.js +75 -0
- package/scripts/tui/lib/crashRecovery.js +302 -0
- package/scripts/tui/lib/eventStream.js +316 -0
- package/scripts/tui/lib/keyboard.js +252 -0
- package/scripts/tui/lib/loopControl.js +371 -0
- package/scripts/tui/panels/OutputPanel.js +278 -0
- package/scripts/tui/panels/SessionPanel.js +178 -0
- package/scripts/tui/panels/TracePanel.js +333 -0
- package/src/core/commands/tui.md +91 -0
- package/tools/cli/commands/config.js +7 -30
- package/tools/cli/commands/doctor.js +18 -38
- package/tools/cli/commands/list.js +47 -35
- package/tools/cli/commands/status.js +13 -37
- package/tools/cli/commands/uninstall.js +9 -38
- package/tools/cli/installers/core/installer.js +13 -0
- package/tools/cli/lib/command-context.js +374 -0
- package/tools/cli/lib/config-manager.js +394 -0
- package/tools/cli/lib/ide-registry.js +186 -0
- package/tools/cli/lib/npm-utils.js +16 -3
- package/tools/cli/lib/self-update.js +148 -0
- package/tools/cli/lib/validation-middleware.js +491 -0
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const React = require('react');
|
|
4
|
+
const { Box, Text } = require('ink');
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
|
|
8
|
+
// Get session-manager functions
|
|
9
|
+
let getSessions;
|
|
10
|
+
try {
|
|
11
|
+
const sessionManager = require('../../session-manager');
|
|
12
|
+
getSessions = sessionManager.getSessions;
|
|
13
|
+
} catch (e) {
|
|
14
|
+
// Fallback if module not found
|
|
15
|
+
getSessions = () => ({ sessions: [], cleaned: 0 });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Get thread type color
|
|
20
|
+
*/
|
|
21
|
+
function getThreadColor(threadType) {
|
|
22
|
+
const colors = {
|
|
23
|
+
base: 'green',
|
|
24
|
+
parallel: 'cyan',
|
|
25
|
+
chained: 'yellow',
|
|
26
|
+
fusion: 'magenta',
|
|
27
|
+
big: 'blue',
|
|
28
|
+
long: 'white'
|
|
29
|
+
};
|
|
30
|
+
return colors[threadType] || 'gray';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Get status color
|
|
35
|
+
*/
|
|
36
|
+
function getStatusColor(active, current) {
|
|
37
|
+
if (current) return 'green';
|
|
38
|
+
if (active) return 'cyan';
|
|
39
|
+
return 'gray';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Format session for display
|
|
44
|
+
*/
|
|
45
|
+
function formatSession(session) {
|
|
46
|
+
const { id, branch, story, nickname, thread_type, active, current, is_main } = session;
|
|
47
|
+
|
|
48
|
+
// Build display name
|
|
49
|
+
let displayName = `Session ${id}`;
|
|
50
|
+
if (nickname) displayName += ` "${nickname}"`;
|
|
51
|
+
if (is_main) displayName += ' [main]';
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
displayName,
|
|
55
|
+
branch: branch || 'unknown',
|
|
56
|
+
story: story || 'none',
|
|
57
|
+
threadType: thread_type || 'base',
|
|
58
|
+
active,
|
|
59
|
+
current
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* SessionRow component - displays a single session
|
|
65
|
+
*/
|
|
66
|
+
function SessionRow({ session }) {
|
|
67
|
+
const fmt = formatSession(session);
|
|
68
|
+
const statusColor = getStatusColor(fmt.active, fmt.current);
|
|
69
|
+
const threadColor = getThreadColor(fmt.threadType);
|
|
70
|
+
|
|
71
|
+
// Current session indicator
|
|
72
|
+
const indicator = fmt.current ? '▶' : ' ';
|
|
73
|
+
const indicatorColor = fmt.current ? 'green' : 'gray';
|
|
74
|
+
|
|
75
|
+
return React.createElement(
|
|
76
|
+
Box,
|
|
77
|
+
{ flexDirection: 'column', marginBottom: 1 },
|
|
78
|
+
// Session name row
|
|
79
|
+
React.createElement(
|
|
80
|
+
Box,
|
|
81
|
+
null,
|
|
82
|
+
React.createElement(Text, { color: indicatorColor }, indicator + ' '),
|
|
83
|
+
React.createElement(Text, { bold: true, color: statusColor }, fmt.displayName),
|
|
84
|
+
React.createElement(Text, { dimColor: true }, ' ['),
|
|
85
|
+
React.createElement(Text, { color: threadColor }, fmt.threadType),
|
|
86
|
+
React.createElement(Text, { dimColor: true }, ']')
|
|
87
|
+
),
|
|
88
|
+
// Branch and story
|
|
89
|
+
React.createElement(
|
|
90
|
+
Box,
|
|
91
|
+
{ paddingLeft: 3 },
|
|
92
|
+
React.createElement(Text, { dimColor: true }, 'Branch: '),
|
|
93
|
+
React.createElement(Text, { color: 'cyan' }, fmt.branch)
|
|
94
|
+
),
|
|
95
|
+
React.createElement(
|
|
96
|
+
Box,
|
|
97
|
+
{ paddingLeft: 3 },
|
|
98
|
+
React.createElement(Text, { dimColor: true }, 'Story: '),
|
|
99
|
+
React.createElement(Text, { color: fmt.story === 'none' ? 'gray' : 'yellow' }, fmt.story)
|
|
100
|
+
)
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* SessionPanel component - displays all sessions
|
|
106
|
+
*/
|
|
107
|
+
function SessionPanel({ refreshInterval = 5000 }) {
|
|
108
|
+
const [sessions, setSessions] = React.useState([]);
|
|
109
|
+
const [lastUpdate, setLastUpdate] = React.useState(new Date());
|
|
110
|
+
|
|
111
|
+
// Load sessions on mount and on interval
|
|
112
|
+
React.useEffect(() => {
|
|
113
|
+
function loadSessions() {
|
|
114
|
+
try {
|
|
115
|
+
const result = getSessions();
|
|
116
|
+
setSessions(result.sessions || []);
|
|
117
|
+
setLastUpdate(new Date());
|
|
118
|
+
} catch (e) {
|
|
119
|
+
// Silently handle errors
|
|
120
|
+
setSessions([]);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Initial load
|
|
125
|
+
loadSessions();
|
|
126
|
+
|
|
127
|
+
// Refresh periodically
|
|
128
|
+
const interval = setInterval(loadSessions, refreshInterval);
|
|
129
|
+
|
|
130
|
+
return () => clearInterval(interval);
|
|
131
|
+
}, [refreshInterval]);
|
|
132
|
+
|
|
133
|
+
// Empty state
|
|
134
|
+
if (sessions.length === 0) {
|
|
135
|
+
return React.createElement(
|
|
136
|
+
Box,
|
|
137
|
+
{ flexDirection: 'column', borderStyle: 'single', borderColor: 'gray', padding: 1 },
|
|
138
|
+
React.createElement(
|
|
139
|
+
Text,
|
|
140
|
+
{ bold: true, color: 'cyan' },
|
|
141
|
+
'SESSIONS'
|
|
142
|
+
),
|
|
143
|
+
React.createElement(
|
|
144
|
+
Text,
|
|
145
|
+
{ dimColor: true, italic: true },
|
|
146
|
+
'No active sessions'
|
|
147
|
+
)
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return React.createElement(
|
|
152
|
+
Box,
|
|
153
|
+
{ flexDirection: 'column', borderStyle: 'single', borderColor: 'gray', padding: 1 },
|
|
154
|
+
// Header
|
|
155
|
+
React.createElement(
|
|
156
|
+
Box,
|
|
157
|
+
{ marginBottom: 1 },
|
|
158
|
+
React.createElement(Text, { bold: true, color: 'cyan' }, 'SESSIONS'),
|
|
159
|
+
React.createElement(Text, { dimColor: true }, ` (${sessions.length})`)
|
|
160
|
+
),
|
|
161
|
+
// Session list
|
|
162
|
+
...sessions.map((session, index) =>
|
|
163
|
+
React.createElement(SessionRow, { key: session.id || index, session })
|
|
164
|
+
),
|
|
165
|
+
// Footer with refresh info
|
|
166
|
+
React.createElement(
|
|
167
|
+
Box,
|
|
168
|
+
{ marginTop: 1 },
|
|
169
|
+
React.createElement(
|
|
170
|
+
Text,
|
|
171
|
+
{ dimColor: true, italic: true },
|
|
172
|
+
`Updated: ${lastUpdate.toLocaleTimeString()}`
|
|
173
|
+
)
|
|
174
|
+
)
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
module.exports = { SessionPanel, SessionRow, formatSession, getThreadColor };
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Agent Trace Panel - Quality gate and iteration tracking
|
|
5
|
+
*
|
|
6
|
+
* Shows active agent loops with:
|
|
7
|
+
* - Current gate (tests, coverage, lint, types, visual)
|
|
8
|
+
* - Iteration count and progress
|
|
9
|
+
* - Pass/fail status with color coding
|
|
10
|
+
* - Progress bars for coverage
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const React = require('react');
|
|
14
|
+
const { Box, Text } = require('ink');
|
|
15
|
+
const { getLoopStatus } = require('../lib/loopControl');
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Quality gate definitions
|
|
19
|
+
*/
|
|
20
|
+
const QUALITY_GATES = {
|
|
21
|
+
tests: { name: 'Tests', color: 'green', icon: '✓' },
|
|
22
|
+
coverage: { name: 'Coverage', color: 'magenta', icon: '%' },
|
|
23
|
+
lint: { name: 'Lint', color: 'yellow', icon: '◆' },
|
|
24
|
+
types: { name: 'Types', color: 'blue', icon: '⬡' },
|
|
25
|
+
visual: { name: 'Visual', color: 'cyan', icon: '◉' }
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Get status color
|
|
30
|
+
*/
|
|
31
|
+
function getStatusColor(status) {
|
|
32
|
+
switch (status) {
|
|
33
|
+
case 'passed':
|
|
34
|
+
case 'complete':
|
|
35
|
+
case true:
|
|
36
|
+
return 'green';
|
|
37
|
+
case 'failed':
|
|
38
|
+
case 'error':
|
|
39
|
+
case false:
|
|
40
|
+
return 'red';
|
|
41
|
+
case 'running':
|
|
42
|
+
case 'in_progress':
|
|
43
|
+
return 'cyan';
|
|
44
|
+
case 'pending':
|
|
45
|
+
case 'waiting':
|
|
46
|
+
return 'gray';
|
|
47
|
+
default:
|
|
48
|
+
return 'white';
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Get status symbol
|
|
54
|
+
*/
|
|
55
|
+
function getStatusSymbol(status) {
|
|
56
|
+
switch (status) {
|
|
57
|
+
case 'passed':
|
|
58
|
+
case 'complete':
|
|
59
|
+
case true:
|
|
60
|
+
return '✓';
|
|
61
|
+
case 'failed':
|
|
62
|
+
case 'error':
|
|
63
|
+
case false:
|
|
64
|
+
return '✗';
|
|
65
|
+
case 'running':
|
|
66
|
+
case 'in_progress':
|
|
67
|
+
return '↻';
|
|
68
|
+
case 'pending':
|
|
69
|
+
case 'waiting':
|
|
70
|
+
return '○';
|
|
71
|
+
case 'paused':
|
|
72
|
+
return '⏸';
|
|
73
|
+
default:
|
|
74
|
+
return '•';
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Progress bar component
|
|
80
|
+
*/
|
|
81
|
+
function ProgressBar({ value, max = 100, width = 20, fillChar = '█', emptyChar = '░' }) {
|
|
82
|
+
const percent = Math.min(100, Math.max(0, (value / max) * 100));
|
|
83
|
+
const filled = Math.round((percent / 100) * width);
|
|
84
|
+
const empty = width - filled;
|
|
85
|
+
|
|
86
|
+
const filledStr = fillChar.repeat(filled);
|
|
87
|
+
const emptyStr = emptyChar.repeat(empty);
|
|
88
|
+
|
|
89
|
+
// Color based on progress
|
|
90
|
+
let color = 'red';
|
|
91
|
+
if (percent >= 80) color = 'green';
|
|
92
|
+
else if (percent >= 50) color = 'yellow';
|
|
93
|
+
|
|
94
|
+
return React.createElement(
|
|
95
|
+
Box,
|
|
96
|
+
{ flexDirection: 'row' },
|
|
97
|
+
React.createElement(Text, { color }, filledStr),
|
|
98
|
+
React.createElement(Text, { dimColor: true }, emptyStr),
|
|
99
|
+
React.createElement(Text, { dimColor: true }, ` ${percent.toFixed(0)}%`)
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Quality gate row component
|
|
105
|
+
*/
|
|
106
|
+
function GateRow({ gate, status, value }) {
|
|
107
|
+
const gateConfig = QUALITY_GATES[gate] || { name: gate, color: 'white', icon: '•' };
|
|
108
|
+
const statusColor = getStatusColor(status);
|
|
109
|
+
const statusSymbol = getStatusSymbol(status);
|
|
110
|
+
|
|
111
|
+
return React.createElement(
|
|
112
|
+
Box,
|
|
113
|
+
{ flexDirection: 'row', marginLeft: 2 },
|
|
114
|
+
React.createElement(
|
|
115
|
+
Text,
|
|
116
|
+
{ color: gateConfig.color },
|
|
117
|
+
`${gateConfig.icon} `
|
|
118
|
+
),
|
|
119
|
+
React.createElement(
|
|
120
|
+
Text,
|
|
121
|
+
{ color: statusColor },
|
|
122
|
+
`${statusSymbol} `
|
|
123
|
+
),
|
|
124
|
+
React.createElement(
|
|
125
|
+
Text,
|
|
126
|
+
null,
|
|
127
|
+
`${gateConfig.name}`
|
|
128
|
+
),
|
|
129
|
+
value !== undefined && React.createElement(
|
|
130
|
+
Text,
|
|
131
|
+
{ dimColor: true },
|
|
132
|
+
`: ${value}`
|
|
133
|
+
)
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Active loop trace component
|
|
139
|
+
*/
|
|
140
|
+
function LoopTrace({ loopStatus }) {
|
|
141
|
+
if (!loopStatus || !loopStatus.active) {
|
|
142
|
+
return React.createElement(
|
|
143
|
+
Text,
|
|
144
|
+
{ dimColor: true, italic: true },
|
|
145
|
+
'No active loop'
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const {
|
|
150
|
+
epic,
|
|
151
|
+
currentStory,
|
|
152
|
+
iteration,
|
|
153
|
+
maxIterations,
|
|
154
|
+
paused,
|
|
155
|
+
coverageMode,
|
|
156
|
+
coverageThreshold,
|
|
157
|
+
coverageCurrent,
|
|
158
|
+
visualMode
|
|
159
|
+
} = loopStatus;
|
|
160
|
+
|
|
161
|
+
// Build gates list
|
|
162
|
+
const gates = [];
|
|
163
|
+
|
|
164
|
+
// Tests always present
|
|
165
|
+
gates.push({
|
|
166
|
+
gate: 'tests',
|
|
167
|
+
status: 'running',
|
|
168
|
+
value: null
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// Coverage if enabled
|
|
172
|
+
if (coverageMode) {
|
|
173
|
+
gates.push({
|
|
174
|
+
gate: 'coverage',
|
|
175
|
+
status: coverageCurrent >= coverageThreshold ? 'passed' : 'running',
|
|
176
|
+
value: `${coverageCurrent?.toFixed(1) || 0}% / ${coverageThreshold}%`
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Visual if enabled
|
|
181
|
+
if (visualMode) {
|
|
182
|
+
gates.push({
|
|
183
|
+
gate: 'visual',
|
|
184
|
+
status: 'pending',
|
|
185
|
+
value: null
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return React.createElement(
|
|
190
|
+
Box,
|
|
191
|
+
{ flexDirection: 'column' },
|
|
192
|
+
// Loop header
|
|
193
|
+
React.createElement(
|
|
194
|
+
Box,
|
|
195
|
+
{ flexDirection: 'row', marginBottom: 1 },
|
|
196
|
+
React.createElement(
|
|
197
|
+
Text,
|
|
198
|
+
{ bold: true, color: paused ? 'yellow' : 'green' },
|
|
199
|
+
paused ? '⏸ PAUSED' : '▶ RUNNING'
|
|
200
|
+
),
|
|
201
|
+
React.createElement(Text, { dimColor: true }, ` • ${epic}`)
|
|
202
|
+
),
|
|
203
|
+
// Current story
|
|
204
|
+
React.createElement(
|
|
205
|
+
Box,
|
|
206
|
+
{ flexDirection: 'row' },
|
|
207
|
+
React.createElement(Text, { dimColor: true }, 'Story: '),
|
|
208
|
+
React.createElement(Text, { color: 'cyan' }, currentStory || 'none')
|
|
209
|
+
),
|
|
210
|
+
// Iteration progress
|
|
211
|
+
React.createElement(
|
|
212
|
+
Box,
|
|
213
|
+
{ flexDirection: 'row', marginY: 1 },
|
|
214
|
+
React.createElement(Text, { dimColor: true }, 'Iteration: '),
|
|
215
|
+
React.createElement(
|
|
216
|
+
ProgressBar,
|
|
217
|
+
{ value: iteration || 0, max: maxIterations || 20, width: 15 }
|
|
218
|
+
),
|
|
219
|
+
React.createElement(Text, { dimColor: true }, ` ${iteration || 0}/${maxIterations || 20}`)
|
|
220
|
+
),
|
|
221
|
+
// Coverage progress (if enabled)
|
|
222
|
+
coverageMode && React.createElement(
|
|
223
|
+
Box,
|
|
224
|
+
{ flexDirection: 'row', marginBottom: 1 },
|
|
225
|
+
React.createElement(Text, { dimColor: true }, 'Coverage: '),
|
|
226
|
+
React.createElement(
|
|
227
|
+
ProgressBar,
|
|
228
|
+
{ value: coverageCurrent || 0, max: 100, width: 15 }
|
|
229
|
+
)
|
|
230
|
+
),
|
|
231
|
+
// Quality gates
|
|
232
|
+
React.createElement(
|
|
233
|
+
Box,
|
|
234
|
+
{ flexDirection: 'column', marginTop: 1 },
|
|
235
|
+
React.createElement(Text, { dimColor: true }, 'Quality Gates:'),
|
|
236
|
+
gates.map((g, i) =>
|
|
237
|
+
React.createElement(GateRow, { key: `gate-${i}`, ...g })
|
|
238
|
+
)
|
|
239
|
+
)
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Main Trace Panel component
|
|
245
|
+
*/
|
|
246
|
+
function TracePanel({ visible = true, refreshInterval = 2000 }) {
|
|
247
|
+
const [loopStatus, setLoopStatus] = React.useState(null);
|
|
248
|
+
|
|
249
|
+
React.useEffect(() => {
|
|
250
|
+
if (!visible) return;
|
|
251
|
+
|
|
252
|
+
const loadStatus = () => {
|
|
253
|
+
try {
|
|
254
|
+
const status = getLoopStatus();
|
|
255
|
+
setLoopStatus(status);
|
|
256
|
+
} catch (e) {
|
|
257
|
+
// Ignore errors
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
loadStatus();
|
|
262
|
+
const interval = setInterval(loadStatus, refreshInterval);
|
|
263
|
+
|
|
264
|
+
return () => clearInterval(interval);
|
|
265
|
+
}, [visible, refreshInterval]);
|
|
266
|
+
|
|
267
|
+
if (!visible) {
|
|
268
|
+
return null;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return React.createElement(
|
|
272
|
+
Box,
|
|
273
|
+
{
|
|
274
|
+
flexDirection: 'column',
|
|
275
|
+
borderStyle: 'single',
|
|
276
|
+
borderColor: 'blue',
|
|
277
|
+
padding: 1
|
|
278
|
+
},
|
|
279
|
+
// Header
|
|
280
|
+
React.createElement(
|
|
281
|
+
Box,
|
|
282
|
+
{ marginBottom: 1 },
|
|
283
|
+
React.createElement(
|
|
284
|
+
Text,
|
|
285
|
+
{ bold: true, color: 'blue' },
|
|
286
|
+
'AGENT TRACE'
|
|
287
|
+
),
|
|
288
|
+
React.createElement(
|
|
289
|
+
Text,
|
|
290
|
+
{ dimColor: true },
|
|
291
|
+
' (T to toggle)'
|
|
292
|
+
)
|
|
293
|
+
),
|
|
294
|
+
// Loop trace
|
|
295
|
+
React.createElement(LoopTrace, { loopStatus })
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Compact trace indicator (for status bar)
|
|
301
|
+
*/
|
|
302
|
+
function CompactTrace({ loopStatus }) {
|
|
303
|
+
if (!loopStatus || !loopStatus.active) {
|
|
304
|
+
return React.createElement(
|
|
305
|
+
Text,
|
|
306
|
+
{ dimColor: true },
|
|
307
|
+
'Loop: inactive'
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const { iteration, maxIterations, paused, currentStory } = loopStatus;
|
|
312
|
+
const statusIcon = paused ? '⏸' : '▶';
|
|
313
|
+
const statusColor = paused ? 'yellow' : 'green';
|
|
314
|
+
|
|
315
|
+
return React.createElement(
|
|
316
|
+
Box,
|
|
317
|
+
{ flexDirection: 'row' },
|
|
318
|
+
React.createElement(Text, { color: statusColor }, statusIcon),
|
|
319
|
+
React.createElement(Text, null, ` ${currentStory || '?'} `),
|
|
320
|
+
React.createElement(Text, { dimColor: true }, `[${iteration || 0}/${maxIterations || 20}]`)
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
module.exports = {
|
|
325
|
+
TracePanel,
|
|
326
|
+
LoopTrace,
|
|
327
|
+
GateRow,
|
|
328
|
+
ProgressBar,
|
|
329
|
+
CompactTrace,
|
|
330
|
+
getStatusColor,
|
|
331
|
+
getStatusSymbol,
|
|
332
|
+
QUALITY_GATES
|
|
333
|
+
};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Launch Terminal User Interface for real-time session monitoring
|
|
3
|
+
argument-hint:
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# /agileflow:tui
|
|
7
|
+
|
|
8
|
+
Launch the AgileFlow Terminal User Interface for real-time session visualization.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Purpose
|
|
13
|
+
|
|
14
|
+
The TUI provides a unified dashboard for:
|
|
15
|
+
- **Session monitoring** - See all active sessions and their current stories
|
|
16
|
+
- **Agent visualization** - Watch multi-agent orchestration in real-time
|
|
17
|
+
- **Loop control** - Pause, resume, and manage autonomous loops
|
|
18
|
+
- **Event streaming** - Live feed of agent activity and test results
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## IMMEDIATE ACTIONS
|
|
23
|
+
|
|
24
|
+
Upon invocation, execute these steps:
|
|
25
|
+
|
|
26
|
+
### Step 1: Launch TUI
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
node packages/cli/scripts/tui/index.js
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### Step 2: Display Key Bindings
|
|
33
|
+
|
|
34
|
+
The TUI shows key bindings in the footer:
|
|
35
|
+
- **Q** - Quit TUI
|
|
36
|
+
- **S** - Start loop on current story
|
|
37
|
+
- **P** - Pause active loop
|
|
38
|
+
- **R** - Resume paused loop
|
|
39
|
+
- **T** - Toggle trace panel
|
|
40
|
+
- **1-9** - Switch session focus
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## Key Features
|
|
45
|
+
|
|
46
|
+
### Session Overview Panel
|
|
47
|
+
- Lists all active sessions with ID, branch, and current story
|
|
48
|
+
- Color coding by thread type (base=green, parallel=cyan)
|
|
49
|
+
- Shows loop progress and iteration count
|
|
50
|
+
|
|
51
|
+
### Agent Output Panel
|
|
52
|
+
- Real-time stream of agent messages
|
|
53
|
+
- Timestamps and agent identification
|
|
54
|
+
- Auto-scroll with history buffer
|
|
55
|
+
|
|
56
|
+
### Trace Panel (Toggle with T)
|
|
57
|
+
- Detailed view of agent loops and quality gates
|
|
58
|
+
- Progress bars for each gate (tests, coverage, lint, types)
|
|
59
|
+
- Pass/fail status indicators
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## Example Session
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
┌─────────────────────────────────────────────────────────────────────┐
|
|
67
|
+
│ AgileFlow TUI │
|
|
68
|
+
├─────────────────────────────┬───────────────────────────────────────┤
|
|
69
|
+
│ SESSIONS │ AGENT OUTPUT │
|
|
70
|
+
│ ───────── │ ──────────── │
|
|
71
|
+
│ ▶ Session 1 [main] │ [agileflow-api] Reading status.json │
|
|
72
|
+
│ Story: US-0115 │ [agileflow-api] Running tests... │
|
|
73
|
+
│ Loop: 3/20 iterations │ ✓ 47 tests passed │
|
|
74
|
+
├─────────────────────────────┴───────────────────────────────────────┤
|
|
75
|
+
│ [S]tart [P]ause [R]esume [T]race [Q]uit │
|
|
76
|
+
└─────────────────────────────────────────────────────────────────────┘
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## Related Commands
|
|
82
|
+
|
|
83
|
+
- `/agileflow:babysit MODE=loop` - Autonomous story processing
|
|
84
|
+
- `/agileflow:session:status` - Session information (non-TUI)
|
|
85
|
+
- `/agileflow:session:new` - Create parallel sessions
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## Research
|
|
90
|
+
|
|
91
|
+
Based on Ralph TUI research: `docs/10-research/20260114-ralph-loop-ralph-tui.md`
|
|
@@ -12,6 +12,7 @@ const { Installer } = require('../installers/core/installer');
|
|
|
12
12
|
const { IdeManager } = require('../installers/ide/manager');
|
|
13
13
|
const { displayLogo, displaySection, success, warning, error, info } = require('../lib/ui');
|
|
14
14
|
const { ErrorHandler } = require('../lib/error-handler');
|
|
15
|
+
const { IdeRegistry } = require('../lib/ide-registry');
|
|
15
16
|
|
|
16
17
|
const installer = new Installer();
|
|
17
18
|
const ideManager = new IdeManager();
|
|
@@ -209,11 +210,12 @@ async function handleSet(directory, status, manifestPath, key, value) {
|
|
|
209
210
|
|
|
210
211
|
case 'ides': {
|
|
211
212
|
const newIdes = value.split(',').map(ide => ide.trim());
|
|
212
|
-
const validIdes =
|
|
213
|
+
const validIdes = IdeRegistry.getAll();
|
|
213
214
|
|
|
214
215
|
// Validate IDEs
|
|
215
216
|
for (const ide of newIdes) {
|
|
216
|
-
|
|
217
|
+
const validation = IdeRegistry.validate(ide);
|
|
218
|
+
if (!validation.ok) {
|
|
217
219
|
handler.warning(
|
|
218
220
|
`Invalid IDE: ${ide}`,
|
|
219
221
|
`Valid IDEs: ${validIdes.join(', ')}`,
|
|
@@ -261,10 +263,10 @@ async function handleSet(directory, status, manifestPath, key, value) {
|
|
|
261
263
|
// Remove old IDE configs
|
|
262
264
|
for (const ide of oldIdes) {
|
|
263
265
|
if (!manifest.ides.includes(ide)) {
|
|
264
|
-
const configPath =
|
|
266
|
+
const configPath = IdeRegistry.getConfigPath(ide, directory);
|
|
265
267
|
if (await fs.pathExists(configPath)) {
|
|
266
268
|
await fs.remove(configPath);
|
|
267
|
-
info(`Removed ${
|
|
269
|
+
info(`Removed ${IdeRegistry.getDisplayName(ide)} configuration`);
|
|
268
270
|
}
|
|
269
271
|
}
|
|
270
272
|
}
|
|
@@ -272,7 +274,7 @@ async function handleSet(directory, status, manifestPath, key, value) {
|
|
|
272
274
|
// Add new IDE configs
|
|
273
275
|
for (const ide of manifest.ides) {
|
|
274
276
|
await ideManager.setup(ide, directory, status.path);
|
|
275
|
-
success(`Updated ${
|
|
277
|
+
success(`Updated ${IdeRegistry.getDisplayName(ide)} configuration`);
|
|
276
278
|
}
|
|
277
279
|
|
|
278
280
|
console.log();
|
|
@@ -282,28 +284,3 @@ async function handleSet(directory, status, manifestPath, key, value) {
|
|
|
282
284
|
console.log();
|
|
283
285
|
}
|
|
284
286
|
|
|
285
|
-
/**
|
|
286
|
-
* Get IDE config path
|
|
287
|
-
*/
|
|
288
|
-
function getIdeConfigPath(projectDir, ide) {
|
|
289
|
-
const paths = {
|
|
290
|
-
'claude-code': '.claude/commands/agileflow',
|
|
291
|
-
cursor: '.cursor/rules/agileflow',
|
|
292
|
-
windsurf: '.windsurf/workflows/agileflow',
|
|
293
|
-
};
|
|
294
|
-
|
|
295
|
-
return path.join(projectDir, paths[ide] || '');
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
/**
|
|
299
|
-
* Format IDE name for display
|
|
300
|
-
*/
|
|
301
|
-
function formatIdeName(ide) {
|
|
302
|
-
const names = {
|
|
303
|
-
'claude-code': 'Claude Code',
|
|
304
|
-
cursor: 'Cursor',
|
|
305
|
-
windsurf: 'Windsurf',
|
|
306
|
-
};
|
|
307
|
-
|
|
308
|
-
return names[ide] || ide;
|
|
309
|
-
}
|