agent-sh 0.12.15 → 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.
@@ -83,11 +83,21 @@ export class ConversationState {
83
83
  this.eagerNucleateUser(text);
84
84
  }
85
85
  addAssistantMessage(content, toolCalls, extras) {
86
- // extras is opaque provider payload to echo back (reasoning_content,
87
- // reasoning_details, etc.). Spread verbatim; shape is the stream
88
- // parser's concern.
89
- const base = { role: "assistant", content: content ?? (toolCalls?.length ? null : "") };
90
- if (toolCalls?.length) {
86
+ const hasToolCalls = !!toolCalls?.length;
87
+ // Promote reasoning into content on reasoning-only turns; strict
88
+ // providers (DeepSeek native) reject content="" with no tool_calls.
89
+ if (!content && !hasToolCalls) {
90
+ const r = (extras?.reasoning_content ?? extras?.reasoning);
91
+ if (typeof r === "string" && r)
92
+ content = r;
93
+ }
94
+ if (!content && !hasToolCalls)
95
+ return;
96
+ const base = {
97
+ role: "assistant",
98
+ content: hasToolCalls ? (content ?? null) : content,
99
+ };
100
+ if (hasToolCalls) {
91
101
  base.tool_calls = toolCalls.map((tc) => ({
92
102
  id: tc.id,
93
103
  type: "function",
@@ -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 (windowed only diffs the edit region)
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
- 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`);
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
- 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`);
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}`
package/dist/index.js CHANGED
@@ -9,6 +9,7 @@ import { loadExtensions } from "./extension-loader.js";
9
9
  import { getSettings } from "./settings.js";
10
10
  import { discoverSkills } from "./agent/skills.js";
11
11
  import { runInit } from "./init.js";
12
+ import { PACKAGE_VERSION } from "./utils/package-version.js";
12
13
  /**
13
14
  * Capture the user's full shell environment.
14
15
  * This picks up env vars exported in .zshrc/.bashrc that the
@@ -102,6 +103,10 @@ function parseArgs(argv) {
102
103
  const exts = argv[++i].split(",").map(s => s.trim());
103
104
  extensions = extensions ? [...extensions, ...exts] : exts;
104
105
  }
106
+ else if (arg === "--version" || arg === "-V") {
107
+ console.log(PACKAGE_VERSION);
108
+ process.exit(0);
109
+ }
105
110
  else if (arg === "--help" || arg === "-h") {
106
111
  console.log(`agent-sh — a shell-first terminal where AI is one keystroke away
107
112
 
@@ -120,6 +125,7 @@ General Options:
120
125
  --shell <path> Shell to use (default: $SHELL or /bin/bash)
121
126
  -e, --extensions Extensions to load (comma-separated, repeatable)
122
127
  -h, --help Show this help
128
+ -V, --version Print version and exit
123
129
 
124
130
  Environment Variables:
125
131
  OPENAI_API_KEY API key for LLM provider
@@ -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.15",
3
+ "version": "0.12.17",
4
4
  "description": "A shell-first terminal where AI is one keystroke away",
5
5
  "type": "module",
6
6
  "main": "dist/core.js",