zames_pro 2.6.0 → 2.8.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 +21 -0
- package/dist/agent-loop.js +141 -83
- package/dist/attachments.js +267 -0
- package/dist/browser.js +139 -50
- package/dist/config-menu.js +25 -25
- package/dist/config.js +5 -5
- package/dist/gitTools.js +4 -4
- package/dist/i18n.js +31 -7
- package/dist/index.js +295 -168
- package/dist/input.js +237 -83
- package/dist/markdown.js +1 -1
- package/dist/net-capture.js +20 -20
- package/dist/self-review.js +16 -16
- package/dist/sessions.js +11 -11
- package/dist/spinner.js +20 -16
- package/dist/system-prompt.js +29 -2
- package/dist/theme.js +26 -26
- package/dist/tools.js +9 -9
- package/dist/transcript.js +2 -2
- package/dist/types.js +2 -2
- package/dist/web.js +16 -16
- package/dist/xml-toolcall.js +89 -14
- package/package.json +1 -1
- package/scripts/postinstall.mjs +78 -51
package/dist/input.js
CHANGED
|
@@ -1,34 +1,73 @@
|
|
|
1
1
|
import { theme } from './theme.js';
|
|
2
2
|
import { renderMarkdown } from './markdown.js';
|
|
3
3
|
import { randomThinkingPhrase, stripEllipsis } from './spinner.js';
|
|
4
|
-
|
|
4
|
+
import { AttachmentStore, parseImagePaste, looksLikeFilePath, } from './attachments.js';
|
|
5
|
+
// A permanent input line at the bottom of the terminal + a status/output area above it.
|
|
5
6
|
//
|
|
6
|
-
//
|
|
7
|
-
// *
|
|
8
|
-
// *
|
|
9
|
-
// *
|
|
10
|
-
//
|
|
11
|
-
// *
|
|
12
|
-
//
|
|
13
|
-
// Ctrl+J, Ctrl+Enter
|
|
14
|
-
//
|
|
15
|
-
// *
|
|
16
|
-
// *
|
|
17
|
-
// Ctrl+W; Ctrl+K —
|
|
18
|
-
// *
|
|
19
|
-
//
|
|
20
|
-
// *
|
|
21
|
-
//
|
|
7
|
+
// Why a custom editor:
|
|
8
|
+
// * the input line is ALWAYS visible and is not overwritten by the agent's output;
|
|
9
|
+
// * the spinner/status and answers are printed ABOVE the input line (printAbove);
|
|
10
|
+
// * redraw accounts for wrapping by terminal width, so there is no
|
|
11
|
+
// line duplication on multiline input;
|
|
12
|
+
// * multiline input: Enter sends, but if the cursor is right after a «\»,
|
|
13
|
+
// Enter removes the «\» and breaks the line (like in Claude Code).
|
|
14
|
+
// Ctrl+J, Ctrl+Enter and Shift+Enter (in terminals with the extended
|
|
15
|
+
// protocol) always insert a newline;
|
|
16
|
+
// * long lines wrap by words (a word is not split in the middle);
|
|
17
|
+
// * word movement: Ctrl+←/→ (and Alt+B/Alt+F), word deletion
|
|
18
|
+
// Ctrl+W; Ctrl+K — delete to end of line;
|
|
19
|
+
// * input history: ↑/↓ at the edges of the input (inside multiline
|
|
20
|
+
// input the arrows move by lines, like in a normal terminal);
|
|
21
|
+
// * slash-command hints: when you type «/», matching commands with
|
|
22
|
+
// descriptions are shown below the line, Tab completes.
|
|
22
23
|
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
24
|
+
// All control characters are assembled from codes so the file has no
|
|
25
|
+
// "raw" ESC/CR/LF in string literals (see AGENTS.md).
|
|
25
26
|
const NL = String.fromCharCode(10);
|
|
26
27
|
const CR = String.fromCharCode(13);
|
|
27
28
|
const ESC = String.fromCharCode(27);
|
|
28
29
|
const PASTE_START = ESC + '[200~';
|
|
29
30
|
const PASTE_END = ESC + '[201~';
|
|
30
|
-
//
|
|
31
|
-
//
|
|
31
|
+
// A large paste (3+ lines) is collapsed in the input line into a short
|
|
32
|
+
// marker "[Pasted lines#N]" so the line stays readable; the original text is
|
|
33
|
+
// kept aside and expanded back on submit. Pastes of 1–2 lines are inserted
|
|
34
|
+
// as-is (small pastes are usually short and don't clutter the line).
|
|
35
|
+
export const PASTE_MIN_LINES = 3;
|
|
36
|
+
// Number of lines in a pasted block (the trailing newline doesn't add a line).
|
|
37
|
+
export function countPasteLines(raw) {
|
|
38
|
+
const normalized = String(raw).split(CR + NL).join(NL).split(CR).join(NL);
|
|
39
|
+
const trimmed = normalized.replace(/\n+$/, '');
|
|
40
|
+
if (trimmed === '')
|
|
41
|
+
return 1;
|
|
42
|
+
return trimmed.split(NL).length;
|
|
43
|
+
}
|
|
44
|
+
export function formatPasteMarker(lines) {
|
|
45
|
+
return '[Pasted lines#' + lines + ']';
|
|
46
|
+
}
|
|
47
|
+
// Decide how to represent a pasted block: null — insert as-is (small paste),
|
|
48
|
+
// otherwise { marker, text }: the marker goes into the input line, and the
|
|
49
|
+
// original text is expanded back on submit.
|
|
50
|
+
export function pasteReplacement(raw) {
|
|
51
|
+
const text = String(raw).split(CR + NL).join(NL).split(CR).join(NL);
|
|
52
|
+
const lines = countPasteLines(text);
|
|
53
|
+
if (lines < PASTE_MIN_LINES)
|
|
54
|
+
return null;
|
|
55
|
+
return { marker: formatPasteMarker(lines), text };
|
|
56
|
+
}
|
|
57
|
+
// Replace "[Pasted lines#N]" markers with the original pasted text (the first
|
|
58
|
+
// occurrence in order — matches the order in which the pastes were inserted).
|
|
59
|
+
export function expandPastes(pastes, text) {
|
|
60
|
+
let out = text;
|
|
61
|
+
for (const p of pastes) {
|
|
62
|
+
const idx = out.indexOf(p.marker);
|
|
63
|
+
if (idx !== -1) {
|
|
64
|
+
out = out.slice(0, idx) + p.text + out.slice(idx + p.marker.length);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return out;
|
|
68
|
+
}
|
|
69
|
+
// Dot animation: start from an empty string (0 dots), then grow.
|
|
70
|
+
// We align the width to the maximum (3) so the hint doesn't shift.
|
|
32
71
|
const DOTS = ['', '.', '..', '...'];
|
|
33
72
|
const DOTS_PAD = ' ';
|
|
34
73
|
const HINT = theme.dim(' · Esc — стоп');
|
|
@@ -40,15 +79,15 @@ function safeJson(v) {
|
|
|
40
79
|
return String(v);
|
|
41
80
|
}
|
|
42
81
|
}
|
|
43
|
-
//
|
|
44
|
-
// (
|
|
45
|
-
//
|
|
82
|
+
// How many visual lines the text occupies at the given terminal width
|
|
83
|
+
// (accounting for wrapping). Needed so the cursor/block erase don't drift
|
|
84
|
+
// when the status is longer than the terminal width and wraps onto 2+ lines.
|
|
46
85
|
export function visRows(s, cols) {
|
|
47
86
|
const width = Math.max(1, Number(cols) || 80);
|
|
48
87
|
const len = visLen(s);
|
|
49
88
|
return Math.max(1, Math.ceil(len / width));
|
|
50
89
|
}
|
|
51
|
-
//
|
|
90
|
+
// Visible length of a line without ANSI sequences.
|
|
52
91
|
export function visLen(s) {
|
|
53
92
|
s = String(s);
|
|
54
93
|
let n = 0;
|
|
@@ -82,13 +121,13 @@ export function layoutInput(promptStr, buf, cursor, cols) {
|
|
|
82
121
|
const prefix = first ? promptStr : pad;
|
|
83
122
|
const avail = Math.max(1, width - promptW);
|
|
84
123
|
const start = i;
|
|
85
|
-
//
|
|
86
|
-
//
|
|
87
|
-
//
|
|
88
|
-
//
|
|
124
|
+
// Build the next visual line. First we take as many
|
|
125
|
+
// characters as fit (count < avail). If we hit the width
|
|
126
|
+
// and the next character is not the end of the line, we roll back to the
|
|
127
|
+
// last space so we don't split a word in the middle (word-wrap).
|
|
89
128
|
let text = '';
|
|
90
129
|
let count = 0;
|
|
91
|
-
let lastSpace = -1; //
|
|
130
|
+
let lastSpace = -1; // index of the space in text (by Array.from)
|
|
92
131
|
while (i < chars.length && chars[i] !== NL && count < avail) {
|
|
93
132
|
if (chars[i] === ' ')
|
|
94
133
|
lastSpace = count;
|
|
@@ -98,7 +137,7 @@ export function layoutInput(promptStr, buf, cursor, cols) {
|
|
|
98
137
|
}
|
|
99
138
|
const atLineEnd = i >= chars.length || chars[i] === NL;
|
|
100
139
|
if (!atLineEnd && count >= avail && lastSpace > 0) {
|
|
101
|
-
//
|
|
140
|
+
// Move the "tail" of the line to the next visual line.
|
|
102
141
|
const textChars = Array.from(text);
|
|
103
142
|
const tailLen = textChars.length - lastSpace;
|
|
104
143
|
i -= tailLen;
|
|
@@ -149,13 +188,25 @@ export class LineEditor {
|
|
|
149
188
|
_thinkBase;
|
|
150
189
|
_wasRaw;
|
|
151
190
|
_onData;
|
|
152
|
-
//
|
|
191
|
+
// History of sent messages (for the up/down arrows).
|
|
153
192
|
history;
|
|
154
193
|
_histIndex;
|
|
155
194
|
_histDraft;
|
|
156
|
-
//
|
|
195
|
+
// Slash-command hints (shown when you type «/»).
|
|
157
196
|
slashCommands;
|
|
158
197
|
_suggestCount;
|
|
198
|
+
// Pasted blocks collapsed into markers (expanded back on submit).
|
|
199
|
+
pastes;
|
|
200
|
+
_pasteBuf;
|
|
201
|
+
// Attached images/files (saved to <project>/tmp and sent to the chat).
|
|
202
|
+
attachments;
|
|
203
|
+
_tmpDir;
|
|
204
|
+
// Insert callback: receives a pasted image/file from the terminal. Returns
|
|
205
|
+
// the attachment if it was saved, so the editor can show a marker.
|
|
206
|
+
onAttach;
|
|
207
|
+
// Clipboard callback: tries to read an image from the OS clipboard when the
|
|
208
|
+
// terminal itself sends no usable data (Ctrl+V, right-click, empty paste).
|
|
209
|
+
onClipboard;
|
|
159
210
|
constructor({ prompt = '> ', commands = [] } = {}) {
|
|
160
211
|
this.promptStr = prompt;
|
|
161
212
|
this.buf = '';
|
|
@@ -179,10 +230,33 @@ export class LineEditor {
|
|
|
179
230
|
this._histDraft = '';
|
|
180
231
|
this.slashCommands = commands;
|
|
181
232
|
this._suggestCount = 0;
|
|
233
|
+
this.pastes = [];
|
|
234
|
+
this._pasteBuf = '';
|
|
235
|
+
this.attachments = new AttachmentStore();
|
|
236
|
+
this._tmpDir = '';
|
|
237
|
+
this.onAttach = null;
|
|
238
|
+
this.onClipboard = null;
|
|
239
|
+
}
|
|
240
|
+
// Read the OS clipboard for an image and insert its marker. Used when the
|
|
241
|
+
// terminal sends no usable paste data (Ctrl+V / right-click / empty paste).
|
|
242
|
+
async _tryClipboard() {
|
|
243
|
+
if (!this.onClipboard)
|
|
244
|
+
return;
|
|
245
|
+
try {
|
|
246
|
+
const att = await this.onClipboard();
|
|
247
|
+
if (att) {
|
|
248
|
+
this._insert(att.marker);
|
|
249
|
+
this._render();
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
catch { }
|
|
182
253
|
}
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
254
|
+
setTmpDir(dir) {
|
|
255
|
+
this._tmpDir = dir;
|
|
256
|
+
}
|
|
257
|
+
// List of hints for the current input. We show them only when the line
|
|
258
|
+
// starts with «/» and is a single word (no spaces/newlines) — like
|
|
259
|
+
// command completion in Claude Code.
|
|
186
260
|
_suggestions() {
|
|
187
261
|
const b = this.buf;
|
|
188
262
|
if (!b.startsWith('/'))
|
|
@@ -192,8 +266,8 @@ export class LineEditor {
|
|
|
192
266
|
const q = b.toLowerCase();
|
|
193
267
|
return this.slashCommands.filter((c) => c.name.toLowerCase().startsWith(q));
|
|
194
268
|
}
|
|
195
|
-
// Tab:
|
|
196
|
-
//
|
|
269
|
+
// Tab: complete the command up to the common prefix; if there is a single
|
|
270
|
+
// match — insert it whole and add a space.
|
|
197
271
|
_completeCommand() {
|
|
198
272
|
const sugg = this._suggestions();
|
|
199
273
|
if (!sugg.length)
|
|
@@ -236,9 +310,9 @@ export class LineEditor {
|
|
|
236
310
|
if (stdin.setRawMode)
|
|
237
311
|
stdin.setRawMode(this._wasRaw || false);
|
|
238
312
|
}
|
|
239
|
-
//
|
|
240
|
-
//
|
|
241
|
-
//
|
|
313
|
+
// Temporarily hand the terminal to an external UI (e.g. the /config menu):
|
|
314
|
+
// we detach our input handler and erase the input block so foreign output
|
|
315
|
+
// doesn't overlap the input line. Paired with resume().
|
|
242
316
|
pause() {
|
|
243
317
|
const stdin = process.stdin;
|
|
244
318
|
this._stopDots();
|
|
@@ -263,7 +337,7 @@ export class LineEditor {
|
|
|
263
337
|
this.promptStr = str;
|
|
264
338
|
this._render();
|
|
265
339
|
}
|
|
266
|
-
//
|
|
340
|
+
// Update slash-command descriptions (interface language change).
|
|
267
341
|
setCommands(commands) {
|
|
268
342
|
this.slashCommands = commands;
|
|
269
343
|
this._render();
|
|
@@ -271,9 +345,10 @@ export class LineEditor {
|
|
|
271
345
|
clear() {
|
|
272
346
|
this.buf = '';
|
|
273
347
|
this.cursor = 0;
|
|
348
|
+
this.pastes = [];
|
|
274
349
|
this._render();
|
|
275
350
|
}
|
|
276
|
-
// ----------
|
|
351
|
+
// ---------- rendering ----------
|
|
277
352
|
_eraseBlock() {
|
|
278
353
|
if (!this.rendered)
|
|
279
354
|
return;
|
|
@@ -289,14 +364,14 @@ export class LineEditor {
|
|
|
289
364
|
let top = 0;
|
|
290
365
|
if (this.statusText) {
|
|
291
366
|
out += this.statusText + NL;
|
|
292
|
-
//
|
|
293
|
-
//
|
|
367
|
+
// The status may wrap onto several lines — we account for this,
|
|
368
|
+
// otherwise the block erase misses and statuses pile up.
|
|
294
369
|
top = visRows(this.statusText, cols);
|
|
295
370
|
}
|
|
296
371
|
const lay = layoutInput(this.promptStr, this.buf, this.cursor, cols);
|
|
297
372
|
out += lay.rows.map((r) => r.prefix + r.text).join(NL);
|
|
298
|
-
//
|
|
299
|
-
//
|
|
373
|
+
// We draw the slash-command hints BELOW the input line. We then move the
|
|
374
|
+
// cursor back up to the input line, so cursorRowFromTop doesn't change.
|
|
300
375
|
const sugg = this._suggestions();
|
|
301
376
|
const shown = sugg.slice(0, 8);
|
|
302
377
|
this._suggestCount = shown.length;
|
|
@@ -314,8 +389,8 @@ export class LineEditor {
|
|
|
314
389
|
}
|
|
315
390
|
process.stdout.write(out);
|
|
316
391
|
const lastRow = lay.rows.length - 1;
|
|
317
|
-
//
|
|
318
|
-
//
|
|
392
|
+
// Move the cursor up: first to the input line within lay, then further by
|
|
393
|
+
// the hint lines (if any) — the cursor must sit on the input.
|
|
319
394
|
const linesBelow = shown.length ? shown.length + (sugg.length > shown.length ? 1 : 0) : 0;
|
|
320
395
|
const up = lastRow - lay.cursorRow + linesBelow;
|
|
321
396
|
if (up > 0)
|
|
@@ -330,13 +405,13 @@ export class LineEditor {
|
|
|
330
405
|
this._eraseBlock();
|
|
331
406
|
this._writeBlock();
|
|
332
407
|
}
|
|
333
|
-
//
|
|
408
|
+
// Print a block ABOVE the input line and put the input line back.
|
|
334
409
|
printAbove(text) {
|
|
335
410
|
this._eraseBlock();
|
|
336
411
|
process.stdout.write(String(text) + NL);
|
|
337
412
|
this._writeBlock();
|
|
338
413
|
}
|
|
339
|
-
// ----------
|
|
414
|
+
// ---------- spinner-compatible interface ----------
|
|
340
415
|
setStatus(text) {
|
|
341
416
|
this.statusText = text || '';
|
|
342
417
|
this._render();
|
|
@@ -348,8 +423,8 @@ export class LineEditor {
|
|
|
348
423
|
}
|
|
349
424
|
}
|
|
350
425
|
_dots(n) {
|
|
351
|
-
//
|
|
352
|
-
//
|
|
426
|
+
// The dots are the same color as the base and of fixed width — otherwise
|
|
427
|
+
// the hint on the right "jumps" when the animation phase changes.
|
|
353
428
|
return theme.brown(DOTS[n] + DOTS_PAD.slice(DOTS[n].length));
|
|
354
429
|
}
|
|
355
430
|
_startThinking() {
|
|
@@ -399,14 +474,22 @@ export class LineEditor {
|
|
|
399
474
|
const rendered = renderMarkdown(msg);
|
|
400
475
|
this.printAbove(NL + theme.assistant('● Ответ') + NL + rendered + NL + theme.dim('─'.repeat(60)));
|
|
401
476
|
}
|
|
402
|
-
//
|
|
477
|
+
// Warning to the operator (e.g. the agent stopped suspiciously).
|
|
478
|
+
// Printed above the input line without overwriting it.
|
|
479
|
+
warning(msg) {
|
|
480
|
+
this.printAbove(NL + theme.warn('⚠ ' + msg));
|
|
481
|
+
}
|
|
482
|
+
// ---------- input ----------
|
|
403
483
|
_submit() {
|
|
404
|
-
const
|
|
405
|
-
if (!
|
|
484
|
+
const display = this.buf;
|
|
485
|
+
if (!display.trim()) {
|
|
406
486
|
this._render();
|
|
407
487
|
return;
|
|
408
488
|
}
|
|
409
|
-
//
|
|
489
|
+
// The input line shows compact markers "[Pasted lines#N]" instead of large
|
|
490
|
+
// pastes — expand them back into the original text before sending.
|
|
491
|
+
const text = expandPastes(this.pastes, display);
|
|
492
|
+
// Add to history only non-empty messages that don't duplicate the previous one.
|
|
410
493
|
if (text.trim() && this.history[this.history.length - 1] !== text) {
|
|
411
494
|
this.history.push(text);
|
|
412
495
|
}
|
|
@@ -417,9 +500,14 @@ export class LineEditor {
|
|
|
417
500
|
this.pendingText = null;
|
|
418
501
|
this._stopDots();
|
|
419
502
|
this.statusText = '';
|
|
420
|
-
this
|
|
503
|
+
// Attachments collected for this message (images/files). They are handed
|
|
504
|
+
// to the browser layer to be attached to the chat.
|
|
505
|
+
const attached = this.attachments.items.slice();
|
|
506
|
+
this.printAbove(theme.user('❯ ') + display);
|
|
507
|
+
this.pastes = [];
|
|
508
|
+
this.attachments.reset();
|
|
421
509
|
if (this.onSubmit)
|
|
422
|
-
this.onSubmit(text);
|
|
510
|
+
this.onSubmit(text, attached);
|
|
423
511
|
}
|
|
424
512
|
_insert(text) {
|
|
425
513
|
const chars = Array.from(this.buf);
|
|
@@ -428,9 +516,56 @@ export class LineEditor {
|
|
|
428
516
|
this.buf = next.join('');
|
|
429
517
|
this.cursor += ins.length;
|
|
430
518
|
}
|
|
431
|
-
//
|
|
432
|
-
// (
|
|
433
|
-
//
|
|
519
|
+
// Insert a pasted block. Small pastes (1–2 lines) go in as-is; large ones
|
|
520
|
+
// (3+ lines) are collapsed into "[Pasted lines#N]" so the input line stays
|
|
521
|
+
// readable — the original text is expanded back on submit.
|
|
522
|
+
_insertPaste(raw) {
|
|
523
|
+
// A pasted image (a data URL / base64 blob) or a file path is saved to
|
|
524
|
+
// tmp and shown as [image#N] / [file#N] — the marker is attached to the
|
|
525
|
+
// message on submit.
|
|
526
|
+
if (this.onAttach) {
|
|
527
|
+
const asImage = parseImagePaste(raw);
|
|
528
|
+
const trimmed = String(raw).trim();
|
|
529
|
+
const isPath = !asImage && looksLikeFilePath(trimmed);
|
|
530
|
+
if (asImage || isPath) {
|
|
531
|
+
void this._attachAsync(raw);
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
this._insertFallback(raw);
|
|
536
|
+
}
|
|
537
|
+
// Save a pasted image/file to tmp and put the [image#N]/[file#N] marker
|
|
538
|
+
// into the input line.
|
|
539
|
+
async _attachAsync(raw) {
|
|
540
|
+
if (!this.onAttach)
|
|
541
|
+
return;
|
|
542
|
+
try {
|
|
543
|
+
const att = await this.onAttach(raw);
|
|
544
|
+
if (att) {
|
|
545
|
+
this._insert(att.marker);
|
|
546
|
+
this._render();
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
catch { }
|
|
551
|
+
// Attachment failed (e.g. the path does not exist) — insert the text as-is
|
|
552
|
+
// so nothing is lost.
|
|
553
|
+
this._insertFallback(raw);
|
|
554
|
+
this._render();
|
|
555
|
+
}
|
|
556
|
+
_insertFallback(raw) {
|
|
557
|
+
const rep = pasteReplacement(raw);
|
|
558
|
+
if (!rep) {
|
|
559
|
+
const text = String(raw).split(CR + NL).join(NL).split(CR).join(NL);
|
|
560
|
+
this._insert(text);
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
this.pastes.push(rep);
|
|
564
|
+
this._insert(rep.marker);
|
|
565
|
+
}
|
|
566
|
+
// Insert a newline. If the cursor is right after a «\»
|
|
567
|
+
// (like in Claude Code), the character is removed — so «\» + Enter gives
|
|
568
|
+
// an ordinary break without a stray backslash in the text.
|
|
434
569
|
_insertNewline() {
|
|
435
570
|
const chars = Array.from(this.buf);
|
|
436
571
|
if (this.cursor > 0 && chars[this.cursor - 1] === '\\') {
|
|
@@ -477,9 +612,9 @@ export class LineEditor {
|
|
|
477
612
|
i++;
|
|
478
613
|
this.cursor = i;
|
|
479
614
|
}
|
|
480
|
-
//
|
|
481
|
-
//
|
|
482
|
-
//
|
|
615
|
+
// Is the cursor on the first/last VISUAL line (accounting for wrapping).
|
|
616
|
+
// Needed so Up/Down act as history at the edges of the input, and inside —
|
|
617
|
+
// as line movement, like in a normal terminal.
|
|
483
618
|
_onFirstVisualLine() {
|
|
484
619
|
const cols = process.stdout.columns || 80;
|
|
485
620
|
const lay = layoutInput(this.promptStr, this.buf, this.cursor, cols);
|
|
@@ -521,8 +656,8 @@ export class LineEditor {
|
|
|
521
656
|
nextEnd++;
|
|
522
657
|
this.cursor = nextStart + Math.min(col, nextEnd - nextStart);
|
|
523
658
|
}
|
|
524
|
-
//
|
|
525
|
-
//
|
|
659
|
+
// Move one word back (Ctrl+Left / Alt+B): skip spaces
|
|
660
|
+
// on the left, then go to the start of the word.
|
|
526
661
|
_wordLeft() {
|
|
527
662
|
const chars = Array.from(this.buf);
|
|
528
663
|
let i = this.cursor;
|
|
@@ -532,7 +667,7 @@ export class LineEditor {
|
|
|
532
667
|
i--;
|
|
533
668
|
this.cursor = i;
|
|
534
669
|
}
|
|
535
|
-
//
|
|
670
|
+
// Move one word forward (Ctrl+Right / Alt+F).
|
|
536
671
|
_wordRight() {
|
|
537
672
|
const chars = Array.from(this.buf);
|
|
538
673
|
let i = this.cursor;
|
|
@@ -542,7 +677,7 @@ export class LineEditor {
|
|
|
542
677
|
i++;
|
|
543
678
|
this.cursor = i;
|
|
544
679
|
}
|
|
545
|
-
//
|
|
680
|
+
// Delete the word on the left (Ctrl+W / Alt+Backspace).
|
|
546
681
|
_deleteWordLeft() {
|
|
547
682
|
const chars = Array.from(this.buf);
|
|
548
683
|
let i = this.cursor;
|
|
@@ -554,7 +689,7 @@ export class LineEditor {
|
|
|
554
689
|
this.buf = chars.join('');
|
|
555
690
|
this.cursor = i;
|
|
556
691
|
}
|
|
557
|
-
// ----------
|
|
692
|
+
// ---------- history ----------
|
|
558
693
|
_historyUp() {
|
|
559
694
|
if (!this.history.length)
|
|
560
695
|
return;
|
|
@@ -590,13 +725,23 @@ export class LineEditor {
|
|
|
590
725
|
if (this._inPaste) {
|
|
591
726
|
const end = s.indexOf(PASTE_END);
|
|
592
727
|
if (end === -1) {
|
|
593
|
-
|
|
728
|
+
// The paste arrived in several chunks — accumulate until the end marker.
|
|
729
|
+
this._pasteBuf += s;
|
|
594
730
|
s = '';
|
|
595
731
|
}
|
|
596
732
|
else {
|
|
597
|
-
this.
|
|
733
|
+
this._pasteBuf += s.slice(0, end);
|
|
598
734
|
s = s.slice(end + PASTE_END.length);
|
|
599
735
|
this._inPaste = false;
|
|
736
|
+
if (this._pasteBuf === '') {
|
|
737
|
+
// Empty bracketed paste: many terminals send this for an image on
|
|
738
|
+
// the clipboard (there is no text to deliver). Try the OS clipboard.
|
|
739
|
+
void this._tryClipboard();
|
|
740
|
+
}
|
|
741
|
+
else {
|
|
742
|
+
this._insertPaste(this._pasteBuf);
|
|
743
|
+
}
|
|
744
|
+
this._pasteBuf = '';
|
|
600
745
|
}
|
|
601
746
|
this._render();
|
|
602
747
|
continue;
|
|
@@ -606,14 +751,15 @@ export class LineEditor {
|
|
|
606
751
|
const before = s.slice(0, pasteAt);
|
|
607
752
|
s = s.slice(pasteAt + PASTE_START.length);
|
|
608
753
|
this._inPaste = true;
|
|
754
|
+
this._pasteBuf = '';
|
|
609
755
|
if (before) {
|
|
610
756
|
this._insert(before);
|
|
611
757
|
this._render();
|
|
612
758
|
}
|
|
613
759
|
continue;
|
|
614
760
|
}
|
|
615
|
-
// Shift+Enter, Ctrl+Enter
|
|
616
|
-
//
|
|
761
|
+
// Shift+Enter, Ctrl+Enter in terminals with the extended protocol
|
|
762
|
+
// and Alt+Enter — insert a newline.
|
|
617
763
|
if (s.startsWith(ESC + '[13;2u')) {
|
|
618
764
|
this._insertNewline();
|
|
619
765
|
s = s.slice(7);
|
|
@@ -647,10 +793,10 @@ export class LineEditor {
|
|
|
647
793
|
const ch = s[0];
|
|
648
794
|
const code = s.charCodeAt(0);
|
|
649
795
|
s = s.slice(1);
|
|
650
|
-
// Enter:
|
|
651
|
-
// «\»
|
|
652
|
-
// Ctrl+J (code 10)
|
|
653
|
-
// (ESC[13;5u)
|
|
796
|
+
// Enter: if the previous character is «\», Claude-Code style: remove
|
|
797
|
+
// the «\» and break the line; otherwise send the message.
|
|
798
|
+
// Ctrl+J (code 10) always inserts a newline; Ctrl+Enter
|
|
799
|
+
// (ESC[13;5u) and Shift+Enter too.
|
|
654
800
|
if (ch === CR) {
|
|
655
801
|
const before = this.cursor > 0 ? Array.from(this.buf)[this.cursor - 1] : '';
|
|
656
802
|
if (before === '\\') {
|
|
@@ -694,16 +840,24 @@ export class LineEditor {
|
|
|
694
840
|
if (code === 21) {
|
|
695
841
|
this.buf = '';
|
|
696
842
|
this.cursor = 0;
|
|
843
|
+
this.pastes = [];
|
|
697
844
|
this._render();
|
|
698
845
|
continue;
|
|
699
846
|
}
|
|
847
|
+
if (code === 22) {
|
|
848
|
+
// Ctrl+V: terminals rarely deliver an image as text here, so we try the
|
|
849
|
+
// OS clipboard first; if there is no image we fall back to reading the
|
|
850
|
+
// text clipboard via the terminal's own paste (nothing to do).
|
|
851
|
+
void this._tryClipboard();
|
|
852
|
+
continue;
|
|
853
|
+
}
|
|
700
854
|
if (code === 23) {
|
|
701
855
|
this._deleteWordLeft();
|
|
702
856
|
this._render();
|
|
703
857
|
continue;
|
|
704
858
|
}
|
|
705
859
|
if (code === 11) {
|
|
706
|
-
// Ctrl+K —
|
|
860
|
+
// Ctrl+K — delete from the cursor to the end of the line.
|
|
707
861
|
const arr = Array.from(this.buf);
|
|
708
862
|
let e = this.cursor;
|
|
709
863
|
while (e < arr.length && arr[e] !== NL)
|
|
@@ -720,7 +874,7 @@ export class LineEditor {
|
|
|
720
874
|
}
|
|
721
875
|
if (code === 27) {
|
|
722
876
|
// Ctrl+Left / Ctrl+Right (xterm: ESC [1;5D / ESC [1;5C;
|
|
723
|
-
//
|
|
877
|
+
// some terminals: ESC [5D / ESC [5C).
|
|
724
878
|
if (s.startsWith('[1;5D') || s.startsWith('[5D')) {
|
|
725
879
|
this._wordLeft();
|
|
726
880
|
s = s.slice(s.startsWith('[1;5D') ? 5 : 3);
|
|
@@ -746,7 +900,7 @@ export class LineEditor {
|
|
|
746
900
|
continue;
|
|
747
901
|
}
|
|
748
902
|
if (s.startsWith('[A')) {
|
|
749
|
-
//
|
|
903
|
+
// Up: on the first visual line — history, otherwise — the line above.
|
|
750
904
|
if (this._onFirstVisualLine())
|
|
751
905
|
this._historyUp();
|
|
752
906
|
else
|
|
@@ -782,7 +936,7 @@ export class LineEditor {
|
|
|
782
936
|
this._render();
|
|
783
937
|
continue;
|
|
784
938
|
}
|
|
785
|
-
// Alt+B / Alt+F —
|
|
939
|
+
// Alt+B / Alt+F — word movement.
|
|
786
940
|
if (s.startsWith('b') || s.startsWith('B')) {
|
|
787
941
|
this._wordLeft();
|
|
788
942
|
s = s.slice(1);
|
|
@@ -808,7 +962,7 @@ export class LineEditor {
|
|
|
808
962
|
}
|
|
809
963
|
}
|
|
810
964
|
}
|
|
811
|
-
//
|
|
965
|
+
// Layout check: node src/input.js --selftest
|
|
812
966
|
export function selftest() {
|
|
813
967
|
const cases = [
|
|
814
968
|
{ p: '> ', b: 'hello', c: 5, w: 80 },
|
package/dist/markdown.js
CHANGED
|
@@ -14,7 +14,7 @@ const theme = {
|
|
|
14
14
|
tableHeader: { color: '#9fb8d8', bold: true },
|
|
15
15
|
tableCell: { color: '#d0d0d0' },
|
|
16
16
|
};
|
|
17
|
-
//
|
|
17
|
+
// Code highlighting via cli-highlight (already present transitively).
|
|
18
18
|
function highlighter(code, lang) {
|
|
19
19
|
try {
|
|
20
20
|
return highlight(code, {
|