wtagent 0.2.2 → 0.4.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 +4 -4
- package/docs/technical-design.md +18 -11
- package/package.json +6 -6
- package/src/browser/base-web-adapter.js +2085 -202
- package/src/browser/cdp-browser.js +223 -25
- package/src/browser/cdp-state.js +8 -5
- package/src/browser/chatgpt-web-adapter.js +803 -218
- package/src/browser/claude-web-adapter.js +13 -0
- package/src/browser/deepseek-web-adapter.js +0 -106
- package/src/browser/fake-web-model-adapter.js +137 -3
- package/src/browser/gemini-web-adapter.js +10 -0
- package/src/browser/glm-web-adapter.js +4 -91
- 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 +217 -223
- package/src/cli/prompt-input.js +349 -151
- package/src/cli/render-events.js +45 -35
- package/src/cli/self-update.js +10 -3
- package/src/platform/command-launcher.js +1 -1
- package/src/platform/paths.js +19 -0
- package/src/platform/windows-diagnostics.js +9 -16
- package/src/policy/path-guard.js +5 -1
- package/src/runtime/agent-runtime.js +1065 -237
- package/src/session/agent-session.js +975 -120
- package/src/shared/limits.js +2 -9
- package/src/tools/default-tools.js +1 -1
- 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) {
|
|
@@ -210,7 +360,7 @@ export class ShellChatInput {
|
|
|
210
360
|
|| 80,
|
|
211
361
|
);
|
|
212
362
|
|
|
213
|
-
let buffer = []; //
|
|
363
|
+
let buffer = []; // grapheme clusters; "\n" entries are real line breaks
|
|
214
364
|
let cursor = 0;
|
|
215
365
|
let historyIndex = this.history.length;
|
|
216
366
|
let draft = "";
|
|
@@ -225,6 +375,7 @@ export class ShellChatInput {
|
|
|
225
375
|
inputStream.removeListener("end", onEnd);
|
|
226
376
|
inputStream.removeListener("close", onEnd);
|
|
227
377
|
inputStream.setRawMode?.(previousRaw);
|
|
378
|
+
outputStream.write(ENABLE_AUTOWRAP);
|
|
228
379
|
outputStream.write(DISABLE_MODIFY_OTHER_KEYS);
|
|
229
380
|
outputStream.write(DISABLE_BRACKETED_PASTE);
|
|
230
381
|
outputStream.write(DISABLE_KITTY_KEYBOARD);
|
|
@@ -244,7 +395,10 @@ export class ShellChatInput {
|
|
|
244
395
|
};
|
|
245
396
|
|
|
246
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.
|
|
247
400
|
const finishLine = () => {
|
|
401
|
+
outputStream.write("\r");
|
|
248
402
|
if (cursorRow < linesDrawn - 1) {
|
|
249
403
|
outputStream.write(`\x1b[${linesDrawn - 1 - cursorRow}B`);
|
|
250
404
|
}
|
|
@@ -266,19 +420,39 @@ export class ShellChatInput {
|
|
|
266
420
|
};
|
|
267
421
|
|
|
268
422
|
const setBufferText = (text) => {
|
|
269
|
-
buffer =
|
|
423
|
+
buffer = splitGraphemes(text);
|
|
270
424
|
cursor = buffer.length;
|
|
271
425
|
render();
|
|
272
426
|
};
|
|
273
427
|
|
|
274
428
|
const insertText = (text) => {
|
|
275
|
-
const chars =
|
|
429
|
+
const chars = splitGraphemes(text).filter((ch) => ch !== "\u001b");
|
|
430
|
+
if (chars.length === 0) {
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
276
433
|
buffer.splice(cursor, 0, ...chars);
|
|
277
434
|
cursor += chars.length;
|
|
278
435
|
historyIndex = this.history.length;
|
|
279
436
|
render();
|
|
280
437
|
};
|
|
281
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
|
+
|
|
282
456
|
// historyIndex is the DISPLAYED entry's index, or history.length while the
|
|
283
457
|
// user edits the fresh draft. ↑ walks into the list newest-first; ↓ walks
|
|
284
458
|
// back out and restores the draft.
|
|
@@ -308,6 +482,74 @@ export class ShellChatInput {
|
|
|
308
482
|
}
|
|
309
483
|
};
|
|
310
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
|
+
|
|
311
553
|
const handlePlainText = (text) => {
|
|
312
554
|
for (const ch of String(text)) {
|
|
313
555
|
if (ch === "\r") {
|
|
@@ -318,46 +560,39 @@ export class ShellChatInput {
|
|
|
318
560
|
insertText("\n");
|
|
319
561
|
continue;
|
|
320
562
|
}
|
|
321
|
-
if (ch === "
|
|
563
|
+
if (ch === "\u0003") {
|
|
322
564
|
cancel();
|
|
323
565
|
return;
|
|
324
566
|
}
|
|
325
|
-
if (ch === "
|
|
567
|
+
if (ch === "\u0004") {
|
|
326
568
|
if (buffer.length === 0) {
|
|
327
569
|
done(null);
|
|
328
|
-
} else
|
|
329
|
-
|
|
330
|
-
historyIndex = this.history.length;
|
|
331
|
-
render();
|
|
570
|
+
} else {
|
|
571
|
+
applyDelete();
|
|
332
572
|
}
|
|
333
573
|
continue;
|
|
334
574
|
}
|
|
335
|
-
if (ch === "
|
|
336
|
-
|
|
337
|
-
buffer.splice(cursor - 1, 1);
|
|
338
|
-
cursor -= 1;
|
|
339
|
-
historyIndex = this.history.length;
|
|
340
|
-
render();
|
|
341
|
-
}
|
|
575
|
+
if (ch === "\u007f" || ch === "\u0008") {
|
|
576
|
+
applyBackspace();
|
|
342
577
|
continue;
|
|
343
578
|
}
|
|
344
|
-
if (ch === "
|
|
579
|
+
if (ch === "\u0001") {
|
|
345
580
|
cursor = 0;
|
|
346
581
|
render();
|
|
347
582
|
continue;
|
|
348
583
|
}
|
|
349
|
-
if (ch === "
|
|
584
|
+
if (ch === "\u0005") {
|
|
350
585
|
cursor = buffer.length;
|
|
351
586
|
render();
|
|
352
587
|
continue;
|
|
353
588
|
}
|
|
354
|
-
if (ch === "
|
|
589
|
+
if (ch === "\u000b") {
|
|
355
590
|
buffer = buffer.slice(0, cursor);
|
|
356
591
|
historyIndex = this.history.length;
|
|
357
592
|
render();
|
|
358
593
|
continue;
|
|
359
594
|
}
|
|
360
|
-
if (ch === "
|
|
595
|
+
if (ch === "\u0015") {
|
|
361
596
|
buffer = [];
|
|
362
597
|
cursor = 0;
|
|
363
598
|
historyIndex = this.history.length;
|
|
@@ -368,55 +603,6 @@ export class ShellChatInput {
|
|
|
368
603
|
}
|
|
369
604
|
};
|
|
370
605
|
|
|
371
|
-
const handleSequence = (sequence) => {
|
|
372
|
-
switch (sequence) {
|
|
373
|
-
case BRACKETED_PASTE_START:
|
|
374
|
-
inPaste = true;
|
|
375
|
-
return;
|
|
376
|
-
case BRACKETED_PASTE_END:
|
|
377
|
-
inPaste = false;
|
|
378
|
-
return;
|
|
379
|
-
case KEY_UP:
|
|
380
|
-
historyPrev();
|
|
381
|
-
return;
|
|
382
|
-
case KEY_DOWN:
|
|
383
|
-
historyNext();
|
|
384
|
-
return;
|
|
385
|
-
case KEY_RIGHT:
|
|
386
|
-
cursor = Math.min(cursor + 1, buffer.length);
|
|
387
|
-
render();
|
|
388
|
-
return;
|
|
389
|
-
case KEY_LEFT:
|
|
390
|
-
cursor = Math.max(cursor - 1, 0);
|
|
391
|
-
render();
|
|
392
|
-
return;
|
|
393
|
-
case KEY_HOME:
|
|
394
|
-
case KEY_HOME_TILDE:
|
|
395
|
-
case KEY_HOME_APP:
|
|
396
|
-
cursor = 0;
|
|
397
|
-
render();
|
|
398
|
-
return;
|
|
399
|
-
case KEY_END:
|
|
400
|
-
case KEY_END_TILDE:
|
|
401
|
-
case KEY_END_APP:
|
|
402
|
-
cursor = buffer.length;
|
|
403
|
-
render();
|
|
404
|
-
return;
|
|
405
|
-
case KEY_DELETE:
|
|
406
|
-
if (cursor < buffer.length) {
|
|
407
|
-
buffer.splice(cursor, 1);
|
|
408
|
-
historyIndex = this.history.length;
|
|
409
|
-
render();
|
|
410
|
-
}
|
|
411
|
-
return;
|
|
412
|
-
default:
|
|
413
|
-
break;
|
|
414
|
-
}
|
|
415
|
-
if (SHIFT_ENTER_SEQUENCES.includes(sequence)) {
|
|
416
|
-
insertText("\n");
|
|
417
|
-
}
|
|
418
|
-
};
|
|
419
|
-
|
|
420
606
|
const consume = (text) => {
|
|
421
607
|
pending += text;
|
|
422
608
|
for (;;) {
|
|
@@ -453,26 +639,26 @@ export class ShellChatInput {
|
|
|
453
639
|
continue;
|
|
454
640
|
}
|
|
455
641
|
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
pending
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
handleSequence(sequence);
|
|
462
|
-
pending = pending.slice(sequence.length);
|
|
463
|
-
continue;
|
|
642
|
+
const matched = matchEscape(pending);
|
|
643
|
+
if (!matched) {
|
|
644
|
+
handlePlainText(pending);
|
|
645
|
+
pending = "";
|
|
646
|
+
return;
|
|
464
647
|
}
|
|
465
|
-
if (
|
|
466
|
-
pending.length === 1
|
|
467
|
-
|| KNOWN_ESCAPE_SEQUENCES.some((candidate) => (
|
|
468
|
-
candidate.startsWith(pending)
|
|
469
|
-
))
|
|
470
|
-
) {
|
|
471
|
-
// The rest of the sequence may arrive in the next chunk.
|
|
648
|
+
if (matched.incomplete) {
|
|
472
649
|
return;
|
|
473
650
|
}
|
|
474
|
-
|
|
475
|
-
|
|
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);
|
|
476
662
|
}
|
|
477
663
|
};
|
|
478
664
|
|
|
@@ -481,7 +667,6 @@ export class ShellChatInput {
|
|
|
481
667
|
const render = () => {
|
|
482
668
|
const width = columns();
|
|
483
669
|
const promptWidth = displayWidth(prompt);
|
|
484
|
-
const text = buffer.join("");
|
|
485
670
|
|
|
486
671
|
const rendered = [];
|
|
487
672
|
const ranges = []; // { start, end } flat char indices per rendered line
|
|
@@ -496,15 +681,27 @@ export class ShellChatInput {
|
|
|
496
681
|
currentWidth = 0;
|
|
497
682
|
rangeStart = charIndex;
|
|
498
683
|
};
|
|
499
|
-
for (
|
|
684
|
+
for (let index = 0; index < buffer.length; index += 1) {
|
|
685
|
+
const ch = buffer[index];
|
|
500
686
|
if (ch === "\n") {
|
|
501
687
|
flush();
|
|
502
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;
|
|
503
696
|
continue;
|
|
504
697
|
}
|
|
505
|
-
const w =
|
|
698
|
+
const w = graphemeWidth(ch);
|
|
506
699
|
const base = rendered.length === 0 ? promptWidth : 0;
|
|
507
|
-
|
|
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) {
|
|
508
705
|
flush();
|
|
509
706
|
}
|
|
510
707
|
current += ch;
|
|
@@ -523,10 +720,12 @@ export class ShellChatInput {
|
|
|
523
720
|
// top — using `linesDrawn - 1` here overshoots whenever the cursor sits
|
|
524
721
|
// above the bottom row, drifting the whole block up the screen on every
|
|
525
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.
|
|
725
|
+
outputStream.write("\r");
|
|
526
726
|
if (cursorRow > 0) {
|
|
527
727
|
outputStream.write(`\x1b[${cursorRow}A`);
|
|
528
728
|
}
|
|
529
|
-
outputStream.write("\r");
|
|
530
729
|
for (let index = 0; index < rendered.length; index += 1) {
|
|
531
730
|
outputStream.write("\x1b[2K");
|
|
532
731
|
if (index === 0) {
|
|
@@ -537,14 +736,11 @@ export class ShellChatInput {
|
|
|
537
736
|
outputStream.write("\r\n");
|
|
538
737
|
}
|
|
539
738
|
}
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
}
|
|
546
|
-
}
|
|
547
|
-
}
|
|
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");
|
|
548
744
|
linesDrawn = rendered.length;
|
|
549
745
|
|
|
550
746
|
// Position the cursor at the flat cursor index.
|
|
@@ -555,20 +751,21 @@ export class ShellChatInput {
|
|
|
555
751
|
if (cursor < range.end || r === rendered.length - 1) {
|
|
556
752
|
row = r;
|
|
557
753
|
const base = r === 0 ? promptWidth : 0;
|
|
558
|
-
const prefixLength = Math.max(0, cursor - range.start);
|
|
559
754
|
col = base + displayWidth(
|
|
560
|
-
|
|
755
|
+
buffer.slice(range.start, cursor).join(""),
|
|
561
756
|
);
|
|
562
757
|
break;
|
|
563
758
|
}
|
|
564
759
|
}
|
|
565
760
|
cursorRow = row;
|
|
566
|
-
// 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.
|
|
567
764
|
const moveUp = rendered.length - 1 - row;
|
|
765
|
+
outputStream.write("\r");
|
|
568
766
|
if (moveUp > 0) {
|
|
569
767
|
outputStream.write(`\x1b[${moveUp}A`);
|
|
570
768
|
}
|
|
571
|
-
outputStream.write("\r");
|
|
572
769
|
if (col > 0) {
|
|
573
770
|
outputStream.write(`\x1b[${col}C`);
|
|
574
771
|
}
|
|
@@ -586,6 +783,7 @@ export class ShellChatInput {
|
|
|
586
783
|
};
|
|
587
784
|
|
|
588
785
|
inputStream.setRawMode?.(true);
|
|
786
|
+
outputStream.write(DISABLE_AUTOWRAP);
|
|
589
787
|
outputStream.write(ENABLE_BRACKETED_PASTE);
|
|
590
788
|
// Ask the terminal to disambiguate modified Enter. Kitty CSI-u covers
|
|
591
789
|
// kitty/iTerm; xterm modifyOtherKeys covers many others, including some
|