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.
- package/dist/bin/anantham.js +12 -1
- package/dist/bin/anantham.js.map +1 -1
- package/dist/cli/command-parser.d.ts +5 -0
- package/dist/cli/command-parser.d.ts.map +1 -1
- package/dist/cli/command-parser.js +40 -9
- package/dist/cli/command-parser.js.map +1 -1
- package/dist/cli/command-registry.d.ts +8 -0
- package/dist/cli/command-registry.d.ts.map +1 -1
- package/dist/cli/command-registry.js +425 -4
- package/dist/cli/command-registry.js.map +1 -1
- package/dist/cli/error-handler.d.ts.map +1 -1
- package/dist/cli/error-handler.js +55 -1
- package/dist/cli/error-handler.js.map +1 -1
- package/dist/domain/tui.d.ts +1 -1
- package/dist/domain/tui.d.ts.map +1 -1
- package/dist/domain/tui.js +1 -0
- package/dist/domain/tui.js.map +1 -1
- package/dist/models/adapters/openai-compatible-adapter.d.ts.map +1 -1
- package/dist/models/adapters/openai-compatible-adapter.js +2 -1
- package/dist/models/adapters/openai-compatible-adapter.js.map +1 -1
- package/dist/models/provider-registry.d.ts +2 -2
- package/dist/persistence/index.d.ts +2 -0
- package/dist/persistence/index.d.ts.map +1 -1
- package/dist/persistence/index.js +2 -0
- package/dist/persistence/index.js.map +1 -1
- package/dist/persistence/token-metrics-manager.d.ts +88 -0
- package/dist/persistence/token-metrics-manager.d.ts.map +1 -0
- package/dist/persistence/token-metrics-manager.js +272 -0
- package/dist/persistence/token-metrics-manager.js.map +1 -0
- package/dist/persistence/user-config-manager.d.ts +40 -0
- package/dist/persistence/user-config-manager.d.ts.map +1 -0
- package/dist/persistence/user-config-manager.js +175 -0
- package/dist/persistence/user-config-manager.js.map +1 -0
- package/dist/tui/ansi-gradient.d.ts +43 -0
- package/dist/tui/ansi-gradient.d.ts.map +1 -0
- package/dist/tui/ansi-gradient.js +131 -0
- package/dist/tui/ansi-gradient.js.map +1 -0
- package/dist/tui/command-palette.d.ts +20 -0
- package/dist/tui/command-palette.d.ts.map +1 -0
- package/dist/tui/command-palette.js +241 -0
- package/dist/tui/command-palette.js.map +1 -0
- package/dist/tui/terminal-layout.d.ts +4 -0
- package/dist/tui/terminal-layout.d.ts.map +1 -1
- package/dist/tui/terminal-layout.js +42 -4
- package/dist/tui/terminal-layout.js.map +1 -1
- package/dist/tui/token-dashboard-renderer.d.ts +6 -0
- package/dist/tui/token-dashboard-renderer.d.ts.map +1 -0
- package/dist/tui/token-dashboard-renderer.js +115 -0
- package/dist/tui/token-dashboard-renderer.js.map +1 -0
- package/dist/tui/tui-application.d.ts +7 -0
- package/dist/tui/tui-application.d.ts.map +1 -1
- package/dist/tui/tui-application.js +110 -5
- package/dist/tui/tui-application.js.map +1 -1
- package/dist/tui/tui-controller.d.ts +78 -2
- package/dist/tui/tui-controller.d.ts.map +1 -1
- package/dist/tui/tui-controller.js +564 -24
- package/dist/tui/tui-controller.js.map +1 -1
- package/dist/tui/tui-renderer.d.ts +12 -1
- package/dist/tui/tui-renderer.d.ts.map +1 -1
- package/dist/tui/tui-renderer.js +104 -40
- package/dist/tui/tui-renderer.js.map +1 -1
- package/dist/tui/tui-sanitizer.d.ts +4 -0
- package/dist/tui/tui-sanitizer.d.ts.map +1 -1
- package/dist/tui/tui-sanitizer.js +6 -0
- package/dist/tui/tui-sanitizer.js.map +1 -1
- 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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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(
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
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
|
-
|
|
96
|
-
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
this.
|
|
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
|
-
|
|
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
|
-
|
|
622
|
+
case "\u000c": // Ctrl+L redraws screen
|
|
623
|
+
this.renderNow();
|
|
153
624
|
return true;
|
|
154
625
|
case "q":
|
|
155
|
-
case "\u001B":
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|