wtagent 0.2.1 → 0.3.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/README.md +2 -2
- package/package.json +2 -2
- package/src/browser/base-web-adapter.js +4 -34
- package/src/browser/chatgpt-web-adapter.js +4 -217
- package/src/browser/deepseek-web-adapter.js +0 -106
- package/src/browser/glm-web-adapter.js +2 -89
- package/src/browser/grok-web-adapter.js +145 -0
- package/src/browser/kimi-web-adapter.js +2 -74
- package/src/browser/provider-registry.js +8 -50
- package/src/cli/i18n.js +949 -0
- package/src/cli/main.js +125 -186
- package/src/cli/prompt-input.js +369 -155
- package/src/cli/render-events.js +45 -35
- package/src/cli/self-update.js +10 -3
- package/src/runtime/agent-runtime.js +14 -51
- package/src/shared/limits.js +2 -9
- package/src/browser/mode-selection.js +0 -214
- package/src/cli/mode-choice.js +0 -25
package/src/cli/prompt-input.js
CHANGED
|
@@ -11,51 +11,25 @@ const DISABLE_BRACKETED_PASTE = "\u001b[?2004l";
|
|
|
11
11
|
// ESC + CR some iTerm / VS Code / tmux setups
|
|
12
12
|
// Terminals that still send a bare CR for Shift+Enter cannot be disambiguated;
|
|
13
13
|
// Ctrl+J (bare LF in raw mode) is the reliable fallback and is also mapped.
|
|
14
|
-
const SHIFT_ENTER_KITTY = "\u001b[13;2u";
|
|
15
|
-
const SHIFT_ENTER_MODIFY_OTHER = "\u001b[27;2;13~";
|
|
16
14
|
const SHIFT_ENTER_ESC_CR = "\u001b\r";
|
|
17
|
-
const SHIFT_ENTER_SEQUENCES = Object.freeze([
|
|
18
|
-
SHIFT_ENTER_KITTY,
|
|
19
|
-
SHIFT_ENTER_MODIFY_OTHER,
|
|
20
|
-
SHIFT_ENTER_ESC_CR,
|
|
21
|
-
]);
|
|
22
15
|
const ENABLE_KITTY_KEYBOARD = "\u001b[>1u";
|
|
23
16
|
const DISABLE_KITTY_KEYBOARD = "\u001b[<1u";
|
|
24
17
|
const ENABLE_MODIFY_OTHER_KEYS = "\u001b[>4;2m";
|
|
25
18
|
const DISABLE_MODIFY_OTHER_KEYS = "\u001b[>4;0m";
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
const
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
// hold a split-across-chunks prefix back instead of inserting it as text.
|
|
41
|
-
const KNOWN_ESCAPE_SEQUENCES = Object.freeze([
|
|
42
|
-
BRACKETED_PASTE_START,
|
|
43
|
-
BRACKETED_PASTE_END,
|
|
44
|
-
KEY_UP,
|
|
45
|
-
KEY_DOWN,
|
|
46
|
-
KEY_RIGHT,
|
|
47
|
-
KEY_LEFT,
|
|
48
|
-
KEY_HOME,
|
|
49
|
-
KEY_END,
|
|
50
|
-
KEY_HOME_TILDE,
|
|
51
|
-
KEY_END_TILDE,
|
|
52
|
-
KEY_HOME_APP,
|
|
53
|
-
KEY_END_APP,
|
|
54
|
-
KEY_DELETE,
|
|
55
|
-
SHIFT_ENTER_KITTY,
|
|
56
|
-
SHIFT_ENTER_MODIFY_OTHER,
|
|
57
|
-
SHIFT_ENTER_ESC_CR,
|
|
58
|
-
]);
|
|
19
|
+
// DECAWM: if a redraw fills the last column, the terminal otherwise wraps onto a
|
|
20
|
+
// phantom next row (XENL). CJK is width 2 so it hits that edge constantly, and
|
|
21
|
+
// the next backspace then leaves the "deleted" glyph sitting on that extra row.
|
|
22
|
+
const DISABLE_AUTOWRAP = "\u001b[?7l";
|
|
23
|
+
const ENABLE_AUTOWRAP = "\u001b[?7h";
|
|
24
|
+
|
|
25
|
+
const SS3_ACTIONS = Object.freeze({
|
|
26
|
+
A: "up",
|
|
27
|
+
B: "down",
|
|
28
|
+
C: "right",
|
|
29
|
+
D: "left",
|
|
30
|
+
H: "home",
|
|
31
|
+
F: "end",
|
|
32
|
+
});
|
|
59
33
|
|
|
60
34
|
const CLEAN_EXIT_ERRORS = new Set([
|
|
61
35
|
"AbortPromptError",
|
|
@@ -69,22 +43,30 @@ const EXIT_COMMANDS = new Set([
|
|
|
69
43
|
"/quit",
|
|
70
44
|
]);
|
|
71
45
|
|
|
72
|
-
// Rough terminal cell width for one code point.
|
|
73
|
-
//
|
|
74
|
-
//
|
|
75
|
-
|
|
46
|
+
// Rough terminal cell width for one code point. CJK/fullwidth/emoji count 2,
|
|
47
|
+
// combining marks count 0, everything else 1 — including East-Asian Ambiguous
|
|
48
|
+
// (quotes, dashes, ›, box drawing), which wcwidth and every default terminal
|
|
49
|
+
// config render as 1 cell.
|
|
50
|
+
//
|
|
51
|
+
// The › in the prompt makes this a hard rule, not a taste call: the 7-cell
|
|
52
|
+
// prompt is ODD and every CJK glyph is 2 cells, so the cursor column after any
|
|
53
|
+
// amount of CJK is always odd = the LEADING cell of a glyph. macOS Terminal.app
|
|
54
|
+
// does not draw the cursor on a glyph's trailing cell, so counting › as 2
|
|
55
|
+
// (prompt 8, even) makes the cursor land on trailing cells and visibly blink
|
|
56
|
+
// in and out of existence while typing Chinese.
|
|
57
|
+
export function charWidth(codePoint) {
|
|
76
58
|
if (
|
|
77
59
|
codePoint >= 0x1100 && (
|
|
78
60
|
codePoint <= 0x115F
|
|
79
61
|
|| (codePoint >= 0x2E80 && codePoint <= 0xA4CF)
|
|
80
|
-
|| (codePoint >=
|
|
62
|
+
|| (codePoint >= 0xA960 && codePoint <= 0xA97F)
|
|
63
|
+
|| (codePoint >= 0xAC00 && codePoint <= 0xD7FB)
|
|
81
64
|
|| (codePoint >= 0xF900 && codePoint <= 0xFAFF)
|
|
82
65
|
|| (codePoint >= 0xFE10 && codePoint <= 0xFE19)
|
|
83
66
|
|| (codePoint >= 0xFE30 && codePoint <= 0xFE6F)
|
|
84
67
|
|| (codePoint >= 0xFF00 && codePoint <= 0xFF60)
|
|
85
68
|
|| (codePoint >= 0xFFE0 && codePoint <= 0xFFE6)
|
|
86
|
-
|| (codePoint >= 0x1F300 && codePoint <=
|
|
87
|
-
|| (codePoint >= 0x1F900 && codePoint <= 0x1F9FF)
|
|
69
|
+
|| (codePoint >= 0x1F300 && codePoint <= 0x1FAFF)
|
|
88
70
|
|| (codePoint >= 0x20000 && codePoint <= 0x3FFFD)
|
|
89
71
|
)
|
|
90
72
|
) {
|
|
@@ -103,7 +85,7 @@ function charWidth(codePoint) {
|
|
|
103
85
|
return 1;
|
|
104
86
|
}
|
|
105
87
|
|
|
106
|
-
function displayWidth(text) {
|
|
88
|
+
export function displayWidth(text) {
|
|
107
89
|
let width = 0;
|
|
108
90
|
for (const ch of String(text ?? "")) {
|
|
109
91
|
width += charWidth(ch.codePointAt(0));
|
|
@@ -111,6 +93,174 @@ function displayWidth(text) {
|
|
|
111
93
|
return width;
|
|
112
94
|
}
|
|
113
95
|
|
|
96
|
+
function splitGraphemes(text) {
|
|
97
|
+
const value = String(text ?? "");
|
|
98
|
+
if (typeof Intl !== "undefined" && Intl.Segmenter) {
|
|
99
|
+
return [...new Intl.Segmenter(undefined, { granularity: "grapheme" })
|
|
100
|
+
.segment(value)]
|
|
101
|
+
.map((part) => part.segment);
|
|
102
|
+
}
|
|
103
|
+
return [...value];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function graphemeWidth(grapheme) {
|
|
107
|
+
let width = 0;
|
|
108
|
+
for (const ch of String(grapheme ?? "")) {
|
|
109
|
+
width += charWidth(ch.codePointAt(0));
|
|
110
|
+
}
|
|
111
|
+
return width;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Parse one escape at the start of `text`. Incomplete sequences (split across
|
|
115
|
+
// TTY chunks) are held back; complete unknown CSI is consumed, never inserted.
|
|
116
|
+
function matchEscape(text) {
|
|
117
|
+
if (!text.startsWith("\u001b")) {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
if (text.length === 1) {
|
|
121
|
+
return { incomplete: true };
|
|
122
|
+
}
|
|
123
|
+
if (text[1] === "\r") {
|
|
124
|
+
return { sequence: SHIFT_ENTER_ESC_CR, kind: "shift-enter" };
|
|
125
|
+
}
|
|
126
|
+
if (text[1] === "O") {
|
|
127
|
+
if (text.length < 3) {
|
|
128
|
+
return { incomplete: true };
|
|
129
|
+
}
|
|
130
|
+
return { sequence: text.slice(0, 3), kind: "ss3", final: text[2] };
|
|
131
|
+
}
|
|
132
|
+
if (text[1] === "[") {
|
|
133
|
+
let index = 2;
|
|
134
|
+
while (
|
|
135
|
+
index < text.length
|
|
136
|
+
&& text.charCodeAt(index) >= 0x30
|
|
137
|
+
&& text.charCodeAt(index) <= 0x3F
|
|
138
|
+
) {
|
|
139
|
+
index += 1;
|
|
140
|
+
}
|
|
141
|
+
while (
|
|
142
|
+
index < text.length
|
|
143
|
+
&& text.charCodeAt(index) >= 0x20
|
|
144
|
+
&& text.charCodeAt(index) <= 0x2F
|
|
145
|
+
) {
|
|
146
|
+
index += 1;
|
|
147
|
+
}
|
|
148
|
+
if (index >= text.length) {
|
|
149
|
+
return { incomplete: true };
|
|
150
|
+
}
|
|
151
|
+
const finalCode = text.charCodeAt(index);
|
|
152
|
+
if (finalCode < 0x40 || finalCode > 0x7E) {
|
|
153
|
+
return { sequence: "\u001b", kind: "unknown" };
|
|
154
|
+
}
|
|
155
|
+
return {
|
|
156
|
+
sequence: text.slice(0, index + 1),
|
|
157
|
+
kind: "csi",
|
|
158
|
+
final: text[index],
|
|
159
|
+
params: text.slice(2, index),
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
return { sequence: text.slice(0, 2), kind: "unknown" };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function parseCsiParams(params) {
|
|
166
|
+
const body = String(params ?? "").replace(/^[?<>]/, "");
|
|
167
|
+
if (!body) {
|
|
168
|
+
return [];
|
|
169
|
+
}
|
|
170
|
+
return body.split(";").map((part) => {
|
|
171
|
+
const base = part.split(":")[0];
|
|
172
|
+
const value = Number(base);
|
|
173
|
+
return Number.isFinite(value) && base.length > 0 ? value : 0;
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function classifyUnicodeKey(key, mods) {
|
|
178
|
+
const bits = Math.max(0, (mods || 1) - 1);
|
|
179
|
+
const shift = (bits & 1) !== 0;
|
|
180
|
+
const ctrl = (bits & 4) !== 0;
|
|
181
|
+
if (key === 13) {
|
|
182
|
+
return shift ? { type: "newline" } : { type: "submit" };
|
|
183
|
+
}
|
|
184
|
+
if (key === 10) {
|
|
185
|
+
return { type: "newline" };
|
|
186
|
+
}
|
|
187
|
+
if (key === 127 || key === 8) {
|
|
188
|
+
return { type: "backspace" };
|
|
189
|
+
}
|
|
190
|
+
if (key === 27 || key === 9) {
|
|
191
|
+
return { type: "ignore" };
|
|
192
|
+
}
|
|
193
|
+
if (ctrl) {
|
|
194
|
+
if (key === 99 || key === 67) {
|
|
195
|
+
return { type: "cancel" };
|
|
196
|
+
}
|
|
197
|
+
if (key === 100 || key === 68) {
|
|
198
|
+
return { type: "eof-or-delete" };
|
|
199
|
+
}
|
|
200
|
+
if (key === 97 || key === 65) {
|
|
201
|
+
return { type: "home" };
|
|
202
|
+
}
|
|
203
|
+
if (key === 101 || key === 69) {
|
|
204
|
+
return { type: "end" };
|
|
205
|
+
}
|
|
206
|
+
if (key === 107 || key === 75) {
|
|
207
|
+
return { type: "kill-line" };
|
|
208
|
+
}
|
|
209
|
+
if (key === 117 || key === 85) {
|
|
210
|
+
return { type: "kill-all" };
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return { type: "ignore" };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function classifyCsi(final, params) {
|
|
217
|
+
const nums = parseCsiParams(params);
|
|
218
|
+
if (final === "u") {
|
|
219
|
+
return classifyUnicodeKey(nums[0] || 0, nums[1] || 1);
|
|
220
|
+
}
|
|
221
|
+
if (final === "~") {
|
|
222
|
+
const first = nums[0] || 0;
|
|
223
|
+
if (first === 200) {
|
|
224
|
+
return { type: "paste-start" };
|
|
225
|
+
}
|
|
226
|
+
if (first === 201) {
|
|
227
|
+
return { type: "paste-end" };
|
|
228
|
+
}
|
|
229
|
+
if (first === 3) {
|
|
230
|
+
return { type: "delete" };
|
|
231
|
+
}
|
|
232
|
+
if (first === 1) {
|
|
233
|
+
return { type: "home" };
|
|
234
|
+
}
|
|
235
|
+
if (first === 4) {
|
|
236
|
+
return { type: "end" };
|
|
237
|
+
}
|
|
238
|
+
if (first === 27) {
|
|
239
|
+
return classifyUnicodeKey(nums[2] || 0, nums[1] || 1);
|
|
240
|
+
}
|
|
241
|
+
return { type: "ignore" };
|
|
242
|
+
}
|
|
243
|
+
if (final === "A") {
|
|
244
|
+
return { type: "up" };
|
|
245
|
+
}
|
|
246
|
+
if (final === "B") {
|
|
247
|
+
return { type: "down" };
|
|
248
|
+
}
|
|
249
|
+
if (final === "C") {
|
|
250
|
+
return { type: "right" };
|
|
251
|
+
}
|
|
252
|
+
if (final === "D") {
|
|
253
|
+
return { type: "left" };
|
|
254
|
+
}
|
|
255
|
+
if (final === "H") {
|
|
256
|
+
return { type: "home" };
|
|
257
|
+
}
|
|
258
|
+
if (final === "F") {
|
|
259
|
+
return { type: "end" };
|
|
260
|
+
}
|
|
261
|
+
return { type: "ignore" };
|
|
262
|
+
}
|
|
263
|
+
|
|
114
264
|
// Longest tail of `text` that is a strict prefix of `sequence` — the part that
|
|
115
265
|
// must be held back because the sequence may continue in the next chunk.
|
|
116
266
|
function longestPrefixAtEnd(text, sequence) {
|
|
@@ -198,9 +348,19 @@ export class ShellChatInput {
|
|
|
198
348
|
const outputStream = this.outputStream;
|
|
199
349
|
const previousRaw = inputStream.isRaw;
|
|
200
350
|
const decoder = new StringDecoder("utf8");
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
351
|
+
// Terminal width lives on the TTY *output* stream; process.stdin.columns is
|
|
352
|
+
// undefined, so reading it always fell back to 80 — wrapping CJK input after
|
|
353
|
+
// ~40 chars and, worse, making the editor's multi-line row model disagree
|
|
354
|
+
// with the terminal's real wrapping (arrow-key redraws then duplicated the
|
|
355
|
+
// input up the screen). Prefer the output stream's live column count.
|
|
356
|
+
const columns = () => Math.max(
|
|
357
|
+
10,
|
|
358
|
+
Number(outputStream.columns)
|
|
359
|
+
|| Number(inputStream.columns)
|
|
360
|
+
|| 80,
|
|
361
|
+
);
|
|
362
|
+
|
|
363
|
+
let buffer = []; // grapheme clusters; "\n" entries are real line breaks
|
|
204
364
|
let cursor = 0;
|
|
205
365
|
let historyIndex = this.history.length;
|
|
206
366
|
let draft = "";
|
|
@@ -215,6 +375,7 @@ export class ShellChatInput {
|
|
|
215
375
|
inputStream.removeListener("end", onEnd);
|
|
216
376
|
inputStream.removeListener("close", onEnd);
|
|
217
377
|
inputStream.setRawMode?.(previousRaw);
|
|
378
|
+
outputStream.write(ENABLE_AUTOWRAP);
|
|
218
379
|
outputStream.write(DISABLE_MODIFY_OTHER_KEYS);
|
|
219
380
|
outputStream.write(DISABLE_BRACKETED_PASTE);
|
|
220
381
|
outputStream.write(DISABLE_KITTY_KEYBOARD);
|
|
@@ -234,7 +395,10 @@ export class ShellChatInput {
|
|
|
234
395
|
};
|
|
235
396
|
|
|
236
397
|
// Moves to the end of the drawn input and starts a fresh line.
|
|
398
|
+
// CR first: CUU/CUD from the trailing cell of a CJK glyph splits it in
|
|
399
|
+
// half on macOS Terminal.app. Column 0 is always a safe cell boundary.
|
|
237
400
|
const finishLine = () => {
|
|
401
|
+
outputStream.write("\r");
|
|
238
402
|
if (cursorRow < linesDrawn - 1) {
|
|
239
403
|
outputStream.write(`\x1b[${linesDrawn - 1 - cursorRow}B`);
|
|
240
404
|
}
|
|
@@ -256,19 +420,39 @@ export class ShellChatInput {
|
|
|
256
420
|
};
|
|
257
421
|
|
|
258
422
|
const setBufferText = (text) => {
|
|
259
|
-
buffer =
|
|
423
|
+
buffer = splitGraphemes(text);
|
|
260
424
|
cursor = buffer.length;
|
|
261
425
|
render();
|
|
262
426
|
};
|
|
263
427
|
|
|
264
428
|
const insertText = (text) => {
|
|
265
|
-
const chars =
|
|
429
|
+
const chars = splitGraphemes(text).filter((ch) => ch !== "\u001b");
|
|
430
|
+
if (chars.length === 0) {
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
266
433
|
buffer.splice(cursor, 0, ...chars);
|
|
267
434
|
cursor += chars.length;
|
|
268
435
|
historyIndex = this.history.length;
|
|
269
436
|
render();
|
|
270
437
|
};
|
|
271
438
|
|
|
439
|
+
const applyBackspace = () => {
|
|
440
|
+
if (cursor > 0) {
|
|
441
|
+
buffer.splice(cursor - 1, 1);
|
|
442
|
+
cursor -= 1;
|
|
443
|
+
historyIndex = this.history.length;
|
|
444
|
+
render();
|
|
445
|
+
}
|
|
446
|
+
};
|
|
447
|
+
|
|
448
|
+
const applyDelete = () => {
|
|
449
|
+
if (cursor < buffer.length) {
|
|
450
|
+
buffer.splice(cursor, 1);
|
|
451
|
+
historyIndex = this.history.length;
|
|
452
|
+
render();
|
|
453
|
+
}
|
|
454
|
+
};
|
|
455
|
+
|
|
272
456
|
// historyIndex is the DISPLAYED entry's index, or history.length while the
|
|
273
457
|
// user edits the fresh draft. ↑ walks into the list newest-first; ↓ walks
|
|
274
458
|
// back out and restores the draft.
|
|
@@ -298,6 +482,74 @@ export class ShellChatInput {
|
|
|
298
482
|
}
|
|
299
483
|
};
|
|
300
484
|
|
|
485
|
+
const handleAction = (action) => {
|
|
486
|
+
switch (action?.type) {
|
|
487
|
+
case "paste-start":
|
|
488
|
+
inPaste = true;
|
|
489
|
+
return;
|
|
490
|
+
case "paste-end":
|
|
491
|
+
inPaste = false;
|
|
492
|
+
return;
|
|
493
|
+
case "up":
|
|
494
|
+
historyPrev();
|
|
495
|
+
return;
|
|
496
|
+
case "down":
|
|
497
|
+
historyNext();
|
|
498
|
+
return;
|
|
499
|
+
case "right":
|
|
500
|
+
cursor = Math.min(cursor + 1, buffer.length);
|
|
501
|
+
render();
|
|
502
|
+
return;
|
|
503
|
+
case "left":
|
|
504
|
+
cursor = Math.max(cursor - 1, 0);
|
|
505
|
+
render();
|
|
506
|
+
return;
|
|
507
|
+
case "home":
|
|
508
|
+
cursor = 0;
|
|
509
|
+
render();
|
|
510
|
+
return;
|
|
511
|
+
case "end":
|
|
512
|
+
cursor = buffer.length;
|
|
513
|
+
render();
|
|
514
|
+
return;
|
|
515
|
+
case "backspace":
|
|
516
|
+
applyBackspace();
|
|
517
|
+
return;
|
|
518
|
+
case "delete":
|
|
519
|
+
applyDelete();
|
|
520
|
+
return;
|
|
521
|
+
case "submit":
|
|
522
|
+
submit();
|
|
523
|
+
return;
|
|
524
|
+
case "newline":
|
|
525
|
+
insertText("\n");
|
|
526
|
+
return;
|
|
527
|
+
case "cancel":
|
|
528
|
+
cancel();
|
|
529
|
+
return;
|
|
530
|
+
case "eof-or-delete":
|
|
531
|
+
if (buffer.length === 0) {
|
|
532
|
+
done(null);
|
|
533
|
+
} else {
|
|
534
|
+
applyDelete();
|
|
535
|
+
}
|
|
536
|
+
return;
|
|
537
|
+
case "kill-line":
|
|
538
|
+
buffer = buffer.slice(0, cursor);
|
|
539
|
+
historyIndex = this.history.length;
|
|
540
|
+
render();
|
|
541
|
+
return;
|
|
542
|
+
case "kill-all":
|
|
543
|
+
buffer = [];
|
|
544
|
+
cursor = 0;
|
|
545
|
+
historyIndex = this.history.length;
|
|
546
|
+
render();
|
|
547
|
+
return;
|
|
548
|
+
default:
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
};
|
|
552
|
+
|
|
301
553
|
const handlePlainText = (text) => {
|
|
302
554
|
for (const ch of String(text)) {
|
|
303
555
|
if (ch === "\r") {
|
|
@@ -308,46 +560,39 @@ export class ShellChatInput {
|
|
|
308
560
|
insertText("\n");
|
|
309
561
|
continue;
|
|
310
562
|
}
|
|
311
|
-
if (ch === "
|
|
563
|
+
if (ch === "\u0003") {
|
|
312
564
|
cancel();
|
|
313
565
|
return;
|
|
314
566
|
}
|
|
315
|
-
if (ch === "
|
|
567
|
+
if (ch === "\u0004") {
|
|
316
568
|
if (buffer.length === 0) {
|
|
317
569
|
done(null);
|
|
318
|
-
} else
|
|
319
|
-
|
|
320
|
-
historyIndex = this.history.length;
|
|
321
|
-
render();
|
|
570
|
+
} else {
|
|
571
|
+
applyDelete();
|
|
322
572
|
}
|
|
323
573
|
continue;
|
|
324
574
|
}
|
|
325
|
-
if (ch === "
|
|
326
|
-
|
|
327
|
-
buffer.splice(cursor - 1, 1);
|
|
328
|
-
cursor -= 1;
|
|
329
|
-
historyIndex = this.history.length;
|
|
330
|
-
render();
|
|
331
|
-
}
|
|
575
|
+
if (ch === "\u007f" || ch === "\u0008") {
|
|
576
|
+
applyBackspace();
|
|
332
577
|
continue;
|
|
333
578
|
}
|
|
334
|
-
if (ch === "
|
|
579
|
+
if (ch === "\u0001") {
|
|
335
580
|
cursor = 0;
|
|
336
581
|
render();
|
|
337
582
|
continue;
|
|
338
583
|
}
|
|
339
|
-
if (ch === "
|
|
584
|
+
if (ch === "\u0005") {
|
|
340
585
|
cursor = buffer.length;
|
|
341
586
|
render();
|
|
342
587
|
continue;
|
|
343
588
|
}
|
|
344
|
-
if (ch === "
|
|
589
|
+
if (ch === "\u000b") {
|
|
345
590
|
buffer = buffer.slice(0, cursor);
|
|
346
591
|
historyIndex = this.history.length;
|
|
347
592
|
render();
|
|
348
593
|
continue;
|
|
349
594
|
}
|
|
350
|
-
if (ch === "
|
|
595
|
+
if (ch === "\u0015") {
|
|
351
596
|
buffer = [];
|
|
352
597
|
cursor = 0;
|
|
353
598
|
historyIndex = this.history.length;
|
|
@@ -358,55 +603,6 @@ export class ShellChatInput {
|
|
|
358
603
|
}
|
|
359
604
|
};
|
|
360
605
|
|
|
361
|
-
const handleSequence = (sequence) => {
|
|
362
|
-
switch (sequence) {
|
|
363
|
-
case BRACKETED_PASTE_START:
|
|
364
|
-
inPaste = true;
|
|
365
|
-
return;
|
|
366
|
-
case BRACKETED_PASTE_END:
|
|
367
|
-
inPaste = false;
|
|
368
|
-
return;
|
|
369
|
-
case KEY_UP:
|
|
370
|
-
historyPrev();
|
|
371
|
-
return;
|
|
372
|
-
case KEY_DOWN:
|
|
373
|
-
historyNext();
|
|
374
|
-
return;
|
|
375
|
-
case KEY_RIGHT:
|
|
376
|
-
cursor = Math.min(cursor + 1, buffer.length);
|
|
377
|
-
render();
|
|
378
|
-
return;
|
|
379
|
-
case KEY_LEFT:
|
|
380
|
-
cursor = Math.max(cursor - 1, 0);
|
|
381
|
-
render();
|
|
382
|
-
return;
|
|
383
|
-
case KEY_HOME:
|
|
384
|
-
case KEY_HOME_TILDE:
|
|
385
|
-
case KEY_HOME_APP:
|
|
386
|
-
cursor = 0;
|
|
387
|
-
render();
|
|
388
|
-
return;
|
|
389
|
-
case KEY_END:
|
|
390
|
-
case KEY_END_TILDE:
|
|
391
|
-
case KEY_END_APP:
|
|
392
|
-
cursor = buffer.length;
|
|
393
|
-
render();
|
|
394
|
-
return;
|
|
395
|
-
case KEY_DELETE:
|
|
396
|
-
if (cursor < buffer.length) {
|
|
397
|
-
buffer.splice(cursor, 1);
|
|
398
|
-
historyIndex = this.history.length;
|
|
399
|
-
render();
|
|
400
|
-
}
|
|
401
|
-
return;
|
|
402
|
-
default:
|
|
403
|
-
break;
|
|
404
|
-
}
|
|
405
|
-
if (SHIFT_ENTER_SEQUENCES.includes(sequence)) {
|
|
406
|
-
insertText("\n");
|
|
407
|
-
}
|
|
408
|
-
};
|
|
409
|
-
|
|
410
606
|
const consume = (text) => {
|
|
411
607
|
pending += text;
|
|
412
608
|
for (;;) {
|
|
@@ -443,26 +639,26 @@ export class ShellChatInput {
|
|
|
443
639
|
continue;
|
|
444
640
|
}
|
|
445
641
|
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
pending
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
handleSequence(sequence);
|
|
452
|
-
pending = pending.slice(sequence.length);
|
|
453
|
-
continue;
|
|
642
|
+
const matched = matchEscape(pending);
|
|
643
|
+
if (!matched) {
|
|
644
|
+
handlePlainText(pending);
|
|
645
|
+
pending = "";
|
|
646
|
+
return;
|
|
454
647
|
}
|
|
455
|
-
if (
|
|
456
|
-
pending.length === 1
|
|
457
|
-
|| KNOWN_ESCAPE_SEQUENCES.some((candidate) => (
|
|
458
|
-
candidate.startsWith(pending)
|
|
459
|
-
))
|
|
460
|
-
) {
|
|
461
|
-
// The rest of the sequence may arrive in the next chunk.
|
|
648
|
+
if (matched.incomplete) {
|
|
462
649
|
return;
|
|
463
650
|
}
|
|
464
|
-
|
|
465
|
-
|
|
651
|
+
if (matched.kind === "shift-enter") {
|
|
652
|
+
handleAction({ type: "newline" });
|
|
653
|
+
} else if (matched.kind === "ss3") {
|
|
654
|
+
const type = SS3_ACTIONS[matched.final];
|
|
655
|
+
if (type) {
|
|
656
|
+
handleAction({ type });
|
|
657
|
+
}
|
|
658
|
+
} else if (matched.kind === "csi") {
|
|
659
|
+
handleAction(classifyCsi(matched.final, matched.params));
|
|
660
|
+
}
|
|
661
|
+
pending = pending.slice(matched.sequence.length);
|
|
466
662
|
}
|
|
467
663
|
};
|
|
468
664
|
|
|
@@ -471,7 +667,6 @@ export class ShellChatInput {
|
|
|
471
667
|
const render = () => {
|
|
472
668
|
const width = columns();
|
|
473
669
|
const promptWidth = displayWidth(prompt);
|
|
474
|
-
const text = buffer.join("");
|
|
475
670
|
|
|
476
671
|
const rendered = [];
|
|
477
672
|
const ranges = []; // { start, end } flat char indices per rendered line
|
|
@@ -486,15 +681,27 @@ export class ShellChatInput {
|
|
|
486
681
|
currentWidth = 0;
|
|
487
682
|
rangeStart = charIndex;
|
|
488
683
|
};
|
|
489
|
-
for (
|
|
684
|
+
for (let index = 0; index < buffer.length; index += 1) {
|
|
685
|
+
const ch = buffer[index];
|
|
490
686
|
if (ch === "\n") {
|
|
491
687
|
flush();
|
|
492
688
|
charIndex += 1;
|
|
689
|
+
// The "\n" itself belongs to no rendered line. flush() left
|
|
690
|
+
// rangeStart on the "\n"'s index, so the next line's range would
|
|
691
|
+
// include it — and the cursor column (displayWidth of the row
|
|
692
|
+
// prefix) came out 1 too far right on every row after a newline,
|
|
693
|
+
// parking the cursor on a CJK glyph's trailing cell where macOS
|
|
694
|
+
// Terminal.app hides it (the "cursor blinks out" bug).
|
|
695
|
+
rangeStart = charIndex;
|
|
493
696
|
continue;
|
|
494
697
|
}
|
|
495
|
-
const w =
|
|
698
|
+
const w = graphemeWidth(ch);
|
|
496
699
|
const base = rendered.length === 0 ? promptWidth : 0;
|
|
497
|
-
|
|
700
|
+
// `>= width` (not `>`): filling the last column trips terminal autowrap
|
|
701
|
+
// even with DECAWM off on some emulators, and CJK (width 2) lands on
|
|
702
|
+
// that boundary far more often than ASCII. Keep one spare cell so the
|
|
703
|
+
// editor's row model and the physical cursor stay aligned.
|
|
704
|
+
if (base + currentWidth + w >= width && currentWidth > 0) {
|
|
498
705
|
flush();
|
|
499
706
|
}
|
|
500
707
|
current += ch;
|
|
@@ -507,10 +714,18 @@ export class ShellChatInput {
|
|
|
507
714
|
ranges.push({ start: 0, end: 0 });
|
|
508
715
|
}
|
|
509
716
|
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
717
|
+
// The previous render left the physical cursor on row `cursorRow`, which
|
|
718
|
+
// is NOT always the last drawn row: arrowing left/up into an earlier
|
|
719
|
+
// wrapped row leaves it higher. Move up by that actual row to reach the
|
|
720
|
+
// top — using `linesDrawn - 1` here overshoots whenever the cursor sits
|
|
721
|
+
// above the bottom row, drifting the whole block up the screen on every
|
|
722
|
+
// keystroke (duplicating wrapped lines). Mirror of finishLine()'s move.
|
|
723
|
+
// CR first: CUU from the trailing cell of a CJK glyph splits it in half
|
|
724
|
+
// on macOS Terminal.app. Column 0 is always a safe cell boundary.
|
|
513
725
|
outputStream.write("\r");
|
|
726
|
+
if (cursorRow > 0) {
|
|
727
|
+
outputStream.write(`\x1b[${cursorRow}A`);
|
|
728
|
+
}
|
|
514
729
|
for (let index = 0; index < rendered.length; index += 1) {
|
|
515
730
|
outputStream.write("\x1b[2K");
|
|
516
731
|
if (index === 0) {
|
|
@@ -521,14 +736,11 @@ export class ShellChatInput {
|
|
|
521
736
|
outputStream.write("\r\n");
|
|
522
737
|
}
|
|
523
738
|
}
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
}
|
|
530
|
-
}
|
|
531
|
-
}
|
|
739
|
+
// Erase leftover rows from a previous taller wrap (CJK backspace/unwrap).
|
|
740
|
+
// ED 0 from the end of the last content line. The old shrink path CSI-2K'd
|
|
741
|
+
// the current line instead — wiping the row we just drew and never moving
|
|
742
|
+
// onto the stale row below, so the "deleted" glyph stayed visible.
|
|
743
|
+
outputStream.write("\x1b[J");
|
|
532
744
|
linesDrawn = rendered.length;
|
|
533
745
|
|
|
534
746
|
// Position the cursor at the flat cursor index.
|
|
@@ -539,20 +751,21 @@ export class ShellChatInput {
|
|
|
539
751
|
if (cursor < range.end || r === rendered.length - 1) {
|
|
540
752
|
row = r;
|
|
541
753
|
const base = r === 0 ? promptWidth : 0;
|
|
542
|
-
const prefixLength = Math.max(0, cursor - range.start);
|
|
543
754
|
col = base + displayWidth(
|
|
544
|
-
|
|
755
|
+
buffer.slice(range.start, cursor).join(""),
|
|
545
756
|
);
|
|
546
757
|
break;
|
|
547
758
|
}
|
|
548
759
|
}
|
|
549
760
|
cursorRow = row;
|
|
550
|
-
// Drawing ends at the last rendered line;
|
|
761
|
+
// Drawing ends at the last rendered line; CR first, then move UP. After
|
|
762
|
+
// the CR the cursor is at column 0, so CUU keeps column 0 — no second CR
|
|
763
|
+
// is needed before the CUF.
|
|
551
764
|
const moveUp = rendered.length - 1 - row;
|
|
765
|
+
outputStream.write("\r");
|
|
552
766
|
if (moveUp > 0) {
|
|
553
767
|
outputStream.write(`\x1b[${moveUp}A`);
|
|
554
768
|
}
|
|
555
|
-
outputStream.write("\r");
|
|
556
769
|
if (col > 0) {
|
|
557
770
|
outputStream.write(`\x1b[${col}C`);
|
|
558
771
|
}
|
|
@@ -570,6 +783,7 @@ export class ShellChatInput {
|
|
|
570
783
|
};
|
|
571
784
|
|
|
572
785
|
inputStream.setRawMode?.(true);
|
|
786
|
+
outputStream.write(DISABLE_AUTOWRAP);
|
|
573
787
|
outputStream.write(ENABLE_BRACKETED_PASTE);
|
|
574
788
|
// Ask the terminal to disambiguate modified Enter. Kitty CSI-u covers
|
|
575
789
|
// kitty/iTerm; xterm modifyOtherKeys covers many others, including some
|