anantham-v2 2.0.1 → 2.0.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.
Files changed (66) hide show
  1. package/dist/bin/anantham.js +12 -1
  2. package/dist/bin/anantham.js.map +1 -1
  3. package/dist/cli/command-parser.d.ts +5 -0
  4. package/dist/cli/command-parser.d.ts.map +1 -1
  5. package/dist/cli/command-parser.js +40 -9
  6. package/dist/cli/command-parser.js.map +1 -1
  7. package/dist/cli/command-registry.d.ts +8 -0
  8. package/dist/cli/command-registry.d.ts.map +1 -1
  9. package/dist/cli/command-registry.js +425 -4
  10. package/dist/cli/command-registry.js.map +1 -1
  11. package/dist/cli/error-handler.d.ts.map +1 -1
  12. package/dist/cli/error-handler.js +55 -1
  13. package/dist/cli/error-handler.js.map +1 -1
  14. package/dist/domain/tui.d.ts +1 -1
  15. package/dist/domain/tui.d.ts.map +1 -1
  16. package/dist/domain/tui.js +1 -0
  17. package/dist/domain/tui.js.map +1 -1
  18. package/dist/models/adapters/openai-compatible-adapter.d.ts.map +1 -1
  19. package/dist/models/adapters/openai-compatible-adapter.js +2 -1
  20. package/dist/models/adapters/openai-compatible-adapter.js.map +1 -1
  21. package/dist/models/provider-registry.d.ts +2 -2
  22. package/dist/persistence/index.d.ts +2 -0
  23. package/dist/persistence/index.d.ts.map +1 -1
  24. package/dist/persistence/index.js +2 -0
  25. package/dist/persistence/index.js.map +1 -1
  26. package/dist/persistence/token-metrics-manager.d.ts +88 -0
  27. package/dist/persistence/token-metrics-manager.d.ts.map +1 -0
  28. package/dist/persistence/token-metrics-manager.js +272 -0
  29. package/dist/persistence/token-metrics-manager.js.map +1 -0
  30. package/dist/persistence/user-config-manager.d.ts +40 -0
  31. package/dist/persistence/user-config-manager.d.ts.map +1 -0
  32. package/dist/persistence/user-config-manager.js +175 -0
  33. package/dist/persistence/user-config-manager.js.map +1 -0
  34. package/dist/tui/ansi-gradient.d.ts +43 -0
  35. package/dist/tui/ansi-gradient.d.ts.map +1 -0
  36. package/dist/tui/ansi-gradient.js +131 -0
  37. package/dist/tui/ansi-gradient.js.map +1 -0
  38. package/dist/tui/command-palette.d.ts +20 -0
  39. package/dist/tui/command-palette.d.ts.map +1 -0
  40. package/dist/tui/command-palette.js +241 -0
  41. package/dist/tui/command-palette.js.map +1 -0
  42. package/dist/tui/terminal-layout.d.ts +4 -0
  43. package/dist/tui/terminal-layout.d.ts.map +1 -1
  44. package/dist/tui/terminal-layout.js +42 -4
  45. package/dist/tui/terminal-layout.js.map +1 -1
  46. package/dist/tui/token-dashboard-renderer.d.ts +6 -0
  47. package/dist/tui/token-dashboard-renderer.d.ts.map +1 -0
  48. package/dist/tui/token-dashboard-renderer.js +115 -0
  49. package/dist/tui/token-dashboard-renderer.js.map +1 -0
  50. package/dist/tui/tui-application.d.ts +7 -0
  51. package/dist/tui/tui-application.d.ts.map +1 -1
  52. package/dist/tui/tui-application.js +110 -5
  53. package/dist/tui/tui-application.js.map +1 -1
  54. package/dist/tui/tui-controller.d.ts +78 -2
  55. package/dist/tui/tui-controller.d.ts.map +1 -1
  56. package/dist/tui/tui-controller.js +564 -24
  57. package/dist/tui/tui-controller.js.map +1 -1
  58. package/dist/tui/tui-renderer.d.ts +12 -1
  59. package/dist/tui/tui-renderer.d.ts.map +1 -1
  60. package/dist/tui/tui-renderer.js +104 -40
  61. package/dist/tui/tui-renderer.js.map +1 -1
  62. package/dist/tui/tui-sanitizer.d.ts +4 -0
  63. package/dist/tui/tui-sanitizer.d.ts.map +1 -1
  64. package/dist/tui/tui-sanitizer.js +6 -0
  65. package/dist/tui/tui-sanitizer.js.map +1 -1
  66. package/package.json +1 -1
@@ -1,3 +1,5 @@
1
+ import { CommandPalette } from "./command-palette.js";
2
+ import { UserConfigManager } from "../persistence/user-config-manager.js";
1
3
  /**
2
4
  * TUI Controller: Manages navigation, keyboard input, command execution, and render coalescing.
3
5
  * PRD Part 2 Section 188–195.
@@ -10,6 +12,7 @@ export class TuiController {
10
12
  errorHandler;
11
13
  output;
12
14
  coalesceIntervalMs;
15
+ maxHistorySize;
13
16
  currentView = "dashboard";
14
17
  isCommandMode = false;
15
18
  commandBuffer = "";
@@ -17,6 +20,14 @@ export class TuiController {
17
20
  isRunning = false;
18
21
  renderTimeout;
19
22
  unregisterStateListener;
23
+ commandHistory = [];
24
+ historyIndex = -1;
25
+ historyDraft = "";
26
+ savedDraft = "";
27
+ commandOutput = null;
28
+ cursorPosition = 0;
29
+ isBracketedPaste = false;
30
+ paletteSelectedIndex = 0;
20
31
  constructor(options) {
21
32
  this.stateAdapter = options.stateAdapter;
22
33
  this.renderer = options.renderer;
@@ -25,6 +36,7 @@ export class TuiController {
25
36
  this.errorHandler = options.errorHandler;
26
37
  this.output = options.output ?? process.stdout;
27
38
  this.coalesceIntervalMs = options.coalesceIntervalMs ?? 30;
39
+ this.maxHistorySize = options.maxHistorySize ?? 100;
28
40
  this.attachStateListener();
29
41
  }
30
42
  attachStateListener() {
@@ -37,10 +49,26 @@ export class TuiController {
37
49
  }
38
50
  setView(view) {
39
51
  this.currentView = view;
52
+ this.commandOutput = null;
40
53
  this.requestRender();
41
54
  }
55
+ setDimensions(dims) {
56
+ this.renderer.setDimensions(dims);
57
+ this.requestRender();
58
+ }
59
+ getCursorPosition() {
60
+ return this.cursorPosition;
61
+ }
62
+ getSavedDraft() {
63
+ return this.savedDraft;
64
+ }
65
+ clearSavedDraft() {
66
+ this.savedDraft = "";
67
+ }
42
68
  start() {
43
69
  this.isRunning = true;
70
+ // Enter alternate screen buffer, hide cursor, clear and home
71
+ this.output.write("\x1b[?1049h\x1b[?25l\x1b[2J\x1b[H");
44
72
  this.renderNow();
45
73
  }
46
74
  stop() {
@@ -53,6 +81,8 @@ export class TuiController {
53
81
  this.unregisterStateListener();
54
82
  this.unregisterStateListener = undefined;
55
83
  }
84
+ // Restore normal screen buffer and restore cursor
85
+ this.output.write("\x1b[?1049l\x1b[?25h");
56
86
  }
57
87
  /**
58
88
  * Request a coalesced render frame to prevent event storms.
@@ -71,47 +101,470 @@ export class TuiController {
71
101
  * Render immediately to output stream.
72
102
  */
73
103
  renderNow() {
74
- const rendered = this.renderer.render(this.currentView, this.stateAdapter, this.isCommandMode ? this.commandBuffer : "", this.errorMessage);
75
- // Clear screen and redraw
76
- this.output.write("\x1b[2J\x1b[H" + rendered + "\n");
104
+ let paletteOverlay;
105
+ if (this.isCommandMode && this.commandBuffer.startsWith("/")) {
106
+ const filtered = CommandPalette.filterCommands(this.commandBuffer);
107
+ paletteOverlay = {
108
+ filtered,
109
+ selectedIndex: this.paletteSelectedIndex,
110
+ };
111
+ }
112
+ let activeModel;
113
+ try {
114
+ activeModel = UserConfigManager.getInstance().getDefaultModel();
115
+ }
116
+ catch {
117
+ // Fallback
118
+ }
119
+ const rendered = this.renderer.render(this.currentView, this.stateAdapter, this.isCommandMode ? this.commandBuffer : "", this.errorMessage, this.isCommandMode, this.isCommandMode ? this.cursorPosition : undefined, this.commandOutput ?? undefined, paletteOverlay, activeModel);
120
+ // In alternate buffer, position at home (1,1) and write frame WITHOUT trailing newline!
121
+ this.output.write("\x1b[H" + rendered);
122
+ }
123
+ /**
124
+ * Helper to identify Up Arrow key sequences across standard ANSI, VT, and modified variants.
125
+ */
126
+ static isArrowUp(token) {
127
+ return /^\x1b(?:\[|O)[0-9;]*[Aa]$/.test(token);
128
+ }
129
+ /**
130
+ * Helper to identify Down Arrow key sequences across standard ANSI, VT, and modified variants.
131
+ */
132
+ static isArrowDown(token) {
133
+ return /^\x1b(?:\[|O)[0-9;]*[Bb]$/.test(token);
134
+ }
135
+ /**
136
+ * Helper to identify Left Arrow key sequences across standard ANSI, VT, and modified variants.
137
+ */
138
+ static isArrowLeft(token) {
139
+ return /^\x1b(?:\[|O)[0-9;]*[Dd]$/.test(token);
140
+ }
141
+ /**
142
+ * Helper to identify Right Arrow key sequences across standard ANSI, VT, and modified variants.
143
+ */
144
+ static isArrowRight(token) {
145
+ return /^\x1b(?:\[|O)[0-9;]*[Cc]$/.test(token);
146
+ }
147
+ /**
148
+ * Helper to identify Home key sequences across standard terminals.
149
+ */
150
+ static isHome(token) {
151
+ return /^\x1b(?:\[(?:[0-9;]*[Hh]|[17]~)|OH)$/.test(token);
152
+ }
153
+ /**
154
+ * Helper to identify End key sequences across standard terminals.
155
+ */
156
+ static isEnd(token) {
157
+ return /^\x1b(?:\[(?:[0-9;]*[Ff]|[48]~)|OF)$/.test(token);
158
+ }
159
+ /**
160
+ * Helper to identify Forward Delete key sequences across standard terminals.
161
+ */
162
+ static isDelete(token) {
163
+ return /^\x1b\[3(?:;[0-9]+)*~$/.test(token);
164
+ }
165
+ /**
166
+ * Helper to identify Page Up key sequences across standard terminals.
167
+ */
168
+ static isPageUp(token) {
169
+ return /^\x1b\[5(?:;[0-9]+)*~$/.test(token);
170
+ }
171
+ /**
172
+ * Helper to identify Page Down key sequences across standard terminals.
173
+ */
174
+ static isPageDown(token) {
175
+ return /^\x1b\[6(?:;[0-9]+)*~$/.test(token);
176
+ }
177
+ /**
178
+ * Helper to identify Function keys (F1-F12) across standard terminals.
179
+ */
180
+ static isFunctionKey(token) {
181
+ return /^\x1b(?:O[P-S]|\[(?:1[1-57-9]|2[0-13-4])(?:;[0-9]+)*~)$/.test(token);
182
+ }
183
+ /**
184
+ * Helper to identify any ANSI escape sequence.
185
+ */
186
+ static isEscapeSequence(token) {
187
+ return token.startsWith("\x1b") && token.length > 1;
188
+ }
189
+ /**
190
+ * Decode an input string into discrete key tokens and remainder buffer.
191
+ */
192
+ static decodeInputTokens(input, flush = false) {
193
+ const tokens = [];
194
+ let i = 0;
195
+ while (i < input.length) {
196
+ if (input[i] === "\x1b") {
197
+ if (i + 1 >= input.length) {
198
+ if (flush) {
199
+ tokens.push("\x1b");
200
+ i++;
201
+ }
202
+ break;
203
+ }
204
+ const nextChar = input[i + 1];
205
+ if (nextChar === "[") {
206
+ // CSI sequence: \x1b[ ... <terminator in 0x40-0x7E>
207
+ let j = i + 2;
208
+ while (j < input.length) {
209
+ const code = input.charCodeAt(j);
210
+ if (code >= 0x40 && code <= 0x7e) {
211
+ break;
212
+ }
213
+ j++;
214
+ }
215
+ if (j < input.length) {
216
+ tokens.push(input.slice(i, j + 1));
217
+ i = j + 1;
218
+ }
219
+ else {
220
+ if (flush) {
221
+ tokens.push(input.slice(i));
222
+ i = input.length;
223
+ }
224
+ break;
225
+ }
226
+ }
227
+ else if (nextChar === "O") {
228
+ // SS3 sequence: \x1bO <char>
229
+ if (i + 2 < input.length) {
230
+ tokens.push(input.slice(i, i + 3));
231
+ i += 3;
232
+ }
233
+ else {
234
+ if (flush) {
235
+ tokens.push(input.slice(i));
236
+ i = input.length;
237
+ }
238
+ break;
239
+ }
240
+ }
241
+ else if (nextChar === "]") {
242
+ // OSC sequence: \x1b] ... <BEL (\x07) or ST (\x1b\\)>
243
+ let j = i + 2;
244
+ let found = false;
245
+ while (j < input.length) {
246
+ if (input[j] === "\x07") {
247
+ found = true;
248
+ break;
249
+ }
250
+ if (input[j] === "\x1b" && j + 1 < input.length && input[j + 1] === "\\") {
251
+ j++; // include backslash
252
+ found = true;
253
+ break;
254
+ }
255
+ j++;
256
+ }
257
+ if (found) {
258
+ tokens.push(input.slice(i, j + 1));
259
+ i = j + 1;
260
+ }
261
+ else {
262
+ if (flush) {
263
+ tokens.push(input.slice(i));
264
+ i = input.length;
265
+ }
266
+ break;
267
+ }
268
+ }
269
+ else {
270
+ // 2-character escape sequence (e.g. Alt+<char>, \x1b<char>)
271
+ tokens.push(input.slice(i, i + 2));
272
+ i += 2;
273
+ }
274
+ }
275
+ else {
276
+ tokens.push(input[i]);
277
+ i++;
278
+ }
279
+ }
280
+ return { tokens, remainder: input.slice(i) };
77
281
  }
78
282
  /**
79
283
  * Handle raw keyboard / character input.
80
284
  */
81
- async handleInput(char) {
285
+ async handleInput(input) {
82
286
  this.errorMessage = "";
287
+ const { tokens } = TuiController.decodeInputTokens(input, true);
288
+ for (const token of tokens) {
289
+ const keepRunning = await this.handleSingleToken(token);
290
+ if (!keepRunning) {
291
+ return false;
292
+ }
293
+ }
294
+ return true;
295
+ }
296
+ /**
297
+ * Process a single decoded token or key sequence.
298
+ */
299
+ async handleSingleToken(token) {
300
+ // Bracketed paste markers (200~ start, 201~ end)
301
+ if (token === "\x1b[200~") {
302
+ this.isBracketedPaste = true;
303
+ return true;
304
+ }
305
+ if (token === "\x1b[201~") {
306
+ this.isBracketedPaste = false;
307
+ return true;
308
+ }
309
+ if (this.isBracketedPaste) {
310
+ if (this.isCommandMode) {
311
+ if (token === "\r" || token === "\n" || token === "\t") {
312
+ // In bracketed paste, replace newlines/tabs with space rather than submitting prematurely
313
+ if (this.commandBuffer.length > 0 && !this.commandBuffer.endsWith(" ")) {
314
+ this.commandBuffer += " ";
315
+ this.cursorPosition = this.commandBuffer.length;
316
+ this.requestRender();
317
+ }
318
+ return true;
319
+ }
320
+ if (TuiController.isEscapeSequence(token)) {
321
+ return true;
322
+ }
323
+ if (token.length === 1 && token >= " ") {
324
+ this.commandBuffer =
325
+ this.commandBuffer.slice(0, this.cursorPosition) + token + this.commandBuffer.slice(this.cursorPosition);
326
+ this.cursorPosition++;
327
+ this.requestRender();
328
+ return true;
329
+ }
330
+ }
331
+ // Swallow pasted characters in normal mode to prevent accidental hotkey execution
332
+ return true;
333
+ }
83
334
  if (this.isCommandMode) {
84
- if (char === "\r" || char === "\n") {
85
- // Submit command
86
- const toExec = this.commandBuffer.trim();
335
+ // 1. Command Palette / History navigation via Up / Down arrows
336
+ if (TuiController.isArrowUp(token)) {
337
+ if (this.commandBuffer.startsWith("/")) {
338
+ const filtered = CommandPalette.filterCommands(this.commandBuffer);
339
+ if (filtered.length > 0) {
340
+ this.paletteSelectedIndex =
341
+ (this.paletteSelectedIndex - 1 + filtered.length) % filtered.length;
342
+ this.requestRender();
343
+ return true;
344
+ }
345
+ }
346
+ if (this.commandHistory.length > 0) {
347
+ if (this.historyIndex === -1) {
348
+ this.historyDraft = this.commandBuffer;
349
+ this.historyIndex = this.commandHistory.length - 1;
350
+ }
351
+ else if (this.historyIndex > 0) {
352
+ this.historyIndex--;
353
+ }
354
+ this.commandBuffer = this.commandHistory[this.historyIndex];
355
+ this.cursorPosition = this.commandBuffer.length;
356
+ this.requestRender();
357
+ }
358
+ return true;
359
+ }
360
+ if (TuiController.isArrowDown(token)) {
361
+ if (this.commandBuffer.startsWith("/")) {
362
+ const filtered = CommandPalette.filterCommands(this.commandBuffer);
363
+ if (filtered.length > 0) {
364
+ this.paletteSelectedIndex =
365
+ (this.paletteSelectedIndex + 1) % filtered.length;
366
+ this.requestRender();
367
+ return true;
368
+ }
369
+ }
370
+ if (this.historyIndex !== -1) {
371
+ if (this.historyIndex < this.commandHistory.length - 1) {
372
+ this.historyIndex++;
373
+ this.commandBuffer = this.commandHistory[this.historyIndex];
374
+ }
375
+ else {
376
+ this.historyIndex = -1;
377
+ this.commandBuffer = this.historyDraft;
378
+ }
379
+ this.cursorPosition = this.commandBuffer.length;
380
+ this.requestRender();
381
+ }
382
+ return true;
383
+ }
384
+ // 2. Cursor navigation: Left, Right, Home, End
385
+ if (TuiController.isArrowLeft(token)) {
386
+ if (this.cursorPosition > 0) {
387
+ this.cursorPosition--;
388
+ this.requestRender();
389
+ }
390
+ return true;
391
+ }
392
+ if (TuiController.isArrowRight(token)) {
393
+ if (this.cursorPosition < this.commandBuffer.length) {
394
+ this.cursorPosition++;
395
+ this.requestRender();
396
+ }
397
+ return true;
398
+ }
399
+ if (TuiController.isHome(token)) {
400
+ this.cursorPosition = 0;
401
+ this.requestRender();
402
+ return true;
403
+ }
404
+ if (TuiController.isEnd(token)) {
405
+ this.cursorPosition = this.commandBuffer.length;
406
+ this.requestRender();
407
+ return true;
408
+ }
409
+ // Forward Delete: deletes character at cursor position
410
+ if (TuiController.isDelete(token)) {
411
+ if (this.cursorPosition < this.commandBuffer.length) {
412
+ this.commandBuffer =
413
+ this.commandBuffer.slice(0, this.cursorPosition) + this.commandBuffer.slice(this.cursorPosition + 1);
414
+ this.requestRender();
415
+ }
416
+ return true;
417
+ }
418
+ // Other unhandled escape sequences: ignore without polluting buffer
419
+ if (TuiController.isEscapeSequence(token)) {
420
+ return true;
421
+ }
422
+ // Tab completion for command palette
423
+ if (token === "\t") {
424
+ if (this.commandBuffer.startsWith("/")) {
425
+ const filtered = CommandPalette.filterCommands(this.commandBuffer);
426
+ if (filtered.length > 0) {
427
+ const selected = filtered[Math.min(this.paletteSelectedIndex, filtered.length - 1)];
428
+ if (selected) {
429
+ this.commandBuffer = selected.command + " ";
430
+ this.cursorPosition = this.commandBuffer.length;
431
+ this.paletteSelectedIndex = 0;
432
+ this.requestRender();
433
+ return true;
434
+ }
435
+ }
436
+ }
437
+ return true;
438
+ }
439
+ // 3. Submit command (Enter)
440
+ if (token === "\r" || token === "\n") {
441
+ let toExec = this.commandBuffer.trim();
442
+ if (toExec === "/" && this.commandBuffer.startsWith("/")) {
443
+ const filtered = CommandPalette.filterCommands(this.commandBuffer);
444
+ if (filtered.length > 0) {
445
+ toExec = filtered[this.paletteSelectedIndex]?.command ?? toExec;
446
+ }
447
+ }
87
448
  this.isCommandMode = false;
88
449
  this.commandBuffer = "";
89
- if (toExec) {
450
+ this.cursorPosition = 0;
451
+ this.historyIndex = -1;
452
+ this.historyDraft = "";
453
+ this.savedDraft = "";
454
+ this.paletteSelectedIndex = 0;
455
+ this.isBracketedPaste = false;
456
+ // Empty command line or bare '/' / ':' cancels or clears without Zod error dump
457
+ if (toExec && !/^[/:\s]*$/.test(toExec)) {
458
+ this.recordCommandHistory(toExec);
90
459
  await this.executeCommand(toExec);
91
460
  }
92
461
  this.requestRender();
93
462
  return true;
94
463
  }
95
- if (char === "\u001B" || char === "\u0003") {
96
- // Cancel command mode (ESC or Ctrl+C)
464
+ // 4. Cancel command mode
465
+ // ESC (\u001B): preserves draft for rapid mode toggle (: -> ESC -> :)
466
+ if (token === "\u001B") {
467
+ const draftToSave = this.historyIndex !== -1 ? this.historyDraft : this.commandBuffer;
468
+ this.savedDraft = draftToSave;
97
469
  this.isCommandMode = false;
98
470
  this.commandBuffer = "";
471
+ this.cursorPosition = 0;
472
+ this.historyIndex = -1;
473
+ this.historyDraft = "";
474
+ this.paletteSelectedIndex = 0;
475
+ this.isBracketedPaste = false;
99
476
  this.requestRender();
100
477
  return true;
101
478
  }
102
- if (char === "\u007F" || char === "\b") {
103
- // Backspace
104
- this.commandBuffer = this.commandBuffer.slice(0, -1);
479
+ // Ctrl+C (\u0003): explicitly aborts command mode and discards draft
480
+ if (token === "\u0003") {
481
+ this.savedDraft = "";
482
+ this.isCommandMode = false;
483
+ this.commandBuffer = "";
484
+ this.cursorPosition = 0;
485
+ this.historyIndex = -1;
486
+ this.historyDraft = "";
487
+ this.isBracketedPaste = false;
488
+ this.requestRender();
489
+ return true;
490
+ }
491
+ // Ctrl+D (\u0004): EOF on empty buffer exits command mode cleanly
492
+ if (token === "\u0004") {
493
+ if (this.commandBuffer.length === 0) {
494
+ this.isCommandMode = false;
495
+ this.cursorPosition = 0;
496
+ this.historyIndex = -1;
497
+ this.requestRender();
498
+ }
499
+ return true;
500
+ }
501
+ // 5. Backspace
502
+ if (token === "\u007F" || token === "\b") {
503
+ if (this.cursorPosition > 0) {
504
+ this.commandBuffer =
505
+ this.commandBuffer.slice(0, this.cursorPosition - 1) + this.commandBuffer.slice(this.cursorPosition);
506
+ this.cursorPosition--;
507
+ this.requestRender();
508
+ }
509
+ return true;
510
+ }
511
+ // Ctrl+L (\u000c): Redraw screen
512
+ if (token === "\u000c") {
513
+ this.renderNow();
514
+ return true;
515
+ }
516
+ // Ctrl+U (\u0015): Erase line
517
+ if (token === "\u0015") {
518
+ this.commandBuffer = "";
519
+ this.cursorPosition = 0;
520
+ this.requestRender();
521
+ return true;
522
+ }
523
+ // Ctrl+W (\u0017): Erase previous word
524
+ if (token === "\u0017") {
525
+ const left = this.commandBuffer.slice(0, this.cursorPosition);
526
+ const right = this.commandBuffer.slice(this.cursorPosition);
527
+ const pruned = left.replace(/\s*\S+\s*$/, "");
528
+ this.commandBuffer = pruned + right;
529
+ this.cursorPosition = pruned.length;
530
+ this.requestRender();
531
+ return true;
532
+ }
533
+ // 6. Append printable character (skip unhandled control characters)
534
+ if (!token.startsWith("\x1b") && token.length === 1 && token >= " ") {
535
+ this.commandBuffer =
536
+ this.commandBuffer.slice(0, this.cursorPosition) + token + this.commandBuffer.slice(this.cursorPosition);
537
+ this.cursorPosition++;
538
+ this.paletteSelectedIndex = 0;
105
539
  this.requestRender();
106
540
  return true;
107
541
  }
108
- // Append character
109
- this.commandBuffer += char;
110
- this.requestRender();
111
542
  return true;
112
543
  }
113
544
  // Normal navigation mode
114
- switch (char) {
545
+ // 1. Arrow keys and escape sequences: MUST NOT terminate the application
546
+ if (TuiController.isArrowUp(token) ||
547
+ TuiController.isArrowDown(token) ||
548
+ TuiController.isArrowLeft(token) ||
549
+ TuiController.isArrowRight(token) ||
550
+ TuiController.isHome(token) ||
551
+ TuiController.isEnd(token) ||
552
+ TuiController.isDelete(token) ||
553
+ TuiController.isPageUp(token) ||
554
+ TuiController.isPageDown(token) ||
555
+ TuiController.isFunctionKey(token) ||
556
+ TuiController.isEscapeSequence(token)) {
557
+ return true;
558
+ }
559
+ // 2. Normal mode actions
560
+ if (this.commandOutput) {
561
+ if (token === "c" || token === "C" || token === "\x1b" || token === "\r" || token === "\n") {
562
+ this.commandOutput = null;
563
+ this.requestRender();
564
+ return true;
565
+ }
566
+ }
567
+ switch (token) {
115
568
  case "1":
116
569
  this.setView("dashboard");
117
570
  return true;
@@ -139,25 +592,77 @@ export class TuiController {
139
592
  case "9":
140
593
  this.setView("events");
141
594
  return true;
595
+ case "u":
596
+ case "U":
597
+ this.setView("usage");
598
+ return true;
142
599
  case "?":
143
600
  case "h":
144
601
  this.setView("help");
145
602
  return true;
603
+ case ":":
604
+ this.isCommandMode = true;
605
+ this.commandBuffer = this.savedDraft;
606
+ this.cursorPosition = this.commandBuffer.length;
607
+ this.historyIndex = -1;
608
+ this.historyDraft = this.savedDraft;
609
+ this.paletteSelectedIndex = 0;
610
+ this.requestRender();
611
+ return true;
146
612
  case "/":
147
613
  this.isCommandMode = true;
148
- this.commandBuffer = "";
614
+ this.commandBuffer = this.savedDraft || "/";
615
+ this.cursorPosition = this.commandBuffer.length;
616
+ this.historyIndex = -1;
617
+ this.historyDraft = this.commandBuffer;
618
+ this.paletteSelectedIndex = 0;
149
619
  this.requestRender();
150
620
  return true;
151
621
  case "r":
152
- this.requestRender();
622
+ case "\u000c": // Ctrl+L redraws screen
623
+ this.renderNow();
153
624
  return true;
154
625
  case "q":
155
- case "\u001B": // ESC
626
+ case "\u001B":
627
+ case "\u0003": // Ctrl+C terminates normal mode
628
+ case "\u0004": // Ctrl+D (EOF) terminates normal mode
156
629
  this.stop();
157
630
  return false;
158
631
  }
159
632
  return true;
160
633
  }
634
+ recordCommandHistory(command) {
635
+ const norm = (cmd) => cmd.replace(/^[/:\s]+/, "").trim();
636
+ if (this.commandHistory.length === 0 ||
637
+ norm(this.commandHistory[this.commandHistory.length - 1]) !== norm(command)) {
638
+ this.commandHistory.push(command);
639
+ while (this.commandHistory.length > this.maxHistorySize) {
640
+ this.commandHistory.shift();
641
+ }
642
+ }
643
+ }
644
+ getCommandHistory() {
645
+ return [...this.commandHistory];
646
+ }
647
+ clearCommandHistory() {
648
+ this.commandHistory = [];
649
+ this.historyIndex = -1;
650
+ this.historyDraft = "";
651
+ this.savedDraft = "";
652
+ this.cursorPosition = 0;
653
+ }
654
+ isInCommandMode() {
655
+ return this.isCommandMode;
656
+ }
657
+ getCommandBuffer() {
658
+ return this.commandBuffer;
659
+ }
660
+ getErrorMessage() {
661
+ return this.errorMessage;
662
+ }
663
+ getIsRunning() {
664
+ return this.isRunning;
665
+ }
161
666
  /**
162
667
  * Bridge slash command to authoritative CommandRegistry.
163
668
  */
@@ -166,11 +671,45 @@ export class TuiController {
166
671
  this.errorMessage = "Command execution bridge unavailable.";
167
672
  return;
168
673
  }
674
+ const trimmed = rawCommand.trim();
675
+ if (!trimmed || /^[/:\s]*$/.test(trimmed)) {
676
+ return;
677
+ }
169
678
  try {
170
- const parsed = this.commandParser.parse(rawCommand.startsWith("/") ? rawCommand : `/${rawCommand}`);
679
+ const parsed = this.commandParser.parse(trimmed.startsWith("/") || trimmed.startsWith(":") ? trimmed : `/${trimmed}`);
171
680
  const result = await this.commandRegistry.execute(parsed);
172
681
  if (!result.success) {
173
- this.errorMessage = result.error ?? "Command failed.";
682
+ let errStr = result.error ?? "Command failed.";
683
+ if (errStr.includes('"code"') && errStr.includes('"message"') && this.errorHandler) {
684
+ errStr = this.errorHandler.handleError(parsed.name, new Error(errStr)).error ?? errStr;
685
+ }
686
+ this.errorMessage = errStr.replace(/\r?\n\s*/g, " ").trim();
687
+ this.commandOutput = null;
688
+ }
689
+ else {
690
+ this.errorMessage = "";
691
+ const nameLower = parsed.name.toLowerCase();
692
+ if (nameLower === "clear") {
693
+ this.commandOutput = null;
694
+ this.errorMessage = "";
695
+ this.requestRender();
696
+ return;
697
+ }
698
+ else if (nameLower === "usage") {
699
+ this.setView("usage");
700
+ this.commandOutput = null;
701
+ this.requestRender();
702
+ return;
703
+ }
704
+ if (result.message) {
705
+ this.commandOutput = {
706
+ title: `COMMAND RESULT: /${parsed.name.toUpperCase()}`,
707
+ lines: result.message.split("\n"),
708
+ };
709
+ }
710
+ else {
711
+ this.commandOutput = null;
712
+ }
174
713
  }
175
714
  if (result.exitRequested) {
176
715
  this.stop();
@@ -179,10 +718,11 @@ export class TuiController {
179
718
  catch (err) {
180
719
  if (this.errorHandler) {
181
720
  const handled = this.errorHandler.handleError("tui", err);
182
- this.errorMessage = handled.error ?? "Command error.";
721
+ this.errorMessage = (handled.error ?? "Command error.").replace(/\r?\n\s*/g, " ").trim();
183
722
  }
184
723
  else {
185
- this.errorMessage = err instanceof Error ? err.message : String(err);
724
+ const raw = err instanceof Error ? err.message : String(err);
725
+ this.errorMessage = raw.replace(/\r?\n\s*/g, " ").trim();
186
726
  }
187
727
  }
188
728
  }