agent-sh 0.12.16 → 0.12.17
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.
|
@@ -120,19 +120,18 @@ export function createEditFileTool(getCwd) {
|
|
|
120
120
|
? newContent.replace(/\n/g, "\r\n")
|
|
121
121
|
: newContent;
|
|
122
122
|
await fs.writeFile(absPath, finalContent);
|
|
123
|
-
// Compute and stream diff for display
|
|
123
|
+
// Compute and stream diff for display. Batch into one onChunk —
|
|
124
|
+
// per-line emits trigger N TUI renders for large hunks.
|
|
124
125
|
const diff = computeEditDiff(normalized, normalizedOld, normalizedNew, replaceAll);
|
|
125
126
|
if (onChunk && diff.hunks.length > 0) {
|
|
127
|
+
const parts = [];
|
|
126
128
|
for (const hunk of diff.hunks) {
|
|
127
129
|
for (const line of hunk.lines) {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
else if (line.type === "removed")
|
|
131
|
-
onChunk(`-${line.text}\n`);
|
|
132
|
-
else
|
|
133
|
-
onChunk(` ${line.text}\n`);
|
|
130
|
+
const prefix = line.type === "added" ? "+" : line.type === "removed" ? "-" : " ";
|
|
131
|
+
parts.push(`${prefix}${line.text}\n`);
|
|
134
132
|
}
|
|
135
133
|
}
|
|
134
|
+
onChunk(parts.join(""));
|
|
136
135
|
}
|
|
137
136
|
const stats = diff.isNewFile
|
|
138
137
|
? `+${diff.added}`
|
|
@@ -49,19 +49,18 @@ export function createWriteFileTool(getCwd) {
|
|
|
49
49
|
}
|
|
50
50
|
await fs.mkdir(path.dirname(absPath), { recursive: true });
|
|
51
51
|
await fs.writeFile(absPath, content);
|
|
52
|
-
// Compute and stream diff for display
|
|
52
|
+
// Compute and stream diff for display. Batch into one onChunk —
|
|
53
|
+
// per-line emits trigger N TUI renders for large files.
|
|
53
54
|
const diff = computeDiff(oldContent, content);
|
|
54
55
|
if (onChunk && diff.hunks.length > 0) {
|
|
56
|
+
const parts = [];
|
|
55
57
|
for (const hunk of diff.hunks) {
|
|
56
58
|
for (const line of hunk.lines) {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
else if (line.type === "removed")
|
|
60
|
-
onChunk(`-${line.text}\n`);
|
|
61
|
-
else
|
|
62
|
-
onChunk(` ${line.text}\n`);
|
|
59
|
+
const prefix = line.type === "added" ? "+" : line.type === "removed" ? "-" : " ";
|
|
60
|
+
parts.push(`${prefix}${line.text}\n`);
|
|
63
61
|
}
|
|
64
62
|
}
|
|
63
|
+
onChunk(parts.join(""));
|
|
65
64
|
}
|
|
66
65
|
const stats = diff.isNewFile
|
|
67
66
|
? `+${diff.added}`
|
|
@@ -54,6 +54,11 @@ export declare class LineEditor {
|
|
|
54
54
|
setText(value: string): void;
|
|
55
55
|
/** Process raw terminal input, return actions for the consumer. */
|
|
56
56
|
feed(data: string): LineEditAction[];
|
|
57
|
+
/** Accumulate `data` into pasteAccum until PASTE_END appears.
|
|
58
|
+
* Returns the marker index, or -1 if not yet seen (a partial-suffix
|
|
59
|
+
* match is stashed in pendingSeq so the next feed() can complete it). */
|
|
60
|
+
private consumePasteChunk;
|
|
61
|
+
private commitPaste;
|
|
57
62
|
/** Check if there's a pending incomplete escape sequence. */
|
|
58
63
|
hasPendingEscape(): boolean;
|
|
59
64
|
/** Flush a pending sequence — treat bare \x1b as cancel, discard incomplete CSI. */
|
|
@@ -19,6 +19,7 @@ const KITTY_KEY_NAMES = {
|
|
|
19
19
|
// ── Paste placeholder ───────────────────────────────────────────
|
|
20
20
|
/** First Unicode Private Use Area codepoint, used as paste placeholder. */
|
|
21
21
|
const PUA_BASE = 0xE000;
|
|
22
|
+
const PASTE_END = "\x1b[201~";
|
|
22
23
|
function isPUA(ch) {
|
|
23
24
|
const code = ch.charCodeAt(0);
|
|
24
25
|
return code >= PUA_BASE && code <= 0xF8FF;
|
|
@@ -115,7 +116,24 @@ export class LineEditor {
|
|
|
115
116
|
}
|
|
116
117
|
const actions = [];
|
|
117
118
|
let i = 0;
|
|
119
|
+
// Heuristic for terminals that don't advertise bracketed paste (or where
|
|
120
|
+
// it's stripped by tmux/ssh): a multi-byte chunk with line breaks is a
|
|
121
|
+
// paste, since typed input arrives one keystroke per chunk in raw mode.
|
|
122
|
+
if (!this.inPaste && data.length > 1 && /[\r\n]/.test(data)
|
|
123
|
+
&& data.indexOf("\x1b[200~") === -1) {
|
|
124
|
+
this.pasteAccum = data.replace(/\r\n?/g, "\n");
|
|
125
|
+
actions.push(...this.commitPaste());
|
|
126
|
+
return actions;
|
|
127
|
+
}
|
|
118
128
|
while (i < data.length) {
|
|
129
|
+
if (this.inPaste) {
|
|
130
|
+
const endIdx = this.consumePasteChunk(data.slice(i));
|
|
131
|
+
if (endIdx === -1)
|
|
132
|
+
return actions;
|
|
133
|
+
i += endIdx + PASTE_END.length;
|
|
134
|
+
actions.push(...this.commitPaste());
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
119
137
|
const ch = data[i];
|
|
120
138
|
// ── Escape sequences ────────────────────────────────
|
|
121
139
|
if (ch === "\x1b") {
|
|
@@ -207,16 +225,6 @@ export class LineEditor {
|
|
|
207
225
|
// Other Alt+key — ignore
|
|
208
226
|
continue;
|
|
209
227
|
}
|
|
210
|
-
// ── Bracket paste: accumulate into side buffer ─────
|
|
211
|
-
if (this.inPaste) {
|
|
212
|
-
if (ch === "\r") {
|
|
213
|
-
i++;
|
|
214
|
-
continue;
|
|
215
|
-
} // skip CR (CR+LF → just LF)
|
|
216
|
-
this.pasteAccum += ch;
|
|
217
|
-
i++;
|
|
218
|
-
continue;
|
|
219
|
-
}
|
|
220
228
|
// ── Control characters ──────────────────────────────
|
|
221
229
|
if (ch.charCodeAt(0) < 0x20 || ch === "\x7f") {
|
|
222
230
|
const action = this.handleControl(ch);
|
|
@@ -233,6 +241,47 @@ export class LineEditor {
|
|
|
233
241
|
}
|
|
234
242
|
return actions;
|
|
235
243
|
}
|
|
244
|
+
/** Accumulate `data` into pasteAccum until PASTE_END appears.
|
|
245
|
+
* Returns the marker index, or -1 if not yet seen (a partial-suffix
|
|
246
|
+
* match is stashed in pendingSeq so the next feed() can complete it). */
|
|
247
|
+
consumePasteChunk(data) {
|
|
248
|
+
const endIdx = data.indexOf(PASTE_END);
|
|
249
|
+
if (endIdx !== -1) {
|
|
250
|
+
this.pasteAccum += data.slice(0, endIdx).replace(/\r/g, "");
|
|
251
|
+
return endIdx;
|
|
252
|
+
}
|
|
253
|
+
let suffixLen = 0;
|
|
254
|
+
for (let p = Math.min(PASTE_END.length - 1, data.length); p > 0; p--) {
|
|
255
|
+
if (data.endsWith(PASTE_END.slice(0, p))) {
|
|
256
|
+
suffixLen = p;
|
|
257
|
+
break;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
const safeEnd = data.length - suffixLen;
|
|
261
|
+
this.pasteAccum += data.slice(0, safeEnd).replace(/\r/g, "");
|
|
262
|
+
if (suffixLen > 0)
|
|
263
|
+
this.pendingSeq = data.slice(safeEnd);
|
|
264
|
+
return -1;
|
|
265
|
+
}
|
|
266
|
+
commitPaste() {
|
|
267
|
+
this.inPaste = false;
|
|
268
|
+
const accum = this.pasteAccum;
|
|
269
|
+
this.pasteAccum = "";
|
|
270
|
+
if (!accum)
|
|
271
|
+
return [];
|
|
272
|
+
if (accum.indexOf("\n") === -1) {
|
|
273
|
+
this._buf = this._buf.slice(0, this.cursor) + accum + this._buf.slice(this.cursor);
|
|
274
|
+
this.cursor += accum.length;
|
|
275
|
+
}
|
|
276
|
+
else {
|
|
277
|
+
const id = this.pasteCounter++;
|
|
278
|
+
this.pastes.set(id, accum);
|
|
279
|
+
const placeholder = String.fromCharCode(PUA_BASE + id);
|
|
280
|
+
this._buf = this._buf.slice(0, this.cursor) + placeholder + this._buf.slice(this.cursor);
|
|
281
|
+
this.cursor++;
|
|
282
|
+
}
|
|
283
|
+
return [{ action: "changed" }];
|
|
284
|
+
}
|
|
236
285
|
/** Check if there's a pending incomplete escape sequence. */
|
|
237
286
|
hasPendingEscape() {
|
|
238
287
|
return this.pendingSeq.length > 0;
|
|
@@ -517,27 +566,6 @@ export class LineEditor {
|
|
|
517
566
|
this.inPaste = true;
|
|
518
567
|
this.pasteAccum = "";
|
|
519
568
|
}
|
|
520
|
-
else if (params === "201") {
|
|
521
|
-
this.inPaste = false;
|
|
522
|
-
if (this.pasteAccum) {
|
|
523
|
-
const lines = this.pasteAccum.split("\n");
|
|
524
|
-
if (lines.length <= 1) {
|
|
525
|
-
// Single-line paste — inline directly
|
|
526
|
-
this._buf = this._buf.slice(0, this.cursor) + this.pasteAccum + this._buf.slice(this.cursor);
|
|
527
|
-
this.cursor += this.pasteAccum.length;
|
|
528
|
-
}
|
|
529
|
-
else {
|
|
530
|
-
// Multi-line paste — store and insert placeholder
|
|
531
|
-
const id = this.pasteCounter++;
|
|
532
|
-
this.pastes.set(id, this.pasteAccum);
|
|
533
|
-
const placeholder = String.fromCharCode(PUA_BASE + id);
|
|
534
|
-
this._buf = this._buf.slice(0, this.cursor) + placeholder + this._buf.slice(this.cursor);
|
|
535
|
-
this.cursor++;
|
|
536
|
-
}
|
|
537
|
-
this.pasteAccum = "";
|
|
538
|
-
actions.push({ action: "changed" });
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
569
|
break;
|
|
542
570
|
// All other CSI sequences — silently ignored
|
|
543
571
|
}
|