agileflow 2.90.2 → 2.90.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agileflow",
3
- "version": "2.90.2",
3
+ "version": "2.90.3",
4
4
  "description": "AI-driven agile development system for Claude Code, Cursor, Windsurf, and more",
5
5
  "keywords": [
6
6
  "agile",
@@ -0,0 +1,277 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * AgileFlow TUI Dashboard
5
+ *
6
+ * Main layout component using Ink's Flexbox for responsive design.
7
+ * Adapts to terminal size automatically.
8
+ *
9
+ * Layout:
10
+ * ┌────────────────────────────────────────────────────────────────┐
11
+ * │ AgileFlow TUI v2.x │
12
+ * ├──────────────────┬─────────────────────────────────────────────┤
13
+ * │ SESSIONS │ AGENT OUTPUT │
14
+ * │ ──────── │ ──────────── │
15
+ * │ ▶ Session 1 │ [10:30] [api] Running tests... │
16
+ * │ Branch: main │ [10:31] [api] ✓ 47 tests passed │
17
+ * │ Story: US-0115 │ [10:32] [ci] Building project... │
18
+ * │ │ │
19
+ * │ Session 2 │ │
20
+ * │ Branch: feat │ │
21
+ * ├──────────────────┴─────────────────────────────────────────────┤
22
+ * │ Loop: EP-0020 US-0115 | ████████░░░░ 65% | Q S P R T 1-9 │
23
+ * └────────────────────────────────────────────────────────────────┘
24
+ */
25
+
26
+ const React = require('react');
27
+ const { Box, Text, useInput, useApp, useStdout } = require('ink');
28
+ const { SessionPanel } = require('./panels/SessionPanel');
29
+ const { OutputPanel } = require('./panels/OutputPanel');
30
+ const { TracePanel } = require('./panels/TracePanel');
31
+ const { getLoopStatus } = require('./lib/loopControl');
32
+
33
+ /**
34
+ * Version display
35
+ */
36
+ const VERSION = '2.90.2';
37
+
38
+ /**
39
+ * Status bar showing loop progress and key bindings
40
+ */
41
+ function StatusBar({ loopStatus, showTrace }) {
42
+ const progress = loopStatus?.progress || 0;
43
+ const epic = loopStatus?.epic || '';
44
+ const story = loopStatus?.story || '';
45
+ const state = loopStatus?.state || 'idle';
46
+
47
+ // Build progress bar
48
+ const barWidth = 15;
49
+ const filled = Math.round((progress / 100) * barWidth);
50
+ const empty = barWidth - filled;
51
+ const progressBar = '█'.repeat(filled) + '░'.repeat(empty);
52
+
53
+ // Progress color
54
+ let progressColor = 'red';
55
+ if (progress >= 80) progressColor = 'green';
56
+ else if (progress >= 50) progressColor = 'yellow';
57
+ else if (progress >= 20) progressColor = 'cyan';
58
+
59
+ // State indicator
60
+ const stateColors = {
61
+ running: 'green',
62
+ paused: 'yellow',
63
+ idle: 'gray',
64
+ error: 'red',
65
+ };
66
+
67
+ return React.createElement(
68
+ Box,
69
+ {
70
+ borderStyle: 'single',
71
+ borderColor: 'gray',
72
+ paddingX: 1,
73
+ justifyContent: 'space-between',
74
+ flexShrink: 0,
75
+ },
76
+ // Left: Loop info
77
+ React.createElement(
78
+ Box,
79
+ { flexDirection: 'row' },
80
+ React.createElement(Text, { color: stateColors[state] || 'gray' }, '● '),
81
+ epic && React.createElement(Text, { bold: true }, `${epic} `),
82
+ story && React.createElement(Text, { color: 'yellow' }, story),
83
+ !epic && !story && React.createElement(Text, { dimColor: true }, 'No active loop')
84
+ ),
85
+ // Center: Progress bar
86
+ React.createElement(
87
+ Box,
88
+ { flexDirection: 'row' },
89
+ React.createElement(Text, { color: progressColor }, progressBar),
90
+ React.createElement(Text, null, ` ${progress}%`)
91
+ ),
92
+ // Right: Key bindings
93
+ React.createElement(
94
+ Box,
95
+ { flexDirection: 'row' },
96
+ React.createElement(Text, { dimColor: true }, '['),
97
+ React.createElement(Text, { color: 'cyan' }, 'Q'),
98
+ React.createElement(Text, { dimColor: true }, ']uit ['),
99
+ React.createElement(Text, { color: 'green' }, 'S'),
100
+ React.createElement(Text, { dimColor: true }, ']tart ['),
101
+ React.createElement(Text, { color: 'yellow' }, 'P'),
102
+ React.createElement(Text, { dimColor: true }, ']ause ['),
103
+ React.createElement(Text, { color: 'cyan' }, 'R'),
104
+ React.createElement(Text, { dimColor: true }, ']esume ['),
105
+ React.createElement(Text, { color: showTrace ? 'green' : 'gray' }, 'T'),
106
+ React.createElement(Text, { dimColor: true }, ']race'),
107
+ React.createElement(Text, { dimColor: true }, ' | '),
108
+ React.createElement(Text, { color: 'blue' }, `v${VERSION}`)
109
+ )
110
+ );
111
+ }
112
+
113
+ /**
114
+ * Main Dashboard component
115
+ */
116
+ function Dashboard({ onAction = null }) {
117
+ const { exit } = useApp();
118
+ const { stdout } = useStdout();
119
+
120
+ // Terminal dimensions
121
+ const [dimensions, setDimensions] = React.useState({
122
+ width: stdout?.columns || 80,
123
+ height: stdout?.rows || 24,
124
+ });
125
+
126
+ // UI state
127
+ const [showTrace, setShowTrace] = React.useState(false);
128
+ const [loopStatus, setLoopStatus] = React.useState(null);
129
+ const [selectedSession, setSelectedSession] = React.useState(1);
130
+
131
+ // Handle terminal resize
132
+ React.useEffect(() => {
133
+ const handleResize = () => {
134
+ setDimensions({
135
+ width: stdout?.columns || 80,
136
+ height: stdout?.rows || 24,
137
+ });
138
+ };
139
+
140
+ stdout?.on('resize', handleResize);
141
+ return () => stdout?.off('resize', handleResize);
142
+ }, [stdout]);
143
+
144
+ // Load loop status periodically
145
+ React.useEffect(() => {
146
+ const loadStatus = () => {
147
+ try {
148
+ const status = getLoopStatus();
149
+ setLoopStatus(status);
150
+ } catch (e) {
151
+ // Silently ignore errors
152
+ }
153
+ };
154
+
155
+ loadStatus();
156
+ const interval = setInterval(loadStatus, 2000);
157
+ return () => clearInterval(interval);
158
+ }, []);
159
+
160
+ // Handle keyboard input
161
+ useInput((input, key) => {
162
+ const lowerInput = input.toLowerCase();
163
+
164
+ // Quit
165
+ if (lowerInput === 'q' || (key.ctrl && lowerInput === 'c')) {
166
+ exit();
167
+ return;
168
+ }
169
+
170
+ // Toggle trace
171
+ if (lowerInput === 't') {
172
+ setShowTrace(prev => !prev);
173
+ return;
174
+ }
175
+
176
+ // Session selection (1-9)
177
+ if (input >= '1' && input <= '9') {
178
+ setSelectedSession(parseInt(input, 10));
179
+ return;
180
+ }
181
+
182
+ // Forward actions to parent
183
+ if (onAction) {
184
+ if (lowerInput === 's') onAction({ action: 'start' });
185
+ if (lowerInput === 'p') onAction({ action: 'pause' });
186
+ if (lowerInput === 'r') onAction({ action: 'resume' });
187
+ }
188
+ });
189
+
190
+ // Calculate panel widths based on terminal size
191
+ const isNarrow = dimensions.width < 100;
192
+ const sessionPanelWidth = isNarrow ? 25 : 30;
193
+
194
+ // Main content height (subtract header + status bar)
195
+ const contentHeight = Math.max(10, dimensions.height - 6);
196
+
197
+ return React.createElement(
198
+ Box,
199
+ {
200
+ flexDirection: 'column',
201
+ width: dimensions.width,
202
+ height: dimensions.height,
203
+ },
204
+ // Header
205
+ React.createElement(
206
+ Box,
207
+ {
208
+ borderStyle: 'double',
209
+ borderColor: 'cyan',
210
+ justifyContent: 'center',
211
+ paddingX: 1,
212
+ flexShrink: 0,
213
+ },
214
+ React.createElement(Text, { bold: true, color: 'cyan' }, 'AgileFlow TUI'),
215
+ React.createElement(Text, { dimColor: true }, ` v${VERSION}`)
216
+ ),
217
+
218
+ // Main content area (sessions + output + optional trace)
219
+ React.createElement(
220
+ Box,
221
+ {
222
+ flexDirection: 'row',
223
+ flexGrow: 1,
224
+ height: contentHeight,
225
+ },
226
+ // Session panel (left sidebar)
227
+ React.createElement(
228
+ Box,
229
+ {
230
+ width: sessionPanelWidth,
231
+ flexShrink: 0,
232
+ borderStyle: 'single',
233
+ borderColor: 'gray',
234
+ flexDirection: 'column',
235
+ overflow: 'hidden',
236
+ },
237
+ React.createElement(SessionPanel, { refreshInterval: 3000 })
238
+ ),
239
+
240
+ // Output panel (center, grows to fill)
241
+ React.createElement(
242
+ Box,
243
+ {
244
+ flexGrow: 1,
245
+ flexDirection: 'column',
246
+ borderStyle: 'single',
247
+ borderColor: 'green',
248
+ overflow: 'hidden',
249
+ },
250
+ React.createElement(OutputPanel, {
251
+ maxMessages: 50,
252
+ showTimestamp: !isNarrow,
253
+ })
254
+ ),
255
+
256
+ // Trace panel (right, optional)
257
+ showTrace &&
258
+ React.createElement(
259
+ Box,
260
+ {
261
+ width: isNarrow ? 25 : 35,
262
+ flexShrink: 0,
263
+ borderStyle: 'single',
264
+ borderColor: 'magenta',
265
+ flexDirection: 'column',
266
+ overflow: 'hidden',
267
+ },
268
+ React.createElement(TracePanel, null)
269
+ )
270
+ ),
271
+
272
+ // Status bar (bottom)
273
+ React.createElement(StatusBar, { loopStatus, showTrace })
274
+ );
275
+ }
276
+
277
+ module.exports = { Dashboard, StatusBar };
@@ -20,12 +20,52 @@
20
20
  * 1-9 - Switch session focus
21
21
  */
22
22
 
23
- // Use the simple TUI implementation (pure Node.js, no React dependencies)
24
- const { main } = require('./simple-tui');
23
+ // Check if we can use Ink (React-based TUI)
24
+ let useInk = true;
25
+ try {
26
+ require('react');
27
+ require('ink');
28
+ } catch (e) {
29
+ useInk = false;
30
+ }
31
+
32
+ /**
33
+ * Main entry point
34
+ */
35
+ async function main() {
36
+ if (useInk) {
37
+ // Use the React/Ink-based Dashboard for modern terminals
38
+ const React = require('react');
39
+ const { render } = require('ink');
40
+ const { Dashboard } = require('./Dashboard');
41
+
42
+ // Handle actions from the dashboard
43
+ const handleAction = action => {
44
+ // TODO: Implement loop control actions
45
+ // console.log('Action:', action);
46
+ };
47
+
48
+ // Render the dashboard
49
+ const { waitUntilExit } = render(
50
+ React.createElement(Dashboard, { onAction: handleAction })
51
+ );
52
+
53
+ // Wait for exit
54
+ await waitUntilExit();
55
+ } else {
56
+ // Fallback to simple TUI (pure Node.js, no React)
57
+ console.log('React/Ink not available, using simple TUI...');
58
+ const { main: simpleTuiMain } = require('./simple-tui');
59
+ simpleTuiMain();
60
+ }
61
+ }
25
62
 
26
63
  // Run if executed directly
27
64
  if (require.main === module) {
28
- main();
65
+ main().catch(err => {
66
+ console.error('TUI Error:', err.message);
67
+ process.exit(1);
68
+ });
29
69
  }
30
70
 
31
71
  module.exports = { main };
@@ -149,8 +149,6 @@ function OutputPanel({
149
149
  Box,
150
150
  {
151
151
  flexDirection: 'column',
152
- borderStyle: 'single',
153
- borderColor: isConnected ? 'green' : 'gray',
154
152
  padding: 1,
155
153
  flexGrow: 1,
156
154
  },
@@ -134,7 +134,7 @@ function SessionPanel({ refreshInterval = 5000 }) {
134
134
  if (sessions.length === 0) {
135
135
  return React.createElement(
136
136
  Box,
137
- { flexDirection: 'column', borderStyle: 'single', borderColor: 'gray', padding: 1 },
137
+ { flexDirection: 'column', padding: 1 },
138
138
  React.createElement(Text, { bold: true, color: 'cyan' }, 'SESSIONS'),
139
139
  React.createElement(Text, { dimColor: true, italic: true }, 'No active sessions')
140
140
  );
@@ -142,7 +142,7 @@ function SessionPanel({ refreshInterval = 5000 }) {
142
142
 
143
143
  return React.createElement(
144
144
  Box,
145
- { flexDirection: 'column', borderStyle: 'single', borderColor: 'gray', padding: 1 },
145
+ { flexDirection: 'column', padding: 1 },
146
146
  // Header
147
147
  React.createElement(
148
148
  Box,
@@ -293,116 +293,175 @@ class SimpleTUI {
293
293
  // Clear screen
294
294
  output.push(ANSI.clear + ANSI.home);
295
295
 
296
- // Header
297
- const title = ' AgileFlow TUI ';
296
+ // Determine layout mode based on terminal width
297
+ const isWide = width >= 100;
298
+ const isNarrow = width < 60;
299
+
300
+ // Header - compact on narrow terminals
301
+ const title = isNarrow ? ' TUI ' : ' AgileFlow TUI ';
298
302
  const headerPadding = Math.floor((width - title.length) / 2);
299
303
  output.push(
300
304
  `${ANSI.bgCyan}${ANSI.black}${'═'.repeat(headerPadding)}${ANSI.bold}${title}${ANSI.reset}${ANSI.bgCyan}${ANSI.black}${'═'.repeat(width - headerPadding - title.length)}${ANSI.reset}`
301
305
  );
302
- output.push('');
303
-
304
- // Calculate panel widths
305
- const leftWidth = Math.floor(width * 0.4);
306
- const rightWidth = width - leftWidth - 1;
307
- const panelHeight = height - 6; // Leave room for header and footer
308
306
 
309
307
  // Get data
310
308
  const sessions = getSessions();
311
309
  const loopStatus = getLoopStatus();
312
- const agentEvents = getAgentEvents(8);
310
+ const agentEvents = getAgentEvents(isNarrow ? 4 : 8);
311
+
312
+ if (isWide) {
313
+ // Side-by-side layout for wide terminals
314
+ this.renderSideBySide(output, width, height, sessions, loopStatus, agentEvents);
315
+ } else {
316
+ // Stacked layout for normal/narrow terminals
317
+ this.renderStacked(output, width, height, sessions, loopStatus, agentEvents, isNarrow);
318
+ }
319
+
320
+ // Output everything
321
+ process.stdout.write(output.join('\n'));
322
+ }
323
+
324
+ renderSideBySide(output, width, height, sessions, loopStatus, agentEvents) {
325
+ // Calculate panel widths: 30% sessions, 70% output
326
+ const leftWidth = Math.max(25, Math.floor(width * 0.3));
327
+ const rightWidth = width - leftWidth - 3;
328
+ const panelHeight = Math.max(6, height - 5);
329
+
330
+ // Build both panels line by line
331
+ const leftLines = this.buildSessionPanel(leftWidth, panelHeight, sessions);
332
+ const rightLines = this.buildOutputPanel(rightWidth, panelHeight, agentEvents);
333
+
334
+ // Combine side by side
335
+ for (let i = 0; i < panelHeight; i++) {
336
+ const left = leftLines[i] || pad('', leftWidth);
337
+ const right = rightLines[i] || pad('', rightWidth);
338
+ output.push(`${left} ${right}`);
339
+ }
340
+
341
+ // Status bar and footer
342
+ this.renderStatusBar(output, width, loopStatus);
343
+ }
344
+
345
+ renderStacked(output, width, height, sessions, loopStatus, agentEvents, isNarrow) {
346
+ // Calculate heights
347
+ const sessionHeight = isNarrow ? 4 : Math.min(6, sessions.length * 3 + 2);
348
+ const outputHeight = Math.max(4, height - sessionHeight - 5);
349
+
350
+ // Build panels
351
+ const sessionLines = this.buildSessionPanel(width - 2, sessionHeight, sessions);
352
+ const outputLines = this.buildOutputPanel(width - 2, outputHeight, agentEvents);
353
+
354
+ // Add session panel
355
+ for (const line of sessionLines) {
356
+ output.push(line);
357
+ }
358
+ output.push('');
359
+
360
+ // Add output panel
361
+ for (const line of outputLines) {
362
+ output.push(line);
363
+ }
364
+
365
+ // Status bar and footer
366
+ this.renderStatusBar(output, width, loopStatus);
367
+ }
368
+
369
+ buildSessionPanel(panelWidth, panelHeight, sessions) {
370
+ const lines = [];
371
+ const innerWidth = panelWidth - 2;
313
372
 
314
- // Build left panel (sessions)
315
- output.push(`${ANSI.cyan}${ANSI.bold}┌─ SESSIONS ─${'─'.repeat(leftWidth - 14)}┐${ANSI.reset}`);
373
+ // Header
374
+ const header = `─ SESSIONS ─`;
375
+ lines.push(`${ANSI.cyan}┌${header}${'─'.repeat(Math.max(0, innerWidth - header.length))}┐${ANSI.reset}`);
316
376
 
317
377
  if (sessions.length === 0) {
318
- output.push(
319
- `${ANSI.cyan}│${ANSI.reset} ${ANSI.dim}No active sessions${ANSI.reset}${' '.repeat(leftWidth - 21)}${ANSI.cyan}│${ANSI.reset}`
320
- );
378
+ lines.push(`${ANSI.cyan}│${ANSI.reset} ${ANSI.dim}${pad('No active sessions', innerWidth)}${ANSI.reset}${ANSI.cyan}│${ANSI.reset}`);
321
379
  } else {
322
- for (const session of sessions.slice(0, 5)) {
323
- const indicator = session.current ? `${ANSI.green}>` : ' ';
380
+ const maxSessions = Math.floor((panelHeight - 2) / 2);
381
+ for (const session of sessions.slice(0, maxSessions)) {
382
+ const indicator = session.current ? `${ANSI.green}▶` : ' ';
324
383
  const name = `Session ${session.id}${session.is_main ? ' [main]' : ''}`;
325
- const branch = session.branch || 'unknown';
326
- const story = session.story || 'none';
327
-
328
- output.push(
329
- `${ANSI.cyan}│${ANSI.reset} ${indicator} ${ANSI.bold}${pad(name, leftWidth - 6)}${ANSI.reset}${ANSI.cyan}│${ANSI.reset}`
330
- );
331
- output.push(
332
- `${ANSI.cyan}│${ANSI.reset} ${ANSI.dim}Branch:${ANSI.reset} ${ANSI.cyan}${pad(branch, leftWidth - 12)}${ANSI.reset}${ANSI.cyan}│${ANSI.reset}`
333
- );
334
- output.push(
335
- `${ANSI.cyan}│${ANSI.reset} ${ANSI.dim}Story:${ANSI.reset} ${ANSI.yellow}${pad(story, leftWidth - 12)}${ANSI.reset}${ANSI.cyan}│${ANSI.reset}`
336
- );
384
+ lines.push(`${ANSI.cyan}│${ANSI.reset}${indicator} ${ANSI.bold}${pad(name, innerWidth - 2)}${ANSI.reset}${ANSI.cyan}│${ANSI.reset}`);
385
+
386
+ const info = `${session.branch || '?'}${session.story ? ' / ' + session.story : ''}`;
387
+ lines.push(`${ANSI.cyan}│${ANSI.reset} ${ANSI.dim}${pad(info, innerWidth - 1)}${ANSI.reset}${ANSI.cyan}│${ANSI.reset}`);
337
388
  }
338
389
  }
339
390
 
340
- // Fill remaining space in left panel
341
- const usedRows = sessions.length === 0 ? 1 : Math.min(sessions.length, 5) * 3;
342
- const remainingRows = Math.max(0, panelHeight - usedRows - 2);
343
- for (let i = 0; i < remainingRows; i++) {
344
- output.push(
345
- `${ANSI.cyan}│${ANSI.reset}${' '.repeat(leftWidth - 2)}${ANSI.cyan}│${ANSI.reset}`
346
- );
391
+ // Fill remaining rows
392
+ while (lines.length < panelHeight - 1) {
393
+ lines.push(`${ANSI.cyan}│${ANSI.reset}${' '.repeat(innerWidth)}${ANSI.cyan}│${ANSI.reset}`);
347
394
  }
348
395
 
349
- output.push(`${ANSI.cyan}└${'─'.repeat(leftWidth - 2)}┘${ANSI.reset}`);
396
+ // Footer
397
+ lines.push(`${ANSI.cyan}└${'─'.repeat(innerWidth)}┘${ANSI.reset}`);
350
398
 
351
- // Move cursor to right panel position and draw
352
- // For simplicity, we'll draw the right panel below the left panel
353
- output.push('');
354
- output.push(
355
- `${ANSI.green}${ANSI.bold}┌─ AGENT OUTPUT ─${'─'.repeat(width - 19)}┐${ANSI.reset}`
356
- );
399
+ return lines;
400
+ }
357
401
 
358
- if (agentEvents.length === 0 && this.messages.length === 0) {
359
- output.push(
360
- `${ANSI.green}│${ANSI.reset} ${ANSI.dim}Waiting for agent activity...${ANSI.reset}${' '.repeat(width - 34)}${ANSI.green}│${ANSI.reset}`
361
- );
362
- } else {
363
- // Show recent events
364
- const allMessages = [
365
- ...agentEvents.map(e => ({
366
- timestamp: e.timestamp ? new Date(e.timestamp).toLocaleTimeString() : '',
367
- agent: e.agent || 'unknown',
368
- message:
369
- e.message ||
370
- (e.event === 'iteration' ? `Iteration ${e.iter}` : e.event || JSON.stringify(e)),
371
- })),
372
- ...this.messages,
373
- ].slice(-8);
402
+ buildOutputPanel(panelWidth, panelHeight, agentEvents) {
403
+ const lines = [];
404
+ const innerWidth = panelWidth - 2;
374
405
 
406
+ // Header
407
+ const header = `─ OUTPUT ─`;
408
+ lines.push(`${ANSI.green}┌${header}${'─'.repeat(Math.max(0, innerWidth - header.length))}┐${ANSI.reset}`);
409
+
410
+ // Combine events and messages
411
+ const allMessages = [
412
+ ...agentEvents.map(e => ({
413
+ time: e.timestamp ? new Date(e.timestamp).toLocaleTimeString('en-US', { hour12: false, hour: '2-digit', minute: '2-digit' }) : '',
414
+ agent: (e.agent || 'unknown').replace('agileflow-', ''),
415
+ msg: e.message || (e.event === 'iteration' ? `Iter ${e.iter}` : e.event || ''),
416
+ })),
417
+ ...this.messages.map(m => ({
418
+ time: m.timestamp || '',
419
+ agent: (m.agent || 'unknown').replace('agileflow-', ''),
420
+ msg: m.message || '',
421
+ })),
422
+ ].slice(-(panelHeight - 2));
423
+
424
+ if (allMessages.length === 0) {
425
+ lines.push(`${ANSI.green}│${ANSI.reset} ${ANSI.dim}${pad('Waiting for activity...', innerWidth)}${ANSI.reset}${ANSI.green}│${ANSI.reset}`);
426
+ } else {
375
427
  for (const msg of allMessages) {
376
- const line = `[${msg.timestamp}] [${ANSI.cyan}${msg.agent}${ANSI.reset}] ${msg.message}`;
377
- const cleanLine = `[${msg.timestamp}] [${msg.agent}] ${msg.message}`;
378
- const padding = width - cleanLine.length - 4;
379
- output.push(
380
- `${ANSI.green}│${ANSI.reset} ${line}${' '.repeat(Math.max(0, padding))}${ANSI.green}│${ANSI.reset}`
381
- );
428
+ const prefix = `${msg.time} [${msg.agent}] `;
429
+ const maxMsgLen = Math.max(10, innerWidth - prefix.length - 1);
430
+ const truncatedMsg = msg.msg.length > maxMsgLen ? msg.msg.slice(0, maxMsgLen - 2) + '..' : msg.msg;
431
+ const line = `${ANSI.dim}${msg.time}${ANSI.reset} [${ANSI.cyan}${msg.agent}${ANSI.reset}] ${truncatedMsg}`;
432
+ const cleanLen = prefix.length + truncatedMsg.length;
433
+ lines.push(`${ANSI.green}│${ANSI.reset} ${line}${' '.repeat(Math.max(0, innerWidth - cleanLen - 1))}${ANSI.green}│${ANSI.reset}`);
382
434
  }
383
435
  }
384
436
 
385
- output.push(`${ANSI.green}└${'─'.repeat(width - 2)}┘${ANSI.reset}`);
386
-
387
- // Loop status (if active)
388
- if (loopStatus.active) {
389
- output.push('');
390
- const statusIcon = loopStatus.paused
391
- ? `${ANSI.yellow}||${ANSI.reset}`
392
- : `${ANSI.green}>${ANSI.reset}`;
393
- output.push(
394
- `${statusIcon} ${ANSI.bold}Loop:${ANSI.reset} ${loopStatus.epic || 'unknown'} | Story: ${loopStatus.currentStory || 'none'} | ${progressBar(loopStatus.iteration, loopStatus.maxIterations, 15)}`
395
- );
437
+ // Fill remaining rows
438
+ while (lines.length < panelHeight - 1) {
439
+ lines.push(`${ANSI.green}│${ANSI.reset}${' '.repeat(innerWidth)}${ANSI.green}│${ANSI.reset}`);
396
440
  }
397
441
 
398
- // Footer with key bindings
442
+ // Footer
443
+ lines.push(`${ANSI.green}└${'─'.repeat(innerWidth)}┘${ANSI.reset}`);
444
+
445
+ return lines;
446
+ }
447
+
448
+ renderStatusBar(output, width, loopStatus) {
399
449
  output.push('');
400
- output.push(
401
- `${ANSI.dim}[Q]uit [S]tart [P]ause [R]esume [T]race [1-9]Sessions ${ANSI.reset}${ANSI.cyan}AgileFlow v2.90.0${ANSI.reset}`
402
- );
403
450
 
404
- // Output everything
405
- process.stdout.write(output.join('\n'));
451
+ // Loop status
452
+ if (loopStatus.active) {
453
+ const statusIcon = loopStatus.paused ? `${ANSI.yellow}⏸${ANSI.reset}` : `${ANSI.green}▶${ANSI.reset}`;
454
+ const bar = progressBar(loopStatus.iteration, loopStatus.maxIterations, Math.min(15, Math.floor(width / 6)));
455
+ const info = `${loopStatus.epic || ''}${loopStatus.currentStory ? ' / ' + loopStatus.currentStory : ''}`;
456
+ output.push(`${statusIcon} ${ANSI.bold}${info}${ANSI.reset} ${bar}`);
457
+ }
458
+
459
+ // Key bindings - compact on narrow terminals
460
+ const keys = width >= 70
461
+ ? `${ANSI.dim}[Q]uit [S]tart [P]ause [R]esume [T]race [1-9]Sessions${ANSI.reset}`
462
+ : `${ANSI.dim}Q S P R T 1-9${ANSI.reset}`;
463
+ const version = `${ANSI.cyan}v2.90.3${ANSI.reset}`;
464
+ output.push(`${keys} ${version}`);
406
465
  }
407
466
  }
408
467