agileflow 2.90.1 → 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/CHANGELOG.md CHANGED
@@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [2.90.2] - 2026-01-17
11
+
12
+ ### Fixed
13
+ - TUI CLI command fix
14
+
10
15
  ## [2.90.1] - 2026-01-16
11
16
 
12
17
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agileflow",
3
- "version": "2.90.1",
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,
@@ -164,7 +164,8 @@ function pad(str, width, align = 'left') {
164
164
  const s = String(str).slice(0, width);
165
165
  const padding = width - s.length;
166
166
  if (align === 'right') return ' '.repeat(padding) + s;
167
- if (align === 'center') return ' '.repeat(Math.floor(padding / 2)) + s + ' '.repeat(Math.ceil(padding / 2));
167
+ if (align === 'center')
168
+ return ' '.repeat(Math.floor(padding / 2)) + s + ' '.repeat(Math.ceil(padding / 2));
168
169
  return s + ' '.repeat(padding);
169
170
  }
170
171
 
@@ -292,87 +293,175 @@ class SimpleTUI {
292
293
  // Clear screen
293
294
  output.push(ANSI.clear + ANSI.home);
294
295
 
295
- // Header
296
- const title = ' AgileFlow TUI ';
297
- const headerPadding = Math.floor((width - title.length) / 2);
298
- output.push(`${ANSI.bgCyan}${ANSI.black}${'═'.repeat(headerPadding)}${ANSI.bold}${title}${ANSI.reset}${ANSI.bgCyan}${ANSI.black}${'═'.repeat(width - headerPadding - title.length)}${ANSI.reset}`);
299
- output.push('');
296
+ // Determine layout mode based on terminal width
297
+ const isWide = width >= 100;
298
+ const isNarrow = width < 60;
300
299
 
301
- // Calculate panel widths
302
- const leftWidth = Math.floor(width * 0.4);
303
- const rightWidth = width - leftWidth - 1;
304
- const panelHeight = height - 6; // Leave room for header and footer
300
+ // Header - compact on narrow terminals
301
+ const title = isNarrow ? ' TUI ' : ' AgileFlow TUI ';
302
+ const headerPadding = Math.floor((width - title.length) / 2);
303
+ output.push(
304
+ `${ANSI.bgCyan}${ANSI.black}${'═'.repeat(headerPadding)}${ANSI.bold}${title}${ANSI.reset}${ANSI.bgCyan}${ANSI.black}${'═'.repeat(width - headerPadding - title.length)}${ANSI.reset}`
305
+ );
305
306
 
306
307
  // Get data
307
308
  const sessions = getSessions();
308
309
  const loopStatus = getLoopStatus();
309
- 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
+ }
310
323
 
311
- // Build left panel (sessions)
312
- output.push(`${ANSI.cyan}${ANSI.bold}┌─ SESSIONS ─${'─'.repeat(leftWidth - 14)}┐${ANSI.reset}`);
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;
372
+
373
+ // Header
374
+ const header = `─ SESSIONS ─`;
375
+ lines.push(`${ANSI.cyan}┌${header}${'─'.repeat(Math.max(0, innerWidth - header.length))}┐${ANSI.reset}`);
313
376
 
314
377
  if (sessions.length === 0) {
315
- output.push(`${ANSI.cyan}│${ANSI.reset} ${ANSI.dim}No active sessions${ANSI.reset}${' '.repeat(leftWidth - 21)}${ANSI.cyan}│${ANSI.reset}`);
378
+ lines.push(`${ANSI.cyan}│${ANSI.reset} ${ANSI.dim}${pad('No active sessions', innerWidth)}${ANSI.reset}${ANSI.cyan}│${ANSI.reset}`);
316
379
  } else {
317
- for (const session of sessions.slice(0, 5)) {
318
- 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}▶` : ' ';
319
383
  const name = `Session ${session.id}${session.is_main ? ' [main]' : ''}`;
320
- const branch = session.branch || 'unknown';
321
- const story = session.story || 'none';
384
+ lines.push(`${ANSI.cyan}│${ANSI.reset}${indicator} ${ANSI.bold}${pad(name, innerWidth - 2)}${ANSI.reset}${ANSI.cyan}│${ANSI.reset}`);
322
385
 
323
- output.push(`${ANSI.cyan}│${ANSI.reset} ${indicator} ${ANSI.bold}${pad(name, leftWidth - 6)}${ANSI.reset}${ANSI.cyan}│${ANSI.reset}`);
324
- output.push(`${ANSI.cyan}│${ANSI.reset} ${ANSI.dim}Branch:${ANSI.reset} ${ANSI.cyan}${pad(branch, leftWidth - 12)}${ANSI.reset}${ANSI.cyan}│${ANSI.reset}`);
325
- output.push(`${ANSI.cyan}│${ANSI.reset} ${ANSI.dim}Story:${ANSI.reset} ${ANSI.yellow}${pad(story, leftWidth - 12)}${ANSI.reset}${ANSI.cyan}│${ANSI.reset}`);
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}`);
326
388
  }
327
389
  }
328
390
 
329
- // Fill remaining space in left panel
330
- const usedRows = sessions.length === 0 ? 1 : Math.min(sessions.length, 5) * 3;
331
- const remainingRows = Math.max(0, panelHeight - usedRows - 2);
332
- for (let i = 0; i < remainingRows; i++) {
333
- output.push(`${ANSI.cyan}│${ANSI.reset}${' '.repeat(leftWidth - 2)}${ANSI.cyan}│${ANSI.reset}`);
391
+ // Fill remaining rows
392
+ while (lines.length < panelHeight - 1) {
393
+ lines.push(`${ANSI.cyan}│${ANSI.reset}${' '.repeat(innerWidth)}${ANSI.cyan}│${ANSI.reset}`);
334
394
  }
335
395
 
336
- output.push(`${ANSI.cyan}└${'─'.repeat(leftWidth - 2)}┘${ANSI.reset}`);
396
+ // Footer
397
+ lines.push(`${ANSI.cyan}└${'─'.repeat(innerWidth)}┘${ANSI.reset}`);
337
398
 
338
- // Move cursor to right panel position and draw
339
- // For simplicity, we'll draw the right panel below the left panel
340
- output.push('');
341
- output.push(`${ANSI.green}${ANSI.bold}┌─ AGENT OUTPUT ─${'─'.repeat(width - 19)}┐${ANSI.reset}`);
399
+ return lines;
400
+ }
342
401
 
343
- if (agentEvents.length === 0 && this.messages.length === 0) {
344
- output.push(`${ANSI.green}│${ANSI.reset} ${ANSI.dim}Waiting for agent activity...${ANSI.reset}${' '.repeat(width - 34)}${ANSI.green}│${ANSI.reset}`);
345
- } else {
346
- // Show recent events
347
- const allMessages = [...agentEvents.map(e => ({
348
- timestamp: e.timestamp ? new Date(e.timestamp).toLocaleTimeString() : '',
349
- agent: e.agent || 'unknown',
350
- message: e.message || (e.event === 'iteration' ? `Iteration ${e.iter}` : e.event || JSON.stringify(e))
351
- })), ...this.messages].slice(-8);
402
+ buildOutputPanel(panelWidth, panelHeight, agentEvents) {
403
+ const lines = [];
404
+ const innerWidth = panelWidth - 2;
352
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 {
353
427
  for (const msg of allMessages) {
354
- const line = `[${msg.timestamp}] [${ANSI.cyan}${msg.agent}${ANSI.reset}] ${msg.message}`;
355
- const cleanLine = `[${msg.timestamp}] [${msg.agent}] ${msg.message}`;
356
- const padding = width - cleanLine.length - 4;
357
- output.push(`${ANSI.green}│${ANSI.reset} ${line}${' '.repeat(Math.max(0, padding))}${ANSI.green}│${ANSI.reset}`);
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}`);
358
434
  }
359
435
  }
360
436
 
361
- output.push(`${ANSI.green}└${'─'.repeat(width - 2)}┘${ANSI.reset}`);
362
-
363
- // Loop status (if active)
364
- if (loopStatus.active) {
365
- output.push('');
366
- const statusIcon = loopStatus.paused ? `${ANSI.yellow}||${ANSI.reset}` : `${ANSI.green}>${ANSI.reset}`;
367
- output.push(`${statusIcon} ${ANSI.bold}Loop:${ANSI.reset} ${loopStatus.epic || 'unknown'} | Story: ${loopStatus.currentStory || 'none'} | ${progressBar(loopStatus.iteration, loopStatus.maxIterations, 15)}`);
437
+ // Fill remaining rows
438
+ while (lines.length < panelHeight - 1) {
439
+ lines.push(`${ANSI.green}│${ANSI.reset}${' '.repeat(innerWidth)}${ANSI.green}│${ANSI.reset}`);
368
440
  }
369
441
 
370
- // 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) {
371
449
  output.push('');
372
- output.push(`${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}`);
373
450
 
374
- // Output everything
375
- 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}`);
376
465
  }
377
466
  }
378
467