agent-sh 0.12.16 → 0.12.18
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/agent/agent-loop.js +1 -2
- package/dist/agent/tools/edit-file.js +6 -23
- package/dist/agent/tools/write-file.js +6 -23
- package/dist/agent/types.d.ts +2 -0
- package/dist/extensions/tui-renderer.js +21 -10
- package/dist/settings.d.ts +2 -0
- package/dist/settings.js +1 -0
- package/dist/utils/line-editor.d.ts +5 -0
- package/dist/utils/line-editor.js +59 -31
- package/package.json +1 -1
package/dist/agent/agent-loop.js
CHANGED
|
@@ -995,8 +995,7 @@ export class AgentLoop {
|
|
|
995
995
|
const absPath = path.resolve(process.cwd(), args.path);
|
|
996
996
|
this.fileReadCache.delete(absPath);
|
|
997
997
|
}
|
|
998
|
-
|
|
999
|
-
const resultDisplay = tool.formatResult?.(args, result);
|
|
998
|
+
const resultDisplay = result.display ?? tool.formatResult?.(args, result);
|
|
1000
999
|
// Emit completion events (via transform pipe so extensions can override)
|
|
1001
1000
|
this.bus.emitTransform("agent:tool-completed", {
|
|
1002
1001
|
toolCallId: id, exitCode: result.exitCode,
|
|
@@ -69,13 +69,7 @@ export function createEditFileTool(getCwd) {
|
|
|
69
69
|
icon: "✎",
|
|
70
70
|
locations: [{ path: args.path }],
|
|
71
71
|
}),
|
|
72
|
-
|
|
73
|
-
if (result.isError)
|
|
74
|
-
return {};
|
|
75
|
-
const m = result.content.match(/\((\+\d+(?:\s-\d+)?)\)/);
|
|
76
|
-
return m ? { summary: m[1] } : {};
|
|
77
|
-
},
|
|
78
|
-
async execute(args, onChunk) {
|
|
72
|
+
async execute(args) {
|
|
79
73
|
const filePath = expandHome(args.path);
|
|
80
74
|
const oldText = args.old_text;
|
|
81
75
|
const newText = args.new_text;
|
|
@@ -120,27 +114,16 @@ export function createEditFileTool(getCwd) {
|
|
|
120
114
|
? newContent.replace(/\n/g, "\r\n")
|
|
121
115
|
: newContent;
|
|
122
116
|
await fs.writeFile(absPath, finalContent);
|
|
123
|
-
// Compute and stream diff for display (windowed — only diffs the edit region)
|
|
124
117
|
const diff = computeEditDiff(normalized, normalizedOld, normalizedNew, replaceAll);
|
|
125
|
-
|
|
126
|
-
for (const hunk of diff.hunks) {
|
|
127
|
-
for (const line of hunk.lines) {
|
|
128
|
-
if (line.type === "added")
|
|
129
|
-
onChunk(`+${line.text}\n`);
|
|
130
|
-
else if (line.type === "removed")
|
|
131
|
-
onChunk(`-${line.text}\n`);
|
|
132
|
-
else
|
|
133
|
-
onChunk(` ${line.text}\n`);
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
const stats = diff.isNewFile
|
|
138
|
-
? `+${diff.added}`
|
|
139
|
-
: `+${diff.added} -${diff.removed}`;
|
|
118
|
+
const stats = diff.isNewFile ? `+${diff.added}` : `+${diff.added} -${diff.removed}`;
|
|
140
119
|
return {
|
|
141
120
|
content: `Edited ${absPath} (${stats})`,
|
|
142
121
|
exitCode: 0,
|
|
143
122
|
isError: false,
|
|
123
|
+
display: {
|
|
124
|
+
summary: stats,
|
|
125
|
+
body: { kind: "diff", diff, filePath: absPath },
|
|
126
|
+
},
|
|
144
127
|
};
|
|
145
128
|
}
|
|
146
129
|
catch (err) {
|
|
@@ -29,13 +29,7 @@ export function createWriteFileTool(getCwd) {
|
|
|
29
29
|
icon: "✎",
|
|
30
30
|
locations: [{ path: args.path }],
|
|
31
31
|
}),
|
|
32
|
-
|
|
33
|
-
if (result.isError)
|
|
34
|
-
return {};
|
|
35
|
-
const m = result.content.match(/\((\+\d+(?:\s-\d+)?)\)/);
|
|
36
|
-
return m ? { summary: m[1] } : {};
|
|
37
|
-
},
|
|
38
|
-
async execute(args, onChunk) {
|
|
32
|
+
async execute(args) {
|
|
39
33
|
const filePath = expandHome(args.path);
|
|
40
34
|
const content = args.content;
|
|
41
35
|
const absPath = path.resolve(getCwd(), filePath);
|
|
@@ -49,29 +43,18 @@ export function createWriteFileTool(getCwd) {
|
|
|
49
43
|
}
|
|
50
44
|
await fs.mkdir(path.dirname(absPath), { recursive: true });
|
|
51
45
|
await fs.writeFile(absPath, content);
|
|
52
|
-
// Compute and stream diff for display
|
|
53
46
|
const diff = computeDiff(oldContent, content);
|
|
54
|
-
|
|
55
|
-
for (const hunk of diff.hunks) {
|
|
56
|
-
for (const line of hunk.lines) {
|
|
57
|
-
if (line.type === "added")
|
|
58
|
-
onChunk(`+${line.text}\n`);
|
|
59
|
-
else if (line.type === "removed")
|
|
60
|
-
onChunk(`-${line.text}\n`);
|
|
61
|
-
else
|
|
62
|
-
onChunk(` ${line.text}\n`);
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
const stats = diff.isNewFile
|
|
67
|
-
? `+${diff.added}`
|
|
68
|
-
: `+${diff.added} -${diff.removed}`;
|
|
47
|
+
const stats = diff.isNewFile ? `+${diff.added}` : `+${diff.added} -${diff.removed}`;
|
|
69
48
|
return {
|
|
70
49
|
content: oldContent === null
|
|
71
50
|
? `Created ${absPath} (${stats})`
|
|
72
51
|
: `Wrote ${absPath} (${stats})`,
|
|
73
52
|
exitCode: 0,
|
|
74
53
|
isError: false,
|
|
54
|
+
display: {
|
|
55
|
+
summary: stats,
|
|
56
|
+
body: { kind: "diff", diff, filePath: absPath },
|
|
57
|
+
},
|
|
75
58
|
};
|
|
76
59
|
}
|
|
77
60
|
catch (err) {
|
package/dist/agent/types.d.ts
CHANGED
|
@@ -19,6 +19,8 @@ export interface ToolResult {
|
|
|
19
19
|
content: string;
|
|
20
20
|
exitCode: number | null;
|
|
21
21
|
isError: boolean;
|
|
22
|
+
/** When set, takes precedence over `tool.formatResult()`. */
|
|
23
|
+
display?: ToolResultDisplay;
|
|
22
24
|
}
|
|
23
25
|
/** Structured result display — returned by formatResult or computed by defaults. */
|
|
24
26
|
export interface ToolResultDisplay {
|
|
@@ -652,22 +652,33 @@ export default function activate(ctx) {
|
|
|
652
652
|
function renderDiffBody(diff, filePath, width) {
|
|
653
653
|
if (diff.isIdentical)
|
|
654
654
|
return [];
|
|
655
|
-
const boxW = Math.min(120, width - 2);
|
|
655
|
+
const boxW = Math.min(120, width - 2);
|
|
656
656
|
const contentW = boxW - 4;
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
657
|
+
let body;
|
|
658
|
+
if (diff.isNewFile) {
|
|
659
|
+
const lines = diff.hunks.flatMap(h => h.lines.map(l => l.text));
|
|
660
|
+
const preview = getSettings().newFilePreviewLines;
|
|
661
|
+
const head = lines.slice(0, preview);
|
|
662
|
+
const truncated = head.map(l => l.length > contentW ? l.slice(0, contentW - 1) + "…" : l);
|
|
663
|
+
const more = lines.length > preview
|
|
664
|
+
? [`${p.dim}… ${lines.length - preview} more lines${p.reset}`]
|
|
665
|
+
: [];
|
|
666
|
+
body = ["", ...truncated, ...more, ""];
|
|
667
|
+
}
|
|
668
|
+
else {
|
|
669
|
+
const diffLines = renderDiff(diff, {
|
|
670
|
+
width: contentW,
|
|
671
|
+
filePath,
|
|
672
|
+
maxLines: getSettings().diffMaxLines,
|
|
673
|
+
trueColor: true,
|
|
674
|
+
});
|
|
675
|
+
body = diffLines.length > 1 ? ["", ...diffLines.slice(1), ""] : diffLines;
|
|
676
|
+
}
|
|
665
677
|
return renderBoxFrame(body, {
|
|
666
678
|
width: boxW,
|
|
667
679
|
style: "rounded",
|
|
668
680
|
borderColor: p.dim,
|
|
669
681
|
title: diffTitle(filePath, diff),
|
|
670
|
-
footer,
|
|
671
682
|
});
|
|
672
683
|
}
|
|
673
684
|
/** Render output lines with truncation. */
|
package/dist/settings.d.ts
CHANGED
|
@@ -67,6 +67,8 @@ export interface Settings {
|
|
|
67
67
|
readOutputMaxLines?: number;
|
|
68
68
|
/** Max diff lines rendered in the TUI (Infinity = no limit). */
|
|
69
69
|
diffMaxLines?: number;
|
|
70
|
+
/** Lines of head content shown when a brand-new file is created. */
|
|
71
|
+
newFilePreviewLines?: number;
|
|
70
72
|
/** Tool protocol:
|
|
71
73
|
* "api" — all tools sent with full schema.
|
|
72
74
|
* "deferred" — extensions dispatched through `use_extension(name, args)` meta-tool.
|
package/dist/settings.js
CHANGED
|
@@ -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
|
}
|