agileflow 2.89.2 → 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 +10 -0
- package/README.md +3 -3
- package/lib/content-sanitizer.js +463 -0
- package/lib/error-codes.js +544 -0
- package/lib/errors.js +336 -5
- package/lib/feedback.js +561 -0
- package/lib/path-resolver.js +396 -0
- package/lib/placeholder-registry.js +617 -0
- package/lib/session-registry.js +461 -0
- package/lib/smart-json-file.js +653 -0
- 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 +38 -584
- package/package.json +4 -1
- package/scripts/agileflow-configure.js +40 -1440
- package/scripts/agileflow-welcome.js +2 -1
- package/scripts/check-update.js +16 -3
- package/scripts/lib/configure-detect.js +383 -0
- package/scripts/lib/configure-features.js +811 -0
- package/scripts/lib/configure-repair.js +314 -0
- package/scripts/lib/configure-utils.js +115 -0
- package/scripts/lib/frontmatter-parser.js +3 -3
- package/scripts/lib/sessionRegistry.js +682 -0
- package/scripts/obtain-context.js +417 -113
- package/scripts/ralph-loop.js +1 -1
- 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 +10 -33
- package/tools/cli/commands/doctor.js +48 -40
- package/tools/cli/commands/list.js +49 -37
- package/tools/cli/commands/status.js +13 -37
- package/tools/cli/commands/uninstall.js +12 -41
- package/tools/cli/installers/core/installer.js +75 -12
- package/tools/cli/installers/ide/_interface.js +238 -0
- package/tools/cli/installers/ide/codex.js +2 -2
- package/tools/cli/installers/ide/manager.js +15 -0
- package/tools/cli/lib/command-context.js +374 -0
- package/tools/cli/lib/config-manager.js +394 -0
- package/tools/cli/lib/content-injector.js +69 -16
- package/tools/cli/lib/ide-errors.js +163 -29
- 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,371 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Loop Control - Pause/Resume mechanism for Ralph Loop
|
|
5
|
+
*
|
|
6
|
+
* Provides file-based pause/resume signals that ralph-loop.js checks
|
|
7
|
+
* between iterations. This allows external control (TUI, CLI) to
|
|
8
|
+
* pause ongoing loops without interrupting mid-work.
|
|
9
|
+
*
|
|
10
|
+
* Mechanism:
|
|
11
|
+
* - Pause: Create .agileflow/sessions/loop.pause file
|
|
12
|
+
* - Resume: Remove the pause file
|
|
13
|
+
* - Status: Check if file exists
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const fs = require('fs');
|
|
17
|
+
const path = require('path');
|
|
18
|
+
const EventEmitter = require('events');
|
|
19
|
+
|
|
20
|
+
// Get project root
|
|
21
|
+
let getProjectRoot;
|
|
22
|
+
try {
|
|
23
|
+
getProjectRoot = require('../../../lib/paths').getProjectRoot;
|
|
24
|
+
} catch (e) {
|
|
25
|
+
getProjectRoot = () => process.cwd();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Get safe JSON utilities
|
|
29
|
+
let safeReadJSON, safeWriteJSON;
|
|
30
|
+
try {
|
|
31
|
+
const errors = require('../../../lib/errors');
|
|
32
|
+
safeReadJSON = errors.safeReadJSON;
|
|
33
|
+
safeWriteJSON = errors.safeWriteJSON;
|
|
34
|
+
} catch (e) {
|
|
35
|
+
safeReadJSON = (path, opts = {}) => {
|
|
36
|
+
try {
|
|
37
|
+
const data = JSON.parse(fs.readFileSync(path, 'utf8'));
|
|
38
|
+
return { ok: true, data };
|
|
39
|
+
} catch (e) {
|
|
40
|
+
return { ok: false, error: e.message, data: opts.defaultValue };
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
safeWriteJSON = (path, data) => {
|
|
44
|
+
fs.writeFileSync(path, JSON.stringify(data, null, 2));
|
|
45
|
+
return { ok: true };
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Get pause file path
|
|
51
|
+
*/
|
|
52
|
+
function getPauseFilePath(sessionId = 'default') {
|
|
53
|
+
const rootDir = getProjectRoot();
|
|
54
|
+
return path.join(rootDir, '.agileflow', 'sessions', `${sessionId}.pause`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Get session state path
|
|
59
|
+
*/
|
|
60
|
+
function getSessionStatePath() {
|
|
61
|
+
const rootDir = getProjectRoot();
|
|
62
|
+
return path.join(rootDir, 'docs', '09-agents', 'session-state.json');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Check if loop is paused
|
|
67
|
+
*/
|
|
68
|
+
function isPaused(sessionId = 'default') {
|
|
69
|
+
const pauseFile = getPauseFilePath(sessionId);
|
|
70
|
+
return fs.existsSync(pauseFile);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Pause the loop
|
|
75
|
+
* Creates pause file and optionally updates session state
|
|
76
|
+
*/
|
|
77
|
+
function pause(sessionId = 'default', reason = 'user_request') {
|
|
78
|
+
const pauseFile = getPauseFilePath(sessionId);
|
|
79
|
+
|
|
80
|
+
// Ensure directory exists
|
|
81
|
+
const dir = path.dirname(pauseFile);
|
|
82
|
+
if (!fs.existsSync(dir)) {
|
|
83
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Create pause file with metadata
|
|
87
|
+
const pauseData = {
|
|
88
|
+
paused_at: new Date().toISOString(),
|
|
89
|
+
reason,
|
|
90
|
+
session_id: sessionId
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
fs.writeFileSync(pauseFile, JSON.stringify(pauseData, null, 2));
|
|
94
|
+
|
|
95
|
+
// Update session state
|
|
96
|
+
const statePath = getSessionStatePath();
|
|
97
|
+
const result = safeReadJSON(statePath, { defaultValue: {} });
|
|
98
|
+
const state = result.ok ? result.data : {};
|
|
99
|
+
|
|
100
|
+
if (state.ralph_loop) {
|
|
101
|
+
state.ralph_loop.paused = true;
|
|
102
|
+
state.ralph_loop.paused_at = pauseData.paused_at;
|
|
103
|
+
state.ralph_loop.pause_reason = reason;
|
|
104
|
+
safeWriteJSON(statePath, state);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return pauseData;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Resume the loop
|
|
112
|
+
* Removes pause file and updates session state
|
|
113
|
+
*/
|
|
114
|
+
function resume(sessionId = 'default') {
|
|
115
|
+
const pauseFile = getPauseFilePath(sessionId);
|
|
116
|
+
|
|
117
|
+
// Get pause data before removing
|
|
118
|
+
let pauseData = null;
|
|
119
|
+
if (fs.existsSync(pauseFile)) {
|
|
120
|
+
try {
|
|
121
|
+
pauseData = JSON.parse(fs.readFileSync(pauseFile, 'utf8'));
|
|
122
|
+
} catch (e) {
|
|
123
|
+
// Ignore parse errors
|
|
124
|
+
}
|
|
125
|
+
fs.unlinkSync(pauseFile);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Update session state
|
|
129
|
+
const statePath = getSessionStatePath();
|
|
130
|
+
const result = safeReadJSON(statePath, { defaultValue: {} });
|
|
131
|
+
const state = result.ok ? result.data : {};
|
|
132
|
+
|
|
133
|
+
if (state.ralph_loop) {
|
|
134
|
+
state.ralph_loop.paused = false;
|
|
135
|
+
state.ralph_loop.resumed_at = new Date().toISOString();
|
|
136
|
+
delete state.ralph_loop.paused_at;
|
|
137
|
+
delete state.ralph_loop.pause_reason;
|
|
138
|
+
safeWriteJSON(statePath, state);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
resumed_at: new Date().toISOString(),
|
|
143
|
+
was_paused: pauseData !== null,
|
|
144
|
+
pause_data: pauseData
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Get loop status
|
|
150
|
+
*/
|
|
151
|
+
function getLoopStatus() {
|
|
152
|
+
const statePath = getSessionStatePath();
|
|
153
|
+
const result = safeReadJSON(statePath, { defaultValue: {} });
|
|
154
|
+
const state = result.ok ? result.data : {};
|
|
155
|
+
|
|
156
|
+
const loop = state.ralph_loop;
|
|
157
|
+
if (!loop || !loop.enabled) {
|
|
158
|
+
return {
|
|
159
|
+
active: false,
|
|
160
|
+
paused: false,
|
|
161
|
+
message: 'Loop not active'
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const paused = isPaused() || loop.paused;
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
active: true,
|
|
169
|
+
paused,
|
|
170
|
+
epic: loop.epic,
|
|
171
|
+
currentStory: loop.current_story,
|
|
172
|
+
iteration: loop.iteration || 0,
|
|
173
|
+
maxIterations: loop.max_iterations || 20,
|
|
174
|
+
visualMode: loop.visual_mode || false,
|
|
175
|
+
coverageMode: loop.coverage_mode || false,
|
|
176
|
+
coverageThreshold: loop.coverage_threshold || 80,
|
|
177
|
+
coverageCurrent: loop.coverage_current || 0,
|
|
178
|
+
pausedAt: loop.paused_at,
|
|
179
|
+
pauseReason: loop.pause_reason,
|
|
180
|
+
startedAt: loop.started_at
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Stop the loop completely
|
|
186
|
+
*/
|
|
187
|
+
function stopLoop(reason = 'user_request') {
|
|
188
|
+
const statePath = getSessionStatePath();
|
|
189
|
+
const result = safeReadJSON(statePath, { defaultValue: {} });
|
|
190
|
+
const state = result.ok ? result.data : {};
|
|
191
|
+
|
|
192
|
+
if (state.ralph_loop) {
|
|
193
|
+
state.ralph_loop.enabled = false;
|
|
194
|
+
state.ralph_loop.stopped_at = new Date().toISOString();
|
|
195
|
+
state.ralph_loop.stopped_reason = reason;
|
|
196
|
+
safeWriteJSON(statePath, state);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Also remove any pause file
|
|
200
|
+
const pauseFile = getPauseFilePath();
|
|
201
|
+
if (fs.existsSync(pauseFile)) {
|
|
202
|
+
fs.unlinkSync(pauseFile);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return {
|
|
206
|
+
stopped: true,
|
|
207
|
+
reason,
|
|
208
|
+
stopped_at: new Date().toISOString()
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* LoopController class - EventEmitter for loop control
|
|
214
|
+
*
|
|
215
|
+
* Emits events:
|
|
216
|
+
* - 'paused' - Loop was paused
|
|
217
|
+
* - 'resumed' - Loop was resumed
|
|
218
|
+
* - 'stopped' - Loop was stopped
|
|
219
|
+
* - 'statusChange' - Loop status changed
|
|
220
|
+
*/
|
|
221
|
+
class LoopController extends EventEmitter {
|
|
222
|
+
constructor(options = {}) {
|
|
223
|
+
super();
|
|
224
|
+
|
|
225
|
+
this.sessionId = options.sessionId || 'default';
|
|
226
|
+
this.pollInterval = options.pollInterval || 1000;
|
|
227
|
+
this.lastStatus = null;
|
|
228
|
+
this.pollTimer = null;
|
|
229
|
+
this.isWatching = false;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Start watching for status changes
|
|
234
|
+
*/
|
|
235
|
+
startWatching() {
|
|
236
|
+
if (this.isWatching) return;
|
|
237
|
+
|
|
238
|
+
this.lastStatus = getLoopStatus();
|
|
239
|
+
this.isWatching = true;
|
|
240
|
+
|
|
241
|
+
this.pollTimer = setInterval(() => {
|
|
242
|
+
this._checkStatus();
|
|
243
|
+
}, this.pollInterval);
|
|
244
|
+
|
|
245
|
+
this.emit('started');
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Stop watching
|
|
250
|
+
*/
|
|
251
|
+
stopWatching() {
|
|
252
|
+
if (!this.isWatching) return;
|
|
253
|
+
|
|
254
|
+
if (this.pollTimer) {
|
|
255
|
+
clearInterval(this.pollTimer);
|
|
256
|
+
this.pollTimer = null;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
this.isWatching = false;
|
|
260
|
+
this.emit('stopped');
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Check for status changes
|
|
265
|
+
*/
|
|
266
|
+
_checkStatus() {
|
|
267
|
+
const newStatus = getLoopStatus();
|
|
268
|
+
|
|
269
|
+
if (!this.lastStatus) {
|
|
270
|
+
this.lastStatus = newStatus;
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Check for changes
|
|
275
|
+
if (this.lastStatus.paused !== newStatus.paused) {
|
|
276
|
+
if (newStatus.paused) {
|
|
277
|
+
this.emit('paused', {
|
|
278
|
+
pausedAt: newStatus.pausedAt,
|
|
279
|
+
reason: newStatus.pauseReason
|
|
280
|
+
});
|
|
281
|
+
} else {
|
|
282
|
+
this.emit('resumed');
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (this.lastStatus.active !== newStatus.active) {
|
|
287
|
+
if (!newStatus.active && this.lastStatus.active) {
|
|
288
|
+
this.emit('loopStopped');
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (this.lastStatus.iteration !== newStatus.iteration) {
|
|
293
|
+
this.emit('iteration', {
|
|
294
|
+
iteration: newStatus.iteration,
|
|
295
|
+
maxIterations: newStatus.maxIterations
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (this.lastStatus.currentStory !== newStatus.currentStory) {
|
|
300
|
+
this.emit('storyChange', {
|
|
301
|
+
previousStory: this.lastStatus.currentStory,
|
|
302
|
+
currentStory: newStatus.currentStory
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Always emit statusChange if anything changed
|
|
307
|
+
const changed = JSON.stringify(this.lastStatus) !== JSON.stringify(newStatus);
|
|
308
|
+
if (changed) {
|
|
309
|
+
this.emit('statusChange', newStatus);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
this.lastStatus = newStatus;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Pause the loop
|
|
317
|
+
*/
|
|
318
|
+
pause(reason = 'user_request') {
|
|
319
|
+
const result = pause(this.sessionId, reason);
|
|
320
|
+
this._checkStatus();
|
|
321
|
+
return result;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Resume the loop
|
|
326
|
+
*/
|
|
327
|
+
resume() {
|
|
328
|
+
const result = resume(this.sessionId);
|
|
329
|
+
this._checkStatus();
|
|
330
|
+
return result;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Stop the loop
|
|
335
|
+
*/
|
|
336
|
+
stop(reason = 'user_request') {
|
|
337
|
+
const result = stopLoop(reason);
|
|
338
|
+
this._checkStatus();
|
|
339
|
+
return result;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Get current status
|
|
344
|
+
*/
|
|
345
|
+
getStatus() {
|
|
346
|
+
return getLoopStatus();
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Default controller instance
|
|
352
|
+
*/
|
|
353
|
+
let defaultController = null;
|
|
354
|
+
|
|
355
|
+
function getDefaultController() {
|
|
356
|
+
if (!defaultController) {
|
|
357
|
+
defaultController = new LoopController();
|
|
358
|
+
}
|
|
359
|
+
return defaultController;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
module.exports = {
|
|
363
|
+
isPaused,
|
|
364
|
+
pause,
|
|
365
|
+
resume,
|
|
366
|
+
getLoopStatus,
|
|
367
|
+
stopLoop,
|
|
368
|
+
getPauseFilePath,
|
|
369
|
+
LoopController,
|
|
370
|
+
getDefaultController
|
|
371
|
+
};
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Agent Output Panel - Live updates from agent event stream
|
|
5
|
+
*
|
|
6
|
+
* Displays real-time agent messages with timestamps and agent names.
|
|
7
|
+
* Auto-scrolls to bottom, limits display to configurable message count.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const React = require('react');
|
|
11
|
+
const { Box, Text, Newline } = require('ink');
|
|
12
|
+
const { EventStream, formatEvent } = require('../lib/eventStream');
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Get color for agent type
|
|
16
|
+
*/
|
|
17
|
+
function getAgentColor(agent) {
|
|
18
|
+
const colors = {
|
|
19
|
+
'agileflow-api': 'cyan',
|
|
20
|
+
'agileflow-ui': 'magenta',
|
|
21
|
+
'agileflow-testing': 'yellow',
|
|
22
|
+
'agileflow-ci': 'green',
|
|
23
|
+
'agileflow-security': 'red',
|
|
24
|
+
'agileflow-devops': 'blue',
|
|
25
|
+
'agileflow-database': 'white',
|
|
26
|
+
'agileflow-performance': 'cyan',
|
|
27
|
+
'agileflow-documentation': 'gray'
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// Check for partial matches
|
|
31
|
+
for (const [key, color] of Object.entries(colors)) {
|
|
32
|
+
if (agent && agent.includes(key.replace('agileflow-', ''))) {
|
|
33
|
+
return color;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return 'gray';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Get status indicator for event type
|
|
42
|
+
*/
|
|
43
|
+
function getStatusIndicator(eventType) {
|
|
44
|
+
switch (eventType) {
|
|
45
|
+
case 'init':
|
|
46
|
+
return { symbol: '▶', color: 'blue' };
|
|
47
|
+
case 'iteration':
|
|
48
|
+
return { symbol: '↻', color: 'cyan' };
|
|
49
|
+
case 'passed':
|
|
50
|
+
return { symbol: '✓', color: 'green' };
|
|
51
|
+
case 'failed':
|
|
52
|
+
return { symbol: '✗', color: 'red' };
|
|
53
|
+
case 'abort':
|
|
54
|
+
return { symbol: '⊘', color: 'yellow' };
|
|
55
|
+
default:
|
|
56
|
+
return { symbol: '•', color: 'gray' };
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Single output row component
|
|
62
|
+
*/
|
|
63
|
+
function OutputRow({ event, showTimestamp = true }) {
|
|
64
|
+
const formatted = formatEvent(event);
|
|
65
|
+
const agentColor = getAgentColor(formatted.agent);
|
|
66
|
+
const status = getStatusIndicator(formatted.eventType);
|
|
67
|
+
|
|
68
|
+
return React.createElement(
|
|
69
|
+
Box,
|
|
70
|
+
{ flexDirection: 'row' },
|
|
71
|
+
// Timestamp
|
|
72
|
+
showTimestamp && formatted.timestamp && React.createElement(
|
|
73
|
+
Text,
|
|
74
|
+
{ dimColor: true },
|
|
75
|
+
`[${formatted.timestamp}] `
|
|
76
|
+
),
|
|
77
|
+
// Status indicator
|
|
78
|
+
React.createElement(
|
|
79
|
+
Text,
|
|
80
|
+
{ color: status.color },
|
|
81
|
+
`${status.symbol} `
|
|
82
|
+
),
|
|
83
|
+
// Agent name
|
|
84
|
+
React.createElement(
|
|
85
|
+
Text,
|
|
86
|
+
{ color: agentColor, bold: true },
|
|
87
|
+
`[${formatted.agent}] `
|
|
88
|
+
),
|
|
89
|
+
// Message
|
|
90
|
+
React.createElement(
|
|
91
|
+
Text,
|
|
92
|
+
null,
|
|
93
|
+
formatted.message
|
|
94
|
+
)
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Main Output Panel component
|
|
100
|
+
*/
|
|
101
|
+
function OutputPanel({
|
|
102
|
+
maxMessages = 100,
|
|
103
|
+
showTimestamp = true,
|
|
104
|
+
logPath = null,
|
|
105
|
+
title = 'AGENT OUTPUT'
|
|
106
|
+
}) {
|
|
107
|
+
const [messages, setMessages] = React.useState([]);
|
|
108
|
+
const [isConnected, setIsConnected] = React.useState(false);
|
|
109
|
+
const [error, setError] = React.useState(null);
|
|
110
|
+
const streamRef = React.useRef(null);
|
|
111
|
+
|
|
112
|
+
React.useEffect(() => {
|
|
113
|
+
// Create event stream
|
|
114
|
+
const options = logPath ? { logPath, emitHistory: true, historyLimit: 10 } : { emitHistory: true, historyLimit: 10 };
|
|
115
|
+
const stream = new EventStream(options);
|
|
116
|
+
streamRef.current = stream;
|
|
117
|
+
|
|
118
|
+
// Handle events
|
|
119
|
+
stream.on('event', (event) => {
|
|
120
|
+
setMessages((prev) => {
|
|
121
|
+
const newMessages = [...prev, event];
|
|
122
|
+
// Limit to maxMessages
|
|
123
|
+
if (newMessages.length > maxMessages) {
|
|
124
|
+
return newMessages.slice(-maxMessages);
|
|
125
|
+
}
|
|
126
|
+
return newMessages;
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
stream.on('started', () => {
|
|
131
|
+
setIsConnected(true);
|
|
132
|
+
setError(null);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
stream.on('stopped', () => {
|
|
136
|
+
setIsConnected(false);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
stream.on('error', (err) => {
|
|
140
|
+
setError(err.message);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
stream.on('truncated', () => {
|
|
144
|
+
// Clear messages on log rotation
|
|
145
|
+
setMessages([]);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// Start watching
|
|
149
|
+
stream.start();
|
|
150
|
+
|
|
151
|
+
// Cleanup
|
|
152
|
+
return () => {
|
|
153
|
+
if (streamRef.current) {
|
|
154
|
+
streamRef.current.stop();
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
}, [logPath, maxMessages]);
|
|
158
|
+
|
|
159
|
+
// Render panel
|
|
160
|
+
return React.createElement(
|
|
161
|
+
Box,
|
|
162
|
+
{
|
|
163
|
+
flexDirection: 'column',
|
|
164
|
+
borderStyle: 'single',
|
|
165
|
+
borderColor: isConnected ? 'green' : 'gray',
|
|
166
|
+
padding: 1,
|
|
167
|
+
flexGrow: 1
|
|
168
|
+
},
|
|
169
|
+
// Header
|
|
170
|
+
React.createElement(
|
|
171
|
+
Box,
|
|
172
|
+
{ marginBottom: 1 },
|
|
173
|
+
React.createElement(
|
|
174
|
+
Text,
|
|
175
|
+
{ bold: true, color: 'cyan' },
|
|
176
|
+
title
|
|
177
|
+
),
|
|
178
|
+
React.createElement(
|
|
179
|
+
Text,
|
|
180
|
+
{ dimColor: true },
|
|
181
|
+
` (${messages.length}/${maxMessages})`
|
|
182
|
+
),
|
|
183
|
+
isConnected && React.createElement(
|
|
184
|
+
Text,
|
|
185
|
+
{ color: 'green' },
|
|
186
|
+
' ●'
|
|
187
|
+
),
|
|
188
|
+
!isConnected && !error && React.createElement(
|
|
189
|
+
Text,
|
|
190
|
+
{ color: 'yellow' },
|
|
191
|
+
' ○'
|
|
192
|
+
),
|
|
193
|
+
error && React.createElement(
|
|
194
|
+
Text,
|
|
195
|
+
{ color: 'red' },
|
|
196
|
+
' ✗'
|
|
197
|
+
)
|
|
198
|
+
),
|
|
199
|
+
// Messages or placeholder
|
|
200
|
+
messages.length === 0
|
|
201
|
+
? React.createElement(
|
|
202
|
+
Text,
|
|
203
|
+
{ dimColor: true, italic: true },
|
|
204
|
+
error ? `Error: ${error}` : 'Waiting for agent activity...'
|
|
205
|
+
)
|
|
206
|
+
: messages.map((event, index) =>
|
|
207
|
+
React.createElement(OutputRow, {
|
|
208
|
+
key: `msg-${index}`,
|
|
209
|
+
event,
|
|
210
|
+
showTimestamp
|
|
211
|
+
})
|
|
212
|
+
)
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Compact output view (for split panels)
|
|
218
|
+
*/
|
|
219
|
+
function CompactOutput({ maxLines = 5, logPath = null }) {
|
|
220
|
+
const [messages, setMessages] = React.useState([]);
|
|
221
|
+
const streamRef = React.useRef(null);
|
|
222
|
+
|
|
223
|
+
React.useEffect(() => {
|
|
224
|
+
const options = logPath ? { logPath } : {};
|
|
225
|
+
const stream = new EventStream(options);
|
|
226
|
+
streamRef.current = stream;
|
|
227
|
+
|
|
228
|
+
stream.on('event', (event) => {
|
|
229
|
+
setMessages((prev) => {
|
|
230
|
+
const newMessages = [...prev, event];
|
|
231
|
+
return newMessages.slice(-maxLines);
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
stream.start();
|
|
236
|
+
|
|
237
|
+
return () => {
|
|
238
|
+
if (streamRef.current) {
|
|
239
|
+
streamRef.current.stop();
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
}, [logPath, maxLines]);
|
|
243
|
+
|
|
244
|
+
if (messages.length === 0) {
|
|
245
|
+
return React.createElement(
|
|
246
|
+
Text,
|
|
247
|
+
{ dimColor: true },
|
|
248
|
+
'No recent activity'
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return React.createElement(
|
|
253
|
+
Box,
|
|
254
|
+
{ flexDirection: 'column' },
|
|
255
|
+
messages.map((event, index) => {
|
|
256
|
+
const formatted = formatEvent(event);
|
|
257
|
+
const status = getStatusIndicator(formatted.eventType);
|
|
258
|
+
return React.createElement(
|
|
259
|
+
Text,
|
|
260
|
+
{ key: `compact-${index}` },
|
|
261
|
+
React.createElement(Text, { color: status.color }, status.symbol),
|
|
262
|
+
' ',
|
|
263
|
+
React.createElement(Text, { dimColor: true }, `[${formatted.agent}]`),
|
|
264
|
+
' ',
|
|
265
|
+
formatted.message.substring(0, 50),
|
|
266
|
+
formatted.message.length > 50 ? '...' : ''
|
|
267
|
+
);
|
|
268
|
+
})
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
module.exports = {
|
|
273
|
+
OutputPanel,
|
|
274
|
+
OutputRow,
|
|
275
|
+
CompactOutput,
|
|
276
|
+
getAgentColor,
|
|
277
|
+
getStatusIndicator
|
|
278
|
+
};
|