agent-sh 0.9.0 → 0.10.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.
Files changed (78) hide show
  1. package/README.md +14 -21
  2. package/dist/agent/agent-loop.d.ts +43 -3
  3. package/dist/agent/agent-loop.js +811 -128
  4. package/dist/agent/conversation-state.d.ts +72 -21
  5. package/dist/agent/conversation-state.js +357 -150
  6. package/dist/agent/history-file.d.ts +13 -4
  7. package/dist/agent/history-file.js +110 -36
  8. package/dist/agent/nuclear-form.d.ts +28 -3
  9. package/dist/agent/nuclear-form.js +84 -3
  10. package/dist/agent/skills.d.ts +2 -4
  11. package/dist/agent/skills.js +10 -4
  12. package/dist/agent/subagent.d.ts +23 -0
  13. package/dist/agent/subagent.js +53 -11
  14. package/dist/agent/system-prompt.d.ts +34 -1
  15. package/dist/agent/system-prompt.js +96 -47
  16. package/dist/agent/token-budget.d.ts +5 -4
  17. package/dist/agent/token-budget.js +14 -19
  18. package/dist/agent/tool-protocol.d.ts +23 -1
  19. package/dist/agent/tool-protocol.js +169 -4
  20. package/dist/agent/tools/bash.js +3 -3
  21. package/dist/agent/tools/edit-file.js +9 -6
  22. package/dist/agent/tools/glob.js +4 -2
  23. package/dist/agent/tools/grep.js +27 -3
  24. package/dist/agent/tools/ls.js +5 -6
  25. package/dist/agent/types.d.ts +1 -1
  26. package/dist/context-manager.d.ts +17 -0
  27. package/dist/context-manager.js +37 -4
  28. package/dist/core.js +27 -6
  29. package/dist/event-bus.d.ts +59 -2
  30. package/dist/executor.d.ts +4 -3
  31. package/dist/executor.js +18 -15
  32. package/dist/extension-loader.js +50 -13
  33. package/dist/extensions/agent-backend.d.ts +8 -7
  34. package/dist/extensions/agent-backend.js +69 -48
  35. package/dist/extensions/index.js +0 -1
  36. package/dist/extensions/slash-commands.js +14 -9
  37. package/dist/extensions/tui-renderer.js +62 -78
  38. package/dist/index.js +25 -6
  39. package/dist/settings.d.ts +36 -5
  40. package/dist/settings.js +53 -9
  41. package/dist/shell/input-handler.d.ts +2 -1
  42. package/dist/shell/input-handler.js +82 -73
  43. package/dist/shell/shell.js +19 -2
  44. package/dist/types.d.ts +12 -0
  45. package/dist/utils/ansi.d.ts +5 -0
  46. package/dist/utils/ansi.js +1 -1
  47. package/dist/utils/compositor.d.ts +5 -0
  48. package/dist/utils/compositor.js +31 -3
  49. package/dist/utils/diff-renderer.d.ts +9 -0
  50. package/dist/utils/diff-renderer.js +221 -143
  51. package/dist/utils/diff.d.ts +21 -2
  52. package/dist/utils/diff.js +165 -89
  53. package/dist/utils/handler-registry.d.ts +5 -0
  54. package/dist/utils/handler-registry.js +6 -0
  55. package/dist/utils/line-editor.d.ts +11 -1
  56. package/dist/utils/line-editor.js +44 -5
  57. package/dist/utils/tool-display.d.ts +1 -1
  58. package/dist/utils/tool-display.js +4 -4
  59. package/examples/extensions/ash-acp-bridge/src/index.ts +4 -1
  60. package/examples/extensions/ash-mcp-bridge/index.ts +13 -3
  61. package/examples/extensions/claude-code-bridge/index.ts +198 -51
  62. package/examples/extensions/claude-code-bridge/package.json +1 -0
  63. package/examples/extensions/interactive-prompts.ts +39 -25
  64. package/examples/extensions/overlay-agent.ts +3 -3
  65. package/examples/extensions/peer-mesh.ts +115 -0
  66. package/examples/extensions/pi-bridge/index.ts +2 -2
  67. package/examples/extensions/questionnaire.ts +16 -5
  68. package/examples/extensions/subagents.ts +19 -4
  69. package/examples/extensions/terminal-buffer.ts +163 -0
  70. package/examples/extensions/user-shell.ts +136 -0
  71. package/examples/extensions/web-access.ts +8 -0
  72. package/package.json +36 -2
  73. package/dist/agent/tools/display.d.ts +0 -13
  74. package/dist/agent/tools/display.js +0 -70
  75. package/dist/agent/tools/user-shell.d.ts +0 -13
  76. package/dist/agent/tools/user-shell.js +0 -87
  77. package/dist/extensions/terminal-buffer.d.ts +0 -14
  78. package/dist/extensions/terminal-buffer.js +0 -134
package/dist/settings.js CHANGED
@@ -17,23 +17,24 @@ const DEFAULTS = {
17
17
  defaultBackend: "ash",
18
18
  toolMode: "api",
19
19
  contextWindowSize: 20,
20
- contextBudget: 16384,
21
- shellTruncateThreshold: 10,
22
- shellHeadLines: 5,
23
- shellTailLines: 5,
24
- recallExpandMaxLines: 100,
20
+ contextBudget: 32768,
21
+ shellTruncateThreshold: 20,
22
+ shellHeadLines: 10,
23
+ shellTailLines: 10,
24
+ recallExpandMaxLines: 500,
25
25
  shellContextRatio: 0.35,
26
- historyMaxBytes: 102400,
27
- historyStartupEntries: 50,
28
- nuclearMaxEntries: 200,
26
+ historyMaxBytes: 104857600, // 100MB — history is only accessed via search/expand, never loaded wholesale
27
+ historyStartupEntries: 100,
29
28
  autoCompactThreshold: 0.5,
30
29
  maxCommandOutputLines: 3,
31
30
  readOutputMaxLines: 10,
32
- diffMaxLines: 20,
31
+ diffMaxLines: Infinity,
33
32
  skillPaths: [],
33
+ diagnose: false,
34
34
  startupBanner: true,
35
35
  promptIndicator: true,
36
36
  disabledBuiltins: [],
37
+ disabledExtensions: [],
37
38
  };
38
39
  let cached = null;
39
40
  /** Load settings from disk (cached after first call). */
@@ -74,6 +75,49 @@ export function getExtensionSettings(namespace, defaults) {
74
75
  export function reloadSettings() {
75
76
  cached = null;
76
77
  }
78
+ /**
79
+ * Deep-merge a patch into ~/.agent-sh/settings.json on disk.
80
+ *
81
+ * Reads the raw file (preserving unknown keys), merges the patch, writes back
82
+ * with 2-space indentation, and clears the cache so subsequent getSettings()
83
+ * calls see the new values.
84
+ *
85
+ * Used by runtime controls (`/model`, `/backend`) that want their selection
86
+ * to persist as the default across restarts.
87
+ */
88
+ export function updateSettings(patch) {
89
+ let existing = {};
90
+ try {
91
+ const raw = fs.readFileSync(SETTINGS_PATH, "utf-8");
92
+ existing = JSON.parse(raw);
93
+ }
94
+ catch {
95
+ // file missing or unreadable — start fresh
96
+ }
97
+ const merged = deepMerge(existing, patch);
98
+ try {
99
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
100
+ fs.writeFileSync(SETTINGS_PATH, JSON.stringify(merged, null, 2) + "\n", "utf-8");
101
+ cached = null;
102
+ }
103
+ catch (err) {
104
+ console.error(`[agent-sh] Warning: failed to update ${SETTINGS_PATH}: ${err.message}`);
105
+ }
106
+ }
107
+ function deepMerge(target, source) {
108
+ const out = { ...target };
109
+ for (const [key, val] of Object.entries(source)) {
110
+ const existing = out[key];
111
+ if (val !== null && typeof val === "object" && !Array.isArray(val) &&
112
+ existing !== null && typeof existing === "object" && !Array.isArray(existing)) {
113
+ out[key] = deepMerge(existing, val);
114
+ }
115
+ else {
116
+ out[key] = val;
117
+ }
118
+ }
119
+ return out;
120
+ }
77
121
  /**
78
122
  * Expand $ENV_VAR references in a string.
79
123
  * Supports $VAR and ${VAR} syntax.
@@ -28,7 +28,8 @@ export declare class InputHandler {
28
28
  private history;
29
29
  private historyIndex;
30
30
  private savedBuffer;
31
- private promptWrappedLines;
31
+ private cursorRowsBelow;
32
+ private cursorTermCol;
32
33
  private escapeTimer;
33
34
  private bus;
34
35
  private onShowAgentInfo;
@@ -4,7 +4,7 @@ import { visibleLen } from "../utils/ansi.js";
4
4
  import { palette as p } from "../utils/palette.js";
5
5
  import { LineEditor } from "../utils/line-editor.js";
6
6
  import { CONFIG_DIR, getSettings } from "../settings.js";
7
- const HISTORY_FILE = path.join(CONFIG_DIR, "history");
7
+ const HISTORY_FILE = path.join(CONFIG_DIR, "input-history");
8
8
  export class InputHandler {
9
9
  ctx;
10
10
  lineBuffer = "";
@@ -20,7 +20,8 @@ export class InputHandler {
20
20
  history = [];
21
21
  historyIndex = -1; // -1 = not browsing history
22
22
  savedBuffer = ""; // buffer saved when entering history
23
- promptWrappedLines = 0; // extra lines from terminal wrapping
23
+ cursorRowsBelow = 0; // rows from prompt top to cursor row
24
+ cursorTermCol = 1; // 1-indexed terminal column of cursor
24
25
  escapeTimer = null;
25
26
  bus;
26
27
  onShowAgentInfo;
@@ -72,9 +73,10 @@ export class InputHandler {
72
73
  /** Write the mode prompt line with cursor at the correct position. */
73
74
  writeModePromptLine(showBuffer = true) {
74
75
  const termW = process.stdout.columns || 80;
75
- // Move cursor to the start of the prompt area (first line of wrapped content)
76
- if (this.promptWrappedLines > 0) {
77
- process.stdout.write(`\x1b[${this.promptWrappedLines}A`);
76
+ // Move cursor to the start of the prompt area.
77
+ // We know exactly how many rows below the top the cursor currently sits.
78
+ if (this.cursorRowsBelow > 0) {
79
+ process.stdout.write(`\x1b[${this.cursorRowsBelow}A`);
78
80
  }
79
81
  // Clear from here to end of screen — removes current + all wrapped lines below
80
82
  process.stdout.write("\r\x1b[J");
@@ -88,37 +90,36 @@ export class InputHandler {
88
90
  const promptVisLen = visibleLen(infoPrefix) + visibleLen(icon) + 1; // icon + space
89
91
  const display = showBuffer ? this.editor.displayText : "";
90
92
  const dCursor = showBuffer ? this.editor.displayCursor : 0;
91
- if (!showBuffer || !display.includes("\n")) {
92
- // Single-line: simple rendering
93
- const bufferText = showBuffer ? p.accent + display + p.reset : "";
94
- process.stdout.write(promptPrefix + bufferText);
95
- const bufferVisLen = display.length;
96
- const totalVisLen = promptVisLen + bufferVisLen;
97
- this.promptWrappedLines = totalVisLen > 0 ? Math.floor((totalVisLen - 1) / termW) : 0;
98
- // Position cursor within the buffer
99
- if (showBuffer && dCursor < display.length) {
100
- const charsAfterCursor = display.length - dCursor;
101
- process.stdout.write(`\x1b[${charsAfterCursor}D`);
102
- }
93
+ if (!showBuffer) {
94
+ // No buffer — just write the prompt prefix, cursor stays at end
95
+ process.stdout.write(promptPrefix);
96
+ const N = promptVisLen;
97
+ this.cursorRowsBelow = N > 0 ? Math.ceil(N / termW) - 1 : 0;
98
+ this.cursorTermCol = N === 0 ? 1 : (N % termW === 0 ? termW : (N % termW) + 1);
99
+ }
100
+ else if (!display.includes("\n")) {
101
+ // Single-line: write up to cursor, save, write rest, restore.
102
+ // The terminal handles all wrapping — no manual row/col math needed.
103
+ const before = display.slice(0, dCursor);
104
+ const after = display.slice(dCursor);
105
+ process.stdout.write(promptPrefix + p.accent + before + p.reset +
106
+ "\x1b7" + // DECSC — save cursor position
107
+ p.accent + after + p.reset +
108
+ "\x1b8" // DECRC — restore cursor position
109
+ );
110
+ // Clearing on next redraw needs total rows, so measure the full
111
+ // content width — not just up to the cursor.
112
+ const totalVisLen = promptVisLen + visibleLen(display);
113
+ this.cursorRowsBelow = totalVisLen > 0 ? Math.ceil(totalVisLen / termW) - 1 : 0;
114
+ const cursorVisCol = promptVisLen + visibleLen(before);
115
+ this.cursorTermCol = cursorVisCol === 0 ? 1 : (cursorVisCol % termW === 0 ? termW : (cursorVisCol % termW) + 1);
103
116
  }
104
117
  else {
105
- // Multi-line: render each line with continuation indent
118
+ // Multi-line: render each line with continuation indent.
119
+ // Same save/restore strategy — cursor position is never computed.
106
120
  const lines = display.split("\n");
107
121
  const indent = " ".repeat(promptVisLen);
108
- let totalTermLines = 0;
109
- for (let li = 0; li < lines.length; li++) {
110
- const prefix = li === 0 ? promptPrefix : indent;
111
- const prefixVisLen = li === 0 ? promptVisLen : promptVisLen;
112
- const lineText = lines[li];
113
- process.stdout.write(prefix + p.accent + lineText + p.reset);
114
- if (li < lines.length - 1)
115
- process.stdout.write("\n");
116
- // Count terminal lines this logical line occupies
117
- const lineVisLen = prefixVisLen + lineText.length;
118
- totalTermLines += lineVisLen > 0 ? Math.ceil(lineVisLen / termW) : 1;
119
- }
120
- this.promptWrappedLines = totalTermLines - 1;
121
- // Position cursor: find which display line and column the cursor is on
122
+ // Locate cursor: which logical line and offset within it.
122
123
  let charsRemaining = dCursor;
123
124
  let cursorLine = 0;
124
125
  for (let li = 0; li < lines.length; li++) {
@@ -129,31 +130,35 @@ export class InputHandler {
129
130
  charsRemaining -= lines[li].length + 1; // +1 for \n
130
131
  cursorLine = li + 1;
131
132
  }
132
- // Compute terminal rows for cursor positioning (not logical lines)
133
- // Each logical line may wrap across multiple terminal rows.
134
- const cursorColAbs = promptVisLen + charsRemaining;
135
- const cursorTermRow = Math.floor(cursorColAbs / termW);
136
- // Count terminal rows occupied by lines after cursor's logical line
137
- let termRowsAfterCursor = 0;
138
- for (let li = cursorLine + 1; li < lines.length; li++) {
139
- const lineVisLen = promptVisLen + lines[li].length;
140
- termRowsAfterCursor += lineVisLen > 0 ? Math.ceil(lineVisLen / termW) : 1;
141
- }
142
- // Also count remaining terminal rows on cursor's own logical line
143
- const cursorLineVisLen = promptVisLen + lines[cursorLine].length;
144
- const cursorLineTotalRows = cursorLineVisLen > 0 ? Math.ceil(cursorLineVisLen / termW) : 1;
145
- const rowsAfterCursorInLine = cursorLineTotalRows - 1 - cursorTermRow;
146
- const totalRowsFromEnd = termRowsAfterCursor + rowsAfterCursorInLine;
147
- if (totalRowsFromEnd > 0) {
148
- process.stdout.write(`\x1b[${totalRowsFromEnd}A`);
149
- }
150
- const cursorCol = cursorColAbs % termW;
151
- if (cursorCol > 0) {
152
- process.stdout.write(`\r\x1b[${cursorCol}C`);
153
- }
154
- else {
155
- process.stdout.write(`\r`);
133
+ let output = "";
134
+ let cursorRowFromTop = 0;
135
+ let rowsSoFar = 0;
136
+ for (let li = 0; li < lines.length; li++) {
137
+ const prefix = li === 0 ? promptPrefix : indent;
138
+ const lineText = lines[li];
139
+ const lineVisLen = promptVisLen + visibleLen(lineText);
140
+ const lineTermRows = lineVisLen > 0 ? Math.ceil(lineVisLen / termW) : 1;
141
+ if (li === cursorLine) {
142
+ // Split this line at the cursor.
143
+ const before = lineText.slice(0, charsRemaining);
144
+ const after = lineText.slice(charsRemaining);
145
+ output += prefix + p.accent + before + p.reset;
146
+ output += "\x1b7"; // DECSC save cursor position
147
+ output += p.accent + after + p.reset;
148
+ const beforeVisCol = promptVisLen + visibleLen(before);
149
+ cursorRowFromTop = rowsSoFar + (beforeVisCol > 0 ? Math.ceil(beforeVisCol / termW) - 1 : 0);
150
+ this.cursorTermCol = beforeVisCol === 0 ? 1 : (beforeVisCol % termW === 0 ? termW : (beforeVisCol % termW) + 1);
151
+ }
152
+ else {
153
+ output += prefix + p.accent + lineText + p.reset;
154
+ }
155
+ if (li < lines.length - 1)
156
+ output += "\n";
157
+ rowsSoFar += lineTermRows;
156
158
  }
159
+ process.stdout.write(output + "\x1b8"); // DECRC — restore cursor position
160
+ // Total rows (not cursor row) so next redraw clears the whole area.
161
+ this.cursorRowsBelow = rowsSoFar - 1 > 0 ? rowsSoFar - 1 : 0;
157
162
  }
158
163
  }
159
164
  handleInput(data) {
@@ -281,15 +286,17 @@ export class InputHandler {
281
286
  // Disable kitty keyboard protocol and bracket paste mode
282
287
  process.stdout.write("\x1b[<u\x1b[?2004l");
283
288
  this.clearPromptArea();
289
+ this.cursorRowsBelow = 0;
290
+ this.cursorTermCol = 1;
284
291
  this.printPrompt();
285
292
  }
286
293
  /** Move to the start of the prompt area and clear everything below. */
287
294
  clearPromptArea() {
288
- if (this.promptWrappedLines > 0) {
289
- process.stdout.write(`\x1b[${this.promptWrappedLines}A`);
295
+ if (this.cursorRowsBelow > 0) {
296
+ process.stdout.write(`\x1b[${this.cursorRowsBelow}A`);
290
297
  }
291
298
  process.stdout.write("\r\x1b[J");
292
- this.promptWrappedLines = 0;
299
+ this.cursorRowsBelow = 0;
293
300
  }
294
301
  printPrompt() {
295
302
  this.ctx.redrawPrompt();
@@ -363,16 +370,10 @@ export class InputHandler {
363
370
  if (this.autocompleteLines > 0) {
364
371
  process.stdout.write(`\x1b[${this.autocompleteLines}A`);
365
372
  }
366
- // Reposition cursor: must match the layout in writeModePromptLine()
367
- const agentInfo = this.onShowAgentInfo();
368
- const indicator = this.activeMode?.indicator ?? "●";
369
- const infoPrefix = agentInfo.info
370
- ? `${agentInfo.info} ${indicator} `
371
- : `${indicator} `;
372
- const icon = this.activeMode?.promptIcon ?? "❯";
373
- const promptVisLen = visibleLen(infoPrefix) + visibleLen(icon) + 1;
374
- const col = promptVisLen + this.editor.displayCursor;
375
- process.stdout.write(`\r\x1b[${col}C`);
373
+ // Restore cursor column use explicit column set instead of DECRC
374
+ // because writing \n above may have scrolled the terminal, which
375
+ // invalidates the absolute position saved by DECSC.
376
+ process.stdout.write(`\x1b[${this.cursorTermCol}G`);
376
377
  }
377
378
  applyAutocomplete() {
378
379
  if (!this.autocompleteActive || this.autocompleteItems.length === 0)
@@ -407,11 +408,12 @@ export class InputHandler {
407
408
  clearAutocompleteLines() {
408
409
  if (this.autocompleteLines <= 0)
409
410
  return;
410
- process.stdout.write("\x1b7"); // save cursor
411
+ // Use CSI B (cursor down, bounded) instead of \n to avoid scroll
411
412
  for (let i = 0; i < this.autocompleteLines; i++) {
412
- process.stdout.write("\n\x1b[2K"); // move down, clear line
413
+ process.stdout.write("\x1b[B\x1b[2K"); // move down, clear line
413
414
  }
414
- process.stdout.write("\x1b8"); // restore cursor
415
+ // Move back up and restore column with relative movement (scroll-safe)
416
+ process.stdout.write(`\x1b[${this.autocompleteLines}A\x1b[${this.cursorTermCol}G`);
415
417
  this.autocompleteLines = 0;
416
418
  }
417
419
  handleModeInput(data) {
@@ -475,13 +477,20 @@ export class InputHandler {
475
477
  const currentMode = this.activeMode;
476
478
  this.activeMode = null;
477
479
  this.editor.clear();
480
+ this.cursorRowsBelow = 0;
481
+ this.cursorTermCol = 1;
478
482
  this.dismissAutocomplete();
479
483
  if (query && query.startsWith("/")) {
480
484
  const spaceIdx = query.indexOf(" ");
481
485
  const name = spaceIdx === -1 ? query : query.slice(0, spaceIdx);
482
486
  const args = spaceIdx === -1 ? "" : query.slice(spaceIdx + 1).trim();
483
487
  this.bus.emit("command:execute", { name, args });
484
- this.ctx.freshPrompt();
488
+ if (currentMode.returnToSelf) {
489
+ this.enterMode(currentMode);
490
+ }
491
+ else {
492
+ this.ctx.freshPrompt();
493
+ }
485
494
  }
486
495
  else if (query) {
487
496
  this.pendingReturnMode = currentMode.returnToSelf ? currentMode.id : null;
@@ -197,6 +197,11 @@ export class Shell {
197
197
  * For bash, falls back to sending \n for a fresh prompt cycle.
198
198
  */
199
199
  redrawPrompt() {
200
+ // A stale echoSkip or paused flag (left over from handleProcessingDone
201
+ // re-entering a mode) would swallow the redrawn prompt and make the
202
+ // terminal appear frozen. Reset both before emitting.
203
+ this.echoSkip = false;
204
+ this.paused = false;
200
205
  const result = this.bus.emitPipe("shell:redraw-prompt", {
201
206
  cwd: this.outputParser.getCwd(),
202
207
  handled: false,
@@ -277,9 +282,13 @@ export class Shell {
277
282
  this.paused = true;
278
283
  });
279
284
  this.handlers.define("shell:on-processing-done", () => {
280
- this.paused = false;
281
285
  this.agentActive = false;
286
+ // If handleProcessingDone re-entered a mode, leave stdout paused so
287
+ // stale PTY output doesn't overwrite the mode prompt (exitMode →
288
+ // redrawPrompt will unpause). Setting echoSkip here would swallow
289
+ // that PTY output since no \n was sent.
282
290
  if (!this.inputHandler.handleProcessingDone()) {
291
+ this.paused = false;
283
292
  if (this.freshPrompt()) {
284
293
  this.echoSkip = true;
285
294
  }
@@ -317,11 +326,19 @@ export class Shell {
317
326
  const handler = (e) => {
318
327
  clearTimeout(timeout);
319
328
  this.bus.off("shell:command-done", handler);
329
+ // Re-pause stdout so the prompt text following the marker doesn't
330
+ // leak to the terminal while the agent is still processing.
331
+ this.paused = true;
320
332
  resolve({ output: e.output, cwd: e.cwd, exitCode: e.exitCode });
321
333
  };
322
334
  this.bus.on("shell:command-done", handler);
323
335
  this.outputParser.onCommandEntered(payload.command, this.outputParser.getCwd());
324
- this.ptyProcess.write(payload.command + "\r");
336
+ // Collapse literal newlines to spaces so the PTY receives a single-line
337
+ // command. Multi-line commands (e.g. git commit -m "...\n...") would
338
+ // cause the shell to execute prematurely, producing garbled output from
339
+ // syntax highlighting plugins (zsh syntax highlighting, etc).
340
+ const oneLine = payload.command.replace(/\n/g, " ");
341
+ this.ptyProcess.write(oneLine + "\r");
325
342
  });
326
343
  this.paused = true;
327
344
  this.echoSkip = false;
package/dist/types.d.ts CHANGED
@@ -80,6 +80,12 @@ export interface ExtensionContext {
80
80
  createFencedBlockTransform: (opts: FencedBlockTransformOptions) => void;
81
81
  /** Read extension-namespaced settings from ~/.agent-sh/settings.json. */
82
82
  getExtensionSettings: <T extends Record<string, unknown>>(namespace: string, defaults: T) => T;
83
+ /**
84
+ * Get (and lazily create) a per-extension storage directory under
85
+ * ~/.agent-sh/<namespace>/. Returns the absolute path. Lets extensions
86
+ * persist state without each one re-deriving the location.
87
+ */
88
+ getStoragePath: (namespace: string) => string;
83
89
  /** Register a slash command available in any input mode. */
84
90
  registerCommand: (name: string, description: string, handler: (args: string) => Promise<void> | void) => void;
85
91
  /** Register a tool for the built-in agent. No-op when using bridge backends. */
@@ -92,12 +98,18 @@ export interface ExtensionContext {
92
98
  registerInstruction: (name: string, text: string) => void;
93
99
  /** Remove a named instruction block from the system prompt. */
94
100
  removeInstruction: (name: string) => void;
101
+ /** Register a skill (on-demand reference material) for the agent. */
102
+ registerSkill: (name: string, description: string, filePath: string) => void;
103
+ /** Remove a registered skill by name. */
104
+ removeSkill: (name: string) => void;
95
105
  /** Register a named handler. */
96
106
  define: (name: string, fn: (...args: any[]) => any) => void;
97
107
  /** Wrap a named handler. Receives `next` (original) + args. Returns an unadvise function. */
98
108
  advise: (name: string, wrapper: (next: (...args: any[]) => any, ...args: any[]) => any) => () => void;
99
109
  /** Call a named handler. */
100
110
  call: (name: string, ...args: any[]) => any;
111
+ /** Names of all registered handlers — for diagnostic / introspection use. */
112
+ list: () => string[];
101
113
  /**
102
114
  * Shared headless terminal buffer mirroring PTY output.
103
115
  * Lazily created on first access. Returns null if @xterm/headless is not installed.
@@ -6,6 +6,11 @@ export declare const RED = "\u001B[31m";
6
6
  export declare const GRAY = "\u001B[90m";
7
7
  export declare const BOLD = "\u001B[1m";
8
8
  export declare const RESET = "\u001B[0m";
9
+ /**
10
+ * Check if a Unicode code point is a wide character (CJK, fullwidth, emoji, etc.)
11
+ * Returns 2 for wide chars, 1 for normal chars.
12
+ */
13
+ export declare function charWidth(codePoint: number): number;
9
14
  /**
10
15
  * Measure visible string length in terminal columns.
11
16
  * Excludes SGR (color/style) sequences and accounts for CJK double-width chars.
@@ -12,7 +12,7 @@ export const RESET = "\x1b[0m";
12
12
  * Check if a Unicode code point is a wide character (CJK, fullwidth, emoji, etc.)
13
13
  * Returns 2 for wide chars, 1 for normal chars.
14
14
  */
15
- function charWidth(codePoint) {
15
+ export function charWidth(codePoint) {
16
16
  // CJK Unified Ideographs
17
17
  if (codePoint >= 0x4e00 && codePoint <= 0x9fff)
18
18
  return 2;
@@ -24,6 +24,7 @@
24
24
  * compositor.redirect("agent:diff", diffPanelSurface);
25
25
  * // "agent:text", "agent:tool" etc. still go to stdout
26
26
  */
27
+ import type { EventBus } from "../event-bus.js";
27
28
  /**
28
29
  * A surface accepts rendered output. Stdout is a surface.
29
30
  * A floating panel's content area is a surface. A test buffer is a surface.
@@ -56,7 +57,11 @@ export declare class StdoutSurface implements RenderSurface {
56
57
  export declare class DefaultCompositor implements Compositor {
57
58
  private defaults;
58
59
  private overrides;
60
+ private readonly bus?;
61
+ constructor(bus?: EventBus);
59
62
  surface(stream: string): RenderSurface;
60
63
  redirect(stream: string, target: RenderSurface): () => void;
61
64
  setDefault(stream: string, target: RenderSurface): void;
65
+ /** Wrap a surface so writes emit `compositor:write` before delegating. */
66
+ private wrap;
62
67
  }
@@ -50,6 +50,10 @@ export class StdoutSurface {
50
50
  export class DefaultCompositor {
51
51
  defaults = new Map();
52
52
  overrides = new Map();
53
+ bus;
54
+ constructor(bus) {
55
+ this.bus = bus;
56
+ }
53
57
  surface(stream) {
54
58
  const stack = this.overrides.get(stream);
55
59
  if (stack && stack.length > 0)
@@ -63,12 +67,13 @@ export class DefaultCompositor {
63
67
  return nullSurface;
64
68
  }
65
69
  redirect(stream, target) {
70
+ const wrapped = this.wrap(stream, target);
66
71
  let stack = this.overrides.get(stream);
67
72
  if (!stack) {
68
73
  stack = [];
69
74
  this.overrides.set(stream, stack);
70
75
  }
71
- stack.push(target);
76
+ stack.push(wrapped);
72
77
  let restored = false;
73
78
  return () => {
74
79
  if (restored)
@@ -77,12 +82,35 @@ export class DefaultCompositor {
77
82
  const s = this.overrides.get(stream);
78
83
  if (!s)
79
84
  return;
80
- const idx = s.indexOf(target);
85
+ const idx = s.indexOf(wrapped);
81
86
  if (idx !== -1)
82
87
  s.splice(idx, 1);
83
88
  };
84
89
  }
85
90
  setDefault(stream, target) {
86
- this.defaults.set(stream, target);
91
+ this.defaults.set(stream, this.wrap(stream, target));
92
+ }
93
+ /** Wrap a surface so writes emit `compositor:write` before delegating. */
94
+ wrap(stream, target) {
95
+ const bus = this.bus;
96
+ if (!bus)
97
+ return target;
98
+ return {
99
+ write: (text) => {
100
+ try {
101
+ bus.emit("compositor:write", { stream, text });
102
+ }
103
+ catch { }
104
+ target.write(text);
105
+ },
106
+ writeLine: (line) => {
107
+ try {
108
+ bus.emit("compositor:write", { stream, text: line + "\n" });
109
+ }
110
+ catch { }
111
+ target.writeLine(line);
112
+ },
113
+ get columns() { return target.columns; },
114
+ };
87
115
  }
88
116
  }
@@ -18,3 +18,12 @@ export interface DiffRenderOptions {
18
18
  export declare function selectMode(width: number): DiffDisplayMode;
19
19
  /** Render a diff result as an array of ANSI-formatted terminal lines. */
20
20
  export declare function renderDiff(diff: DiffResult, opts: DiffRenderOptions): string[];
21
+ /**
22
+ * Async variant of renderDiff that yields to the event loop between hunks.
23
+ * Use when rendering in a context where a spinner or other UI needs to stay
24
+ * responsive (e.g. showing a large diff during a permission prompt).
25
+ *
26
+ * @param onLines - Callback invoked with each batch of rendered lines as they
27
+ * are produced. Allows progressive/streaming display.
28
+ */
29
+ export declare function renderDiffAsync(diff: DiffResult, opts: DiffRenderOptions, onLines: (lines: string[]) => void): Promise<void>;