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.
@@ -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
- // Compute result display: tool-provided → default (none)
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
- formatResult: (_args, result) => {
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
- if (onChunk && diff.hunks.length > 0) {
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
- formatResult: (_args, result) => {
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
- if (onChunk && diff.hunks.length > 0) {
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) {
@@ -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); // -2 for writeLine indent
655
+ const boxW = Math.min(120, width - 2);
656
656
  const contentW = boxW - 4;
657
- const diffLines = renderDiff(diff, {
658
- width: contentW,
659
- filePath,
660
- maxLines: getSettings().diffMaxLines,
661
- trueColor: true,
662
- });
663
- const body = diffLines.length > 1 ? ["", ...diffLines.slice(1), ""] : diffLines;
664
- const footer = undefined;
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. */
@@ -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
@@ -30,6 +30,7 @@ const DEFAULTS = {
30
30
  maxCommandOutputLines: 3,
31
31
  readOutputMaxLines: 10,
32
32
  diffMaxLines: Infinity,
33
+ newFilePreviewLines: 5,
33
34
  skillPaths: [],
34
35
  diagnose: false,
35
36
  startupBanner: true,
@@ -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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-sh",
3
- "version": "0.12.16",
3
+ "version": "0.12.18",
4
4
  "description": "A shell-first terminal where AI is one keystroke away",
5
5
  "type": "module",
6
6
  "main": "dist/core.js",