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,316 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Event Stream - Real-time event monitoring from agent bus
|
|
5
|
+
*
|
|
6
|
+
* Watches docs/09-agents/bus/log.jsonl for new events and emits them
|
|
7
|
+
* to subscribers. Handles file rotation and truncation gracefully.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
const EventEmitter = require('events');
|
|
13
|
+
|
|
14
|
+
// Get project root
|
|
15
|
+
let getProjectRoot;
|
|
16
|
+
try {
|
|
17
|
+
getProjectRoot = require('../../../lib/paths').getProjectRoot;
|
|
18
|
+
} catch (e) {
|
|
19
|
+
// Fallback
|
|
20
|
+
getProjectRoot = () => process.cwd();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* EventStream class - watches agent bus log and emits events
|
|
25
|
+
*/
|
|
26
|
+
class EventStream extends EventEmitter {
|
|
27
|
+
constructor(options = {}) {
|
|
28
|
+
super();
|
|
29
|
+
|
|
30
|
+
this.options = {
|
|
31
|
+
// Path to log file (defaults to agent bus)
|
|
32
|
+
logPath: options.logPath || path.join(
|
|
33
|
+
getProjectRoot(),
|
|
34
|
+
'docs',
|
|
35
|
+
'09-agents',
|
|
36
|
+
'bus',
|
|
37
|
+
'log.jsonl'
|
|
38
|
+
),
|
|
39
|
+
// Polling interval in ms (fallback if fs.watch fails)
|
|
40
|
+
pollInterval: options.pollInterval || 1000,
|
|
41
|
+
// Maximum events to keep in buffer
|
|
42
|
+
maxBufferSize: options.maxBufferSize || 100,
|
|
43
|
+
// Whether to emit historical events on start
|
|
44
|
+
emitHistory: options.emitHistory || false,
|
|
45
|
+
// How many historical events to emit
|
|
46
|
+
historyLimit: options.historyLimit || 10
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
this.buffer = [];
|
|
50
|
+
this.filePosition = 0;
|
|
51
|
+
this.watcher = null;
|
|
52
|
+
this.pollTimer = null;
|
|
53
|
+
this.isWatching = false;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Start watching the log file
|
|
58
|
+
*/
|
|
59
|
+
start() {
|
|
60
|
+
if (this.isWatching) return;
|
|
61
|
+
|
|
62
|
+
// Check if file exists
|
|
63
|
+
if (!fs.existsSync(this.options.logPath)) {
|
|
64
|
+
this.emit('error', new Error(`Log file not found: ${this.options.logPath}`));
|
|
65
|
+
// Continue anyway - file might be created later
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Get initial file size
|
|
69
|
+
this._updateFilePosition();
|
|
70
|
+
|
|
71
|
+
// Emit historical events if requested
|
|
72
|
+
if (this.options.emitHistory) {
|
|
73
|
+
this._emitHistory();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Try to use fs.watch (more efficient)
|
|
77
|
+
try {
|
|
78
|
+
this.watcher = fs.watch(this.options.logPath, (eventType) => {
|
|
79
|
+
if (eventType === 'change') {
|
|
80
|
+
this._processNewLines();
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
this.watcher.on('error', (err) => {
|
|
85
|
+
this.emit('error', err);
|
|
86
|
+
// Fall back to polling
|
|
87
|
+
this._startPolling();
|
|
88
|
+
});
|
|
89
|
+
} catch (e) {
|
|
90
|
+
// Fall back to polling
|
|
91
|
+
this._startPolling();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
this.isWatching = true;
|
|
95
|
+
this.emit('started');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Stop watching the log file
|
|
100
|
+
*/
|
|
101
|
+
stop() {
|
|
102
|
+
if (!this.isWatching) return;
|
|
103
|
+
|
|
104
|
+
if (this.watcher) {
|
|
105
|
+
this.watcher.close();
|
|
106
|
+
this.watcher = null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (this.pollTimer) {
|
|
110
|
+
clearInterval(this.pollTimer);
|
|
111
|
+
this.pollTimer = null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
this.isWatching = false;
|
|
115
|
+
this.emit('stopped');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Start polling as fallback
|
|
120
|
+
*/
|
|
121
|
+
_startPolling() {
|
|
122
|
+
if (this.pollTimer) return;
|
|
123
|
+
|
|
124
|
+
this.pollTimer = setInterval(() => {
|
|
125
|
+
this._processNewLines();
|
|
126
|
+
}, this.options.pollInterval);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Update tracked file position
|
|
131
|
+
*/
|
|
132
|
+
_updateFilePosition() {
|
|
133
|
+
try {
|
|
134
|
+
if (fs.existsSync(this.options.logPath)) {
|
|
135
|
+
const stats = fs.statSync(this.options.logPath);
|
|
136
|
+
this.filePosition = stats.size;
|
|
137
|
+
}
|
|
138
|
+
} catch (e) {
|
|
139
|
+
// Ignore errors
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Emit historical events from file start
|
|
145
|
+
*/
|
|
146
|
+
_emitHistory() {
|
|
147
|
+
try {
|
|
148
|
+
if (!fs.existsSync(this.options.logPath)) return;
|
|
149
|
+
|
|
150
|
+
const content = fs.readFileSync(this.options.logPath, 'utf8');
|
|
151
|
+
const lines = content.trim().split('\n').filter(Boolean);
|
|
152
|
+
|
|
153
|
+
// Get last N lines
|
|
154
|
+
const historyLines = lines.slice(-this.options.historyLimit);
|
|
155
|
+
|
|
156
|
+
for (const line of historyLines) {
|
|
157
|
+
try {
|
|
158
|
+
const event = JSON.parse(line);
|
|
159
|
+
this._addToBuffer(event);
|
|
160
|
+
this.emit('event', event);
|
|
161
|
+
} catch (e) {
|
|
162
|
+
// Skip invalid JSON
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
} catch (e) {
|
|
166
|
+
this.emit('error', e);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Process new lines added to the file
|
|
172
|
+
*/
|
|
173
|
+
_processNewLines() {
|
|
174
|
+
try {
|
|
175
|
+
if (!fs.existsSync(this.options.logPath)) return;
|
|
176
|
+
|
|
177
|
+
const stats = fs.statSync(this.options.logPath);
|
|
178
|
+
|
|
179
|
+
// Handle file truncation (rotation)
|
|
180
|
+
if (stats.size < this.filePosition) {
|
|
181
|
+
this.filePosition = 0;
|
|
182
|
+
this.emit('truncated');
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// No new content
|
|
186
|
+
if (stats.size <= this.filePosition) return;
|
|
187
|
+
|
|
188
|
+
// Read new content
|
|
189
|
+
const fd = fs.openSync(this.options.logPath, 'r');
|
|
190
|
+
const bufferSize = stats.size - this.filePosition;
|
|
191
|
+
const buffer = Buffer.alloc(bufferSize);
|
|
192
|
+
|
|
193
|
+
fs.readSync(fd, buffer, 0, bufferSize, this.filePosition);
|
|
194
|
+
fs.closeSync(fd);
|
|
195
|
+
|
|
196
|
+
// Update position
|
|
197
|
+
this.filePosition = stats.size;
|
|
198
|
+
|
|
199
|
+
// Process lines
|
|
200
|
+
const content = buffer.toString('utf8');
|
|
201
|
+
const lines = content.split('\n').filter(Boolean);
|
|
202
|
+
|
|
203
|
+
for (const line of lines) {
|
|
204
|
+
try {
|
|
205
|
+
const event = JSON.parse(line);
|
|
206
|
+
this._addToBuffer(event);
|
|
207
|
+
this.emit('event', event);
|
|
208
|
+
} catch (e) {
|
|
209
|
+
// Skip invalid JSON lines
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
} catch (e) {
|
|
213
|
+
this.emit('error', e);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Add event to buffer (with size limit)
|
|
219
|
+
*/
|
|
220
|
+
_addToBuffer(event) {
|
|
221
|
+
this.buffer.push(event);
|
|
222
|
+
|
|
223
|
+
// Trim buffer if too large
|
|
224
|
+
while (this.buffer.length > this.options.maxBufferSize) {
|
|
225
|
+
this.buffer.shift();
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Get buffered events
|
|
231
|
+
*/
|
|
232
|
+
getBuffer() {
|
|
233
|
+
return [...this.buffer];
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Clear buffer
|
|
238
|
+
*/
|
|
239
|
+
clearBuffer() {
|
|
240
|
+
this.buffer = [];
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Get events by type
|
|
245
|
+
*/
|
|
246
|
+
getEventsByType(type) {
|
|
247
|
+
return this.buffer.filter(e => e.type === type);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Get events by agent
|
|
252
|
+
*/
|
|
253
|
+
getEventsByAgent(agent) {
|
|
254
|
+
return this.buffer.filter(e => e.agent === agent);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Create a singleton event stream instance
|
|
260
|
+
*/
|
|
261
|
+
let defaultStream = null;
|
|
262
|
+
|
|
263
|
+
function getDefaultStream() {
|
|
264
|
+
if (!defaultStream) {
|
|
265
|
+
defaultStream = new EventStream();
|
|
266
|
+
}
|
|
267
|
+
return defaultStream;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Format event for display
|
|
272
|
+
*/
|
|
273
|
+
function formatEvent(event) {
|
|
274
|
+
const timestamp = event.timestamp
|
|
275
|
+
? new Date(event.timestamp).toLocaleTimeString()
|
|
276
|
+
: '';
|
|
277
|
+
|
|
278
|
+
const agent = event.agent || 'unknown';
|
|
279
|
+
const eventType = event.event || event.type || 'unknown';
|
|
280
|
+
|
|
281
|
+
let message = '';
|
|
282
|
+
|
|
283
|
+
switch (event.event) {
|
|
284
|
+
case 'init':
|
|
285
|
+
message = `Loop started: gate=${event.gate}, max=${event.max_iterations}`;
|
|
286
|
+
break;
|
|
287
|
+
case 'iteration':
|
|
288
|
+
message = `Iteration ${event.iter}: value=${event.value}, passed=${event.passed}`;
|
|
289
|
+
break;
|
|
290
|
+
case 'passed':
|
|
291
|
+
message = `Loop passed! final=${event.final_value}`;
|
|
292
|
+
break;
|
|
293
|
+
case 'failed':
|
|
294
|
+
message = `Loop failed: ${event.reason}, final=${event.final_value}`;
|
|
295
|
+
break;
|
|
296
|
+
case 'abort':
|
|
297
|
+
message = `Loop aborted: ${event.reason}`;
|
|
298
|
+
break;
|
|
299
|
+
default:
|
|
300
|
+
message = JSON.stringify(event);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return {
|
|
304
|
+
timestamp,
|
|
305
|
+
agent,
|
|
306
|
+
eventType,
|
|
307
|
+
message,
|
|
308
|
+
raw: event
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
module.exports = {
|
|
313
|
+
EventStream,
|
|
314
|
+
getDefaultStream,
|
|
315
|
+
formatEvent
|
|
316
|
+
};
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Keyboard Handler - Key bindings for TUI
|
|
5
|
+
*
|
|
6
|
+
* Provides centralized keyboard handling with configurable bindings
|
|
7
|
+
* and event emission for TUI components.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const EventEmitter = require('events');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Default key bindings
|
|
14
|
+
*/
|
|
15
|
+
const DEFAULT_BINDINGS = {
|
|
16
|
+
quit: { key: 'q', description: 'Quit TUI' },
|
|
17
|
+
start: { key: 's', description: 'Start loop' },
|
|
18
|
+
pause: { key: 'p', description: 'Pause loop' },
|
|
19
|
+
resume: { key: 'r', description: 'Resume loop' },
|
|
20
|
+
trace: { key: 't', description: 'Toggle trace' },
|
|
21
|
+
help: { key: '?', description: 'Show help' },
|
|
22
|
+
// Session switching (1-9)
|
|
23
|
+
session1: { key: '1', description: 'Session 1' },
|
|
24
|
+
session2: { key: '2', description: 'Session 2' },
|
|
25
|
+
session3: { key: '3', description: 'Session 3' },
|
|
26
|
+
session4: { key: '4', description: 'Session 4' },
|
|
27
|
+
session5: { key: '5', description: 'Session 5' },
|
|
28
|
+
session6: { key: '6', description: 'Session 6' },
|
|
29
|
+
session7: { key: '7', description: 'Session 7' },
|
|
30
|
+
session8: { key: '8', description: 'Session 8' },
|
|
31
|
+
session9: { key: '9', description: 'Session 9' }
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* KeyboardHandler class - centralized keyboard handling
|
|
36
|
+
*/
|
|
37
|
+
class KeyboardHandler extends EventEmitter {
|
|
38
|
+
constructor(options = {}) {
|
|
39
|
+
super();
|
|
40
|
+
|
|
41
|
+
this.bindings = { ...DEFAULT_BINDINGS, ...options.bindings };
|
|
42
|
+
this.enabled = true;
|
|
43
|
+
this.keyHistory = [];
|
|
44
|
+
this.historyLimit = options.historyLimit || 50;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Process a key press
|
|
49
|
+
* @param {string} input - The key character
|
|
50
|
+
* @param {object} key - The key object from useInput
|
|
51
|
+
* @returns {object|null} - Action performed or null
|
|
52
|
+
*/
|
|
53
|
+
processKey(input, key = {}) {
|
|
54
|
+
if (!this.enabled) return null;
|
|
55
|
+
|
|
56
|
+
// Record key press
|
|
57
|
+
this._recordKey(input, key);
|
|
58
|
+
|
|
59
|
+
// Handle Ctrl+C for quit
|
|
60
|
+
if (key.ctrl && input === 'c') {
|
|
61
|
+
const action = { action: 'quit', key: 'ctrl+c' };
|
|
62
|
+
this.emit('action', action);
|
|
63
|
+
this.emit('quit');
|
|
64
|
+
return action;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Handle escape for quit
|
|
68
|
+
if (key.escape) {
|
|
69
|
+
const action = { action: 'quit', key: 'escape' };
|
|
70
|
+
this.emit('action', action);
|
|
71
|
+
this.emit('quit');
|
|
72
|
+
return action;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Normalize input to lowercase for matching
|
|
76
|
+
const normalizedInput = input.toLowerCase();
|
|
77
|
+
|
|
78
|
+
// Check against bindings
|
|
79
|
+
for (const [actionName, binding] of Object.entries(this.bindings)) {
|
|
80
|
+
if (binding.key === normalizedInput || binding.key === input) {
|
|
81
|
+
const action = { action: actionName, key: input };
|
|
82
|
+
this.emit('action', action);
|
|
83
|
+
this.emit(actionName, action);
|
|
84
|
+
|
|
85
|
+
// Special handling for session switching
|
|
86
|
+
if (actionName.startsWith('session')) {
|
|
87
|
+
const sessionNum = parseInt(actionName.replace('session', ''), 10);
|
|
88
|
+
this.emit('sessionSwitch', { session: sessionNum });
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return action;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Unknown key
|
|
96
|
+
this.emit('unknownKey', { key: input });
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Record key press to history
|
|
102
|
+
*/
|
|
103
|
+
_recordKey(input, key) {
|
|
104
|
+
this.keyHistory.push({
|
|
105
|
+
input,
|
|
106
|
+
key,
|
|
107
|
+
timestamp: Date.now()
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// Trim history
|
|
111
|
+
while (this.keyHistory.length > this.historyLimit) {
|
|
112
|
+
this.keyHistory.shift();
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Get key history
|
|
118
|
+
*/
|
|
119
|
+
getHistory() {
|
|
120
|
+
return [...this.keyHistory];
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Clear key history
|
|
125
|
+
*/
|
|
126
|
+
clearHistory() {
|
|
127
|
+
this.keyHistory = [];
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Enable keyboard handling
|
|
132
|
+
*/
|
|
133
|
+
enable() {
|
|
134
|
+
this.enabled = true;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Disable keyboard handling
|
|
139
|
+
*/
|
|
140
|
+
disable() {
|
|
141
|
+
this.enabled = false;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Check if a key is bound
|
|
146
|
+
*/
|
|
147
|
+
isBound(key) {
|
|
148
|
+
return Object.values(this.bindings).some(b => b.key === key);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Get binding for action
|
|
153
|
+
*/
|
|
154
|
+
getBinding(action) {
|
|
155
|
+
return this.bindings[action] || null;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Get all bindings
|
|
160
|
+
*/
|
|
161
|
+
getBindings() {
|
|
162
|
+
return { ...this.bindings };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Set custom binding
|
|
167
|
+
*/
|
|
168
|
+
setBinding(action, key, description) {
|
|
169
|
+
this.bindings[action] = { key, description };
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Format key bindings for footer display
|
|
175
|
+
* @param {object} bindings - Key bindings object
|
|
176
|
+
* @returns {string[]} - Array of formatted binding strings
|
|
177
|
+
*/
|
|
178
|
+
function formatBindings(bindings = DEFAULT_BINDINGS) {
|
|
179
|
+
// Primary bindings to show in footer (exclude session numbers)
|
|
180
|
+
const primaryBindings = ['quit', 'start', 'pause', 'resume', 'trace', 'help'];
|
|
181
|
+
|
|
182
|
+
return primaryBindings
|
|
183
|
+
.filter(name => bindings[name])
|
|
184
|
+
.map(name => {
|
|
185
|
+
const binding = bindings[name];
|
|
186
|
+
return `${binding.key.toUpperCase()}:${binding.description}`;
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Format bindings as help text
|
|
192
|
+
* @param {object} bindings - Key bindings object
|
|
193
|
+
* @returns {string} - Multi-line help text
|
|
194
|
+
*/
|
|
195
|
+
function formatHelp(bindings = DEFAULT_BINDINGS) {
|
|
196
|
+
const lines = ['Key Bindings:', ''];
|
|
197
|
+
|
|
198
|
+
// Group bindings
|
|
199
|
+
const groups = {
|
|
200
|
+
'Loop Control': ['start', 'pause', 'resume'],
|
|
201
|
+
'View': ['trace', 'help'],
|
|
202
|
+
'Navigation': ['quit'],
|
|
203
|
+
'Sessions': ['session1', 'session2', 'session3', 'session4', 'session5',
|
|
204
|
+
'session6', 'session7', 'session8', 'session9']
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
for (const [groupName, actions] of Object.entries(groups)) {
|
|
208
|
+
lines.push(` ${groupName}:`);
|
|
209
|
+
for (const action of actions) {
|
|
210
|
+
if (bindings[action]) {
|
|
211
|
+
const binding = bindings[action];
|
|
212
|
+
lines.push(` ${binding.key.toUpperCase()} - ${binding.description}`);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
lines.push('');
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return lines.join('\n');
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Create default keyboard handler instance
|
|
223
|
+
*/
|
|
224
|
+
let defaultHandler = null;
|
|
225
|
+
|
|
226
|
+
function getDefaultHandler() {
|
|
227
|
+
if (!defaultHandler) {
|
|
228
|
+
defaultHandler = new KeyboardHandler();
|
|
229
|
+
}
|
|
230
|
+
return defaultHandler;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* React hook factory for keyboard handling
|
|
235
|
+
* Returns a function that can be used with useInput
|
|
236
|
+
*/
|
|
237
|
+
function createKeyHandler(handler = null) {
|
|
238
|
+
const kbd = handler || getDefaultHandler();
|
|
239
|
+
|
|
240
|
+
return function handleKey(input, key) {
|
|
241
|
+
return kbd.processKey(input, key);
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
module.exports = {
|
|
246
|
+
KeyboardHandler,
|
|
247
|
+
DEFAULT_BINDINGS,
|
|
248
|
+
formatBindings,
|
|
249
|
+
formatHelp,
|
|
250
|
+
getDefaultHandler,
|
|
251
|
+
createKeyHandler
|
|
252
|
+
};
|