agileflow 2.90.6 → 2.91.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.
Files changed (75) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +6 -6
  3. package/lib/codebase-indexer.js +810 -0
  4. package/lib/validate-names.js +3 -3
  5. package/package.json +4 -1
  6. package/scripts/obtain-context.js +238 -0
  7. package/scripts/precompact-context.sh +13 -1
  8. package/scripts/query-codebase.js +430 -0
  9. package/scripts/tui/blessed/data/watcher.js +175 -0
  10. package/scripts/tui/blessed/index.js +244 -0
  11. package/scripts/tui/blessed/panels/output.js +95 -0
  12. package/scripts/tui/blessed/panels/sessions.js +143 -0
  13. package/scripts/tui/blessed/panels/trace.js +91 -0
  14. package/scripts/tui/blessed/ui/help.js +77 -0
  15. package/scripts/tui/blessed/ui/screen.js +52 -0
  16. package/scripts/tui/blessed/ui/statusbar.js +51 -0
  17. package/scripts/tui/blessed/ui/tabbar.js +99 -0
  18. package/scripts/tui/index.js +38 -32
  19. package/scripts/tui/simple-tui.js +8 -5
  20. package/scripts/validators/README.md +143 -0
  21. package/scripts/validators/component-validator.js +212 -0
  22. package/scripts/validators/json-schema-validator.js +179 -0
  23. package/scripts/validators/markdown-validator.js +153 -0
  24. package/scripts/validators/migration-validator.js +117 -0
  25. package/scripts/validators/security-validator.js +276 -0
  26. package/scripts/validators/story-format-validator.js +176 -0
  27. package/scripts/validators/test-result-validator.js +99 -0
  28. package/scripts/validators/workflow-validator.js +240 -0
  29. package/src/core/agents/accessibility.md +6 -0
  30. package/src/core/agents/adr-writer.md +6 -0
  31. package/src/core/agents/analytics.md +6 -0
  32. package/src/core/agents/api.md +6 -0
  33. package/src/core/agents/ci.md +6 -0
  34. package/src/core/agents/codebase-query.md +237 -0
  35. package/src/core/agents/compliance.md +6 -0
  36. package/src/core/agents/configuration-damage-control.md +6 -0
  37. package/src/core/agents/configuration-visual-e2e.md +6 -0
  38. package/src/core/agents/database.md +10 -0
  39. package/src/core/agents/datamigration.md +6 -0
  40. package/src/core/agents/design.md +6 -0
  41. package/src/core/agents/devops.md +6 -0
  42. package/src/core/agents/documentation.md +6 -0
  43. package/src/core/agents/epic-planner.md +6 -0
  44. package/src/core/agents/integrations.md +6 -0
  45. package/src/core/agents/mentor.md +6 -0
  46. package/src/core/agents/mobile.md +6 -0
  47. package/src/core/agents/monitoring.md +6 -0
  48. package/src/core/agents/multi-expert.md +6 -0
  49. package/src/core/agents/performance.md +6 -0
  50. package/src/core/agents/product.md +6 -0
  51. package/src/core/agents/qa.md +6 -0
  52. package/src/core/agents/readme-updater.md +6 -0
  53. package/src/core/agents/refactor.md +6 -0
  54. package/src/core/agents/research.md +6 -0
  55. package/src/core/agents/security.md +6 -0
  56. package/src/core/agents/testing.md +10 -0
  57. package/src/core/agents/ui.md +6 -0
  58. package/src/core/commands/audit.md +401 -0
  59. package/src/core/commands/board.md +1 -0
  60. package/src/core/commands/epic.md +92 -1
  61. package/src/core/commands/help.md +1 -0
  62. package/src/core/commands/metrics.md +1 -0
  63. package/src/core/commands/research/analyze.md +1 -0
  64. package/src/core/commands/research/ask.md +2 -0
  65. package/src/core/commands/research/import.md +1 -0
  66. package/src/core/commands/research/list.md +2 -0
  67. package/src/core/commands/research/synthesize.md +584 -0
  68. package/src/core/commands/research/view.md +2 -0
  69. package/src/core/commands/status.md +126 -1
  70. package/src/core/commands/story/list.md +9 -9
  71. package/src/core/commands/story/view.md +1 -0
  72. package/src/core/experts/codebase-query/expertise.yaml +190 -0
  73. package/src/core/experts/codebase-query/question.md +73 -0
  74. package/src/core/experts/codebase-query/self-improve.md +105 -0
  75. package/tools/cli/commands/tui.js +40 -271
@@ -0,0 +1,244 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * AgileFlow TUI - Terminal User Interface
6
+ *
7
+ * Full-screen, flicker-free dashboard with tabs and live updates.
8
+ *
9
+ * Key bindings:
10
+ * 1-3 Switch tabs (Sessions, Output, Trace)
11
+ * Tab Next tab
12
+ * j/k Navigate items
13
+ * r Refresh data
14
+ * ?/h Toggle help overlay
15
+ * q Quit TUI
16
+ */
17
+
18
+ const blessed = require('blessed');
19
+
20
+ // UI components
21
+ const createScreen = require('./ui/screen');
22
+ const createTabBar = require('./ui/tabbar');
23
+ const createStatusBar = require('./ui/statusbar');
24
+ const createHelpOverlay = require('./ui/help');
25
+
26
+ // Panels
27
+ const createSessionsPanel = require('./panels/sessions');
28
+ const createOutputPanel = require('./panels/output');
29
+ const createTracePanel = require('./panels/trace');
30
+
31
+ // Data
32
+ const DataWatcher = require('./data/watcher');
33
+
34
+ class AgileFlowTUI {
35
+ constructor() {
36
+ this.state = {
37
+ activeTab: 0,
38
+ tabs: ['Sessions', 'Output', 'Trace'],
39
+ sessions: [],
40
+ logs: [],
41
+ traces: [],
42
+ showHelp: false
43
+ };
44
+ this.components = {};
45
+ this.watcher = null;
46
+ this.screen = null;
47
+ }
48
+
49
+ init() {
50
+ // Create screen with flicker-free rendering
51
+ this.screen = createScreen();
52
+
53
+ // Wrapper to pass screen to panels (they expect grid.screen)
54
+ const gridLike = { screen: this.screen };
55
+
56
+ // Create UI components
57
+ this.components.tabBar = createTabBar(this.screen, this.state);
58
+ this.components.statusBar = createStatusBar(this.screen, this.state);
59
+ this.components.help = createHelpOverlay(this.screen, this.state);
60
+
61
+ // Create panels (pass screen wrapper)
62
+ this.components.sessions = createSessionsPanel(gridLike, this.state);
63
+ this.components.output = createOutputPanel(gridLike, this.state);
64
+ this.components.trace = createTracePanel(gridLike, this.state);
65
+
66
+ // Hide all panels initially
67
+ this.components.sessions.hide();
68
+ this.components.output.hide();
69
+ this.components.trace.hide();
70
+
71
+ // Set up keybindings
72
+ this.setupKeys();
73
+
74
+ // Start data watcher for live updates
75
+ this.watcher = new DataWatcher(this.state, () => this.refresh());
76
+ this.watcher.start();
77
+
78
+ // Show initial tab
79
+ this.switchTab(0);
80
+
81
+ // Initial render
82
+ this.screen.render();
83
+ }
84
+
85
+ setupKeys() {
86
+ // Quit
87
+ this.screen.key(['q', 'C-c'], () => {
88
+ this.cleanup();
89
+ process.exit(0);
90
+ });
91
+
92
+ // Tab switching with number keys
93
+ this.screen.key(['1', '2', '3'], (ch) => {
94
+ if (this.components.help.isVisible()) return;
95
+ this.switchTab(parseInt(ch) - 1);
96
+ });
97
+
98
+ // Tab switching with Tab key
99
+ this.screen.key(['tab'], () => {
100
+ if (this.components.help.isVisible()) return;
101
+ this.switchTab((this.state.activeTab + 1) % this.state.tabs.length);
102
+ });
103
+
104
+ // Shift+Tab for reverse tab
105
+ this.screen.key(['S-tab'], () => {
106
+ if (this.components.help.isVisible()) return;
107
+ const prev = (this.state.activeTab - 1 + this.state.tabs.length) % this.state.tabs.length;
108
+ this.switchTab(prev);
109
+ });
110
+
111
+ // Help overlay
112
+ this.screen.key(['?', 'h'], () => {
113
+ this.state.showHelp = !this.state.showHelp;
114
+ this.components.help.toggle();
115
+ this.screen.render();
116
+ });
117
+
118
+ // Refresh
119
+ this.screen.key(['r'], () => {
120
+ if (this.components.help.isVisible()) return;
121
+ this.components.statusBar.setStatus('{yellow-fg}Refreshing...{/}');
122
+ this.screen.render();
123
+
124
+ // Reload data
125
+ if (this.watcher) {
126
+ this.watcher.loadStatus();
127
+ this.watcher.loadLogs();
128
+ this.watcher.loadSessionState();
129
+ }
130
+
131
+ setTimeout(() => {
132
+ this.refresh();
133
+ this.components.statusBar.resetHints();
134
+ this.screen.render();
135
+ }, 100);
136
+ });
137
+
138
+ // Navigation (j/k)
139
+ this.screen.key(['j', 'down'], () => {
140
+ if (this.components.help.isVisible()) return;
141
+ if (this.state.activeTab === 0 && this.components.sessions.selectNext) {
142
+ this.components.sessions.selectNext();
143
+ this.screen.render();
144
+ }
145
+ });
146
+
147
+ this.screen.key(['k', 'up'], () => {
148
+ if (this.components.help.isVisible()) return;
149
+ if (this.state.activeTab === 0 && this.components.sessions.selectPrev) {
150
+ this.components.sessions.selectPrev();
151
+ this.screen.render();
152
+ }
153
+ });
154
+
155
+ // Escape to close help
156
+ this.screen.key(['escape'], () => {
157
+ if (this.components.help.isVisible()) {
158
+ this.components.help.hide();
159
+ this.screen.render();
160
+ }
161
+ });
162
+ }
163
+
164
+ switchTab(index) {
165
+ if (index < 0 || index >= this.state.tabs.length) return;
166
+
167
+ this.state.activeTab = index;
168
+
169
+ // Hide all panels
170
+ this.components.sessions.hide();
171
+ this.components.output.hide();
172
+ this.components.trace.hide();
173
+
174
+ // Show and focus active panel
175
+ switch (index) {
176
+ case 0:
177
+ this.components.sessions.show();
178
+ this.components.sessions.focus();
179
+ break;
180
+ case 1:
181
+ this.components.output.show();
182
+ this.components.output.focus();
183
+ break;
184
+ case 2:
185
+ this.components.trace.show();
186
+ this.components.trace.focus();
187
+ break;
188
+ }
189
+
190
+ // Update tab bar highlighting
191
+ this.components.tabBar.setTab(index);
192
+
193
+ // Render changes
194
+ this.screen.render();
195
+ }
196
+
197
+ refresh() {
198
+ // Update panels with current state
199
+ this.components.sessions.setData(this.state.sessions);
200
+ this.components.output.setData(this.state.logs);
201
+ this.components.trace.setData(this.state.traces);
202
+
203
+ // Re-render
204
+ this.screen.render();
205
+ }
206
+
207
+ cleanup() {
208
+ // Stop watching files
209
+ if (this.watcher) {
210
+ this.watcher.stop();
211
+ }
212
+
213
+ // Destroy screen
214
+ if (this.screen) {
215
+ this.screen.destroy();
216
+ }
217
+ }
218
+
219
+ run() {
220
+ try {
221
+ this.init();
222
+ } catch (err) {
223
+ console.error('Failed to initialize TUI:', err.message);
224
+ console.error('');
225
+ console.error('Try running: npx agileflow tui --fallback');
226
+ process.exit(1);
227
+ }
228
+ }
229
+ }
230
+
231
+ /**
232
+ * Main entry point
233
+ */
234
+ function main() {
235
+ const tui = new AgileFlowTUI();
236
+ tui.run();
237
+ }
238
+
239
+ // Run if executed directly
240
+ if (require.main === module) {
241
+ main();
242
+ }
243
+
244
+ module.exports = { AgileFlowTUI, main };
@@ -0,0 +1,95 @@
1
+ 'use strict';
2
+
3
+ const blessed = require('blessed');
4
+
5
+ /**
6
+ * Create the output panel showing log stream
7
+ */
8
+ module.exports = function createOutputPanel(grid, state) {
9
+ const box = blessed.box({
10
+ parent: grid.screen,
11
+ top: 3,
12
+ left: 0,
13
+ width: '100%',
14
+ height: '100%-4',
15
+ label: ' {green-fg}{bold}Output{/bold}{/green-fg} ',
16
+ tags: true,
17
+ border: {
18
+ type: 'line',
19
+ fg: 'green'
20
+ },
21
+ style: {
22
+ fg: 'white',
23
+ bg: 'black',
24
+ border: { fg: 'green' }
25
+ },
26
+ scrollable: true,
27
+ alwaysScroll: true,
28
+ scrollbar: {
29
+ ch: '│',
30
+ style: { fg: 'green' }
31
+ },
32
+ keys: true,
33
+ vi: true,
34
+ mouse: true
35
+ });
36
+
37
+ let logs = [];
38
+
39
+ function render() {
40
+ if (logs.length === 0) {
41
+ box.setContent(`
42
+ {gray-fg}Waiting for output...{/}
43
+
44
+ {green-fg}What shows here:{/}
45
+ • Agent activity logs
46
+ • Command execution output
47
+ • Tool call results
48
+ • Error messages
49
+
50
+ {gray-fg}Logs are read from docs/09-agents/bus/log.jsonl{/}
51
+ `);
52
+ return;
53
+ }
54
+
55
+ let lines = [];
56
+ logs.forEach(entry => {
57
+ const time = entry.timestamp
58
+ ? new Date(entry.timestamp).toLocaleTimeString('en-US', { hour12: false })
59
+ : '--:--:--';
60
+
61
+ const level = entry.level || 'info';
62
+ let levelTag = '{gray-fg}INFO{/}';
63
+ if (level === 'error') levelTag = '{red-fg}ERRO{/}';
64
+ else if (level === 'warn' || level === 'warning') levelTag = '{yellow-fg}WARN{/}';
65
+ else if (level === 'debug') levelTag = '{gray-fg}DEBG{/}';
66
+ else if (level === 'success') levelTag = '{green-fg}DONE{/}';
67
+
68
+ const msg = entry.message || (typeof entry === 'string' ? entry : JSON.stringify(entry));
69
+ lines.push(` {gray-fg}${time}{/} ${levelTag} ${msg}`);
70
+ });
71
+
72
+ box.setContent(lines.join('\n'));
73
+ box.setScrollPerc(100); // Auto-scroll to bottom
74
+ }
75
+
76
+ return {
77
+ element: box,
78
+ show() { box.show(); },
79
+ hide() { box.hide(); },
80
+ focus() { box.focus(); },
81
+ setData(data) {
82
+ logs = data || [];
83
+ render();
84
+ },
85
+ addLine(text, level = 'info') {
86
+ logs.push({ message: text, level, timestamp: new Date().toISOString() });
87
+ if (logs.length > 200) logs.shift();
88
+ render();
89
+ },
90
+ clear() {
91
+ logs = [];
92
+ render();
93
+ }
94
+ };
95
+ };
@@ -0,0 +1,143 @@
1
+ 'use strict';
2
+
3
+ const blessed = require('blessed');
4
+
5
+ /**
6
+ * Create the sessions panel with a better styled list
7
+ */
8
+ module.exports = function createSessionsPanel(grid, state) {
9
+ // Use a list instead of table for better control
10
+ const box = blessed.box({
11
+ parent: grid.screen,
12
+ top: 3,
13
+ left: 0,
14
+ width: '100%',
15
+ height: '100%-4',
16
+ label: ' {cyan-fg}{bold}Sessions{/bold}{/cyan-fg} ',
17
+ tags: true,
18
+ border: {
19
+ type: 'line',
20
+ fg: 'cyan'
21
+ },
22
+ style: {
23
+ fg: 'white',
24
+ bg: 'black',
25
+ border: { fg: 'cyan' }
26
+ },
27
+ scrollable: true,
28
+ alwaysScroll: true,
29
+ scrollbar: {
30
+ ch: '│',
31
+ style: { fg: 'cyan' }
32
+ },
33
+ keys: true,
34
+ vi: true
35
+ });
36
+
37
+ // Header row
38
+ const header = blessed.box({
39
+ parent: box,
40
+ top: 0,
41
+ left: 0,
42
+ width: '100%-2',
43
+ height: 1,
44
+ content: '{bold}{cyan-fg} ID Story Status Time{/}{/}',
45
+ tags: true,
46
+ style: {
47
+ bg: '#222222'
48
+ }
49
+ });
50
+
51
+ // Divider
52
+ blessed.box({
53
+ parent: box,
54
+ top: 1,
55
+ left: 0,
56
+ width: '100%-2',
57
+ height: 1,
58
+ content: ' ────────── ───────────────────────────────── ──────────── ──────',
59
+ style: {
60
+ fg: 'gray'
61
+ }
62
+ });
63
+
64
+ // Content area
65
+ const content = blessed.box({
66
+ parent: box,
67
+ top: 2,
68
+ left: 0,
69
+ width: '100%-2',
70
+ height: '100%-3',
71
+ tags: true,
72
+ style: {
73
+ fg: 'white'
74
+ }
75
+ });
76
+
77
+ let sessions = [];
78
+ let selectedIndex = 0;
79
+
80
+ function render() {
81
+ if (sessions.length === 0) {
82
+ content.setContent(`
83
+ {gray-fg}No active sessions{/}
84
+
85
+ {cyan-fg}Quick Start:{/}
86
+ • Run {bold}/agileflow:start{/} to begin a session
87
+ • Use {bold}/agileflow:loop{/} for autonomous mode
88
+ • Press {bold}?{/} for help
89
+ `);
90
+ return;
91
+ }
92
+
93
+ let lines = [];
94
+ sessions.forEach((s, i) => {
95
+ const selected = i === selectedIndex;
96
+ const prefix = selected ? '{inverse}' : '';
97
+ const suffix = selected ? '{/inverse}' : '';
98
+
99
+ // Status with colors
100
+ let statusText = s.status || 'unknown';
101
+ if (statusText === 'active') statusText = '{green-fg}● active{/}';
102
+ else if (statusText === 'running') statusText = '{green-fg}● running{/}';
103
+ else if (statusText === 'paused') statusText = '{yellow-fg}◉ paused{/}';
104
+ else if (statusText === 'idle') statusText = '{gray-fg}○ idle{/}';
105
+ else if (statusText === 'error') statusText = '{red-fg}✖ error{/}';
106
+ else statusText = `{gray-fg}○ ${statusText}{/}`;
107
+
108
+ const id = (s.id || '--').padEnd(10);
109
+ const story = (s.story || 'No story').substring(0, 34).padEnd(34);
110
+ const time = (s.duration || '--').padStart(6);
111
+
112
+ lines.push(`${prefix} ${id} ${story} ${statusText.padEnd(22)} ${time}${suffix}`);
113
+ });
114
+
115
+ content.setContent(lines.join('\n'));
116
+ }
117
+
118
+ return {
119
+ element: box,
120
+ show() { box.show(); },
121
+ hide() { box.hide(); },
122
+ focus() { box.focus(); },
123
+ setData(data) {
124
+ sessions = data || [];
125
+ render();
126
+ },
127
+ getSelected() {
128
+ return selectedIndex;
129
+ },
130
+ selectNext() {
131
+ if (selectedIndex < sessions.length - 1) {
132
+ selectedIndex++;
133
+ render();
134
+ }
135
+ },
136
+ selectPrev() {
137
+ if (selectedIndex > 0) {
138
+ selectedIndex--;
139
+ render();
140
+ }
141
+ }
142
+ };
143
+ };
@@ -0,0 +1,91 @@
1
+ 'use strict';
2
+
3
+ const blessed = require('blessed');
4
+
5
+ /**
6
+ * Create the trace panel showing execution steps
7
+ */
8
+ module.exports = function createTracePanel(grid, state) {
9
+ const box = blessed.box({
10
+ parent: grid.screen,
11
+ top: 3,
12
+ left: 0,
13
+ width: '100%',
14
+ height: '100%-4',
15
+ label: ' {yellow-fg}{bold}Trace{/bold}{/yellow-fg} ',
16
+ tags: true,
17
+ border: {
18
+ type: 'line',
19
+ fg: 'yellow'
20
+ },
21
+ style: {
22
+ fg: 'white',
23
+ bg: 'black',
24
+ border: { fg: 'yellow' }
25
+ },
26
+ scrollable: true,
27
+ alwaysScroll: true,
28
+ scrollbar: {
29
+ ch: '│',
30
+ style: { fg: 'yellow' }
31
+ },
32
+ keys: true,
33
+ vi: true
34
+ });
35
+
36
+ let traces = [];
37
+
38
+ function render() {
39
+ if (traces.length === 0) {
40
+ box.setContent(`
41
+ {gray-fg}No trace data{/}
42
+
43
+ {yellow-fg}What shows here:{/}
44
+ • Active command execution
45
+ • Step-by-step agent workflow
46
+ • Tool calls and responses
47
+ • Timing information
48
+
49
+ {gray-fg}Trace data comes from .agileflow/session-state.json{/}
50
+ `);
51
+ return;
52
+ }
53
+
54
+ let lines = [];
55
+ traces.forEach((t, i) => {
56
+ // Status indicator
57
+ let statusIcon = '{gray-fg}○{/}';
58
+ if (t.status === 'running') statusIcon = '{yellow-fg}◉{/}';
59
+ else if (t.status === 'completed') statusIcon = '{green-fg}●{/}';
60
+ else if (t.status === 'error') statusIcon = '{red-fg}✖{/}';
61
+
62
+ const stepNum = String(i + 1).padStart(2, '0');
63
+ const action = (t.action || 'Unknown').substring(0, 40);
64
+ const duration = t.duration || '--';
65
+
66
+ lines.push(` ${statusIcon} Step ${stepNum}: {bold}${action}{/}`);
67
+ if (t.details) {
68
+ lines.push(` {gray-fg}${t.details.substring(0, 60)}{/}`);
69
+ }
70
+ lines.push(` {gray-fg}Duration: ${duration}{/}`);
71
+ lines.push('');
72
+ });
73
+
74
+ box.setContent(lines.join('\n'));
75
+ }
76
+
77
+ return {
78
+ element: box,
79
+ show() { box.show(); },
80
+ hide() { box.hide(); },
81
+ focus() { box.focus(); },
82
+ setData(data) {
83
+ traces = data || [];
84
+ render();
85
+ },
86
+ addStep(action, status = 'running', details = '') {
87
+ traces.push({ action, status, details, duration: '--' });
88
+ render();
89
+ }
90
+ };
91
+ };
@@ -0,0 +1,77 @@
1
+ 'use strict';
2
+
3
+ const blessed = require('blessed');
4
+
5
+ /**
6
+ * Create the help overlay that shows all available commands
7
+ * Toggled with ? or h key
8
+ */
9
+ module.exports = function createHelpOverlay(screen, state) {
10
+ const help = blessed.box({
11
+ parent: screen,
12
+ top: 'center',
13
+ left: 'center',
14
+ width: 55,
15
+ height: 22,
16
+ tags: true,
17
+ border: { type: 'line', fg: 'yellow' },
18
+ style: {
19
+ fg: 'white',
20
+ bg: 'black',
21
+ border: { fg: 'yellow' }
22
+ },
23
+ label: ' {yellow-fg}Help{/yellow-fg} ',
24
+ hidden: true,
25
+ content: `
26
+ {bold}Navigation{/bold}
27
+ {cyan-fg}1{/} Sessions tab
28
+ {cyan-fg}2{/} Output tab
29
+ {cyan-fg}3{/} Trace tab
30
+ {cyan-fg}Tab{/} Next tab
31
+ {cyan-fg}j/k or {/}{cyan-fg}arrow-down/arrow-up{/} Navigate list items
32
+ {cyan-fg}Enter{/} Select item
33
+
34
+ {bold}Actions{/bold}
35
+ {cyan-fg}r{/} Refresh data
36
+ {cyan-fg}s{/} Start loop on current story
37
+ {cyan-fg}p{/} Pause active loop
38
+
39
+ {bold}Display{/bold}
40
+ {cyan-fg}?{/} or {cyan-fg}h{/} Toggle this help
41
+ {cyan-fg}Escape{/} Close help/dialogs
42
+
43
+ {bold}Exit{/bold}
44
+ {cyan-fg}q{/} Quit TUI
45
+ {cyan-fg}Ctrl+C{/} Force quit
46
+
47
+ {gray-fg}Press Escape or ? to close{/gray-fg}`
48
+ });
49
+
50
+ // Close help on various keys
51
+ help.key(['escape', 'q', '?', 'h', 'enter', 'space'], () => {
52
+ help.hide();
53
+ screen.render();
54
+ });
55
+
56
+ return {
57
+ element: help,
58
+ toggle() {
59
+ if (help.hidden) {
60
+ help.show();
61
+ help.focus();
62
+ } else {
63
+ help.hide();
64
+ }
65
+ },
66
+ show() {
67
+ help.show();
68
+ help.focus();
69
+ },
70
+ hide() {
71
+ help.hide();
72
+ },
73
+ isVisible() {
74
+ return !help.hidden;
75
+ }
76
+ };
77
+ };
@@ -0,0 +1,52 @@
1
+ 'use strict';
2
+
3
+ const blessed = require('blessed');
4
+
5
+ /**
6
+ * Create the main blessed screen with flicker-free rendering
7
+ *
8
+ * Key settings:
9
+ * - smartCSR: Enable differential rendering (only update changed cells)
10
+ * - fullUnicode: Support Unicode characters like box drawing
11
+ */
12
+ module.exports = function createScreen() {
13
+ const screen = blessed.screen({
14
+ smartCSR: true, // Key for flicker-free differential rendering
15
+ fullUnicode: true, // Support Unicode characters
16
+ title: 'AgileFlow TUI',
17
+ cursor: {
18
+ artificial: true,
19
+ blink: true,
20
+ shape: 'line'
21
+ },
22
+ debug: false,
23
+ warnings: false,
24
+ autoPadding: true,
25
+ dockBorders: true
26
+ });
27
+
28
+ // Enable synchronized output mode for atomic updates
29
+ // This batches all screen updates and flushes them at once
30
+ // Note: Not all terminals support this, but it gracefully degrades
31
+ try {
32
+ process.stdout.write('\x1b[?2026h');
33
+ } catch (e) {
34
+ // Ignore if terminal doesn't support
35
+ }
36
+
37
+ // Disable synchronized output on exit
38
+ process.on('exit', () => {
39
+ try {
40
+ process.stdout.write('\x1b[?2026l');
41
+ } catch (e) {
42
+ // Ignore
43
+ }
44
+ });
45
+
46
+ // Handle resize gracefully
47
+ screen.on('resize', () => {
48
+ screen.render();
49
+ });
50
+
51
+ return screen;
52
+ };