agent-sh 0.8.0 → 0.9.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 (74) hide show
  1. package/README.md +25 -34
  2. package/dist/agent/agent-loop.d.ts +29 -6
  3. package/dist/agent/agent-loop.js +177 -59
  4. package/dist/agent/conversation-state.d.ts +3 -1
  5. package/dist/agent/conversation-state.js +6 -2
  6. package/dist/agent/nuclear-form.js +5 -4
  7. package/dist/agent/system-prompt.d.ts +4 -5
  8. package/dist/agent/system-prompt.js +12 -28
  9. package/dist/{token-budget.js → agent/token-budget.js} +1 -1
  10. package/dist/agent/tool-protocol.d.ts +83 -0
  11. package/dist/agent/tool-protocol.js +386 -0
  12. package/dist/agent/types.d.ts +21 -1
  13. package/dist/core.d.ts +7 -7
  14. package/dist/core.js +76 -194
  15. package/dist/event-bus.d.ts +26 -0
  16. package/dist/event-bus.js +20 -1
  17. package/dist/extension-loader.d.ts +5 -0
  18. package/dist/extension-loader.js +104 -17
  19. package/dist/extensions/agent-backend.d.ts +13 -0
  20. package/dist/extensions/agent-backend.js +167 -0
  21. package/dist/extensions/command-suggest.d.ts +3 -3
  22. package/dist/extensions/command-suggest.js +4 -3
  23. package/dist/extensions/index.d.ts +19 -0
  24. package/dist/extensions/index.js +25 -0
  25. package/dist/extensions/slash-commands.d.ts +1 -1
  26. package/dist/extensions/slash-commands.js +16 -1
  27. package/dist/extensions/terminal-buffer.d.ts +1 -1
  28. package/dist/extensions/terminal-buffer.js +13 -4
  29. package/dist/extensions/tui-renderer.js +63 -43
  30. package/dist/index.js +14 -20
  31. package/dist/settings.d.ts +6 -0
  32. package/dist/settings.js +4 -1
  33. package/dist/{input-handler.d.ts → shell/input-handler.d.ts} +1 -1
  34. package/dist/{input-handler.js → shell/input-handler.js} +60 -43
  35. package/dist/{output-parser.d.ts → shell/output-parser.d.ts} +1 -1
  36. package/dist/{output-parser.js → shell/output-parser.js} +1 -1
  37. package/dist/{shell.d.ts → shell/shell.d.ts} +8 -2
  38. package/dist/{shell.js → shell/shell.js} +20 -6
  39. package/dist/types.d.ts +49 -10
  40. package/dist/utils/compositor.d.ts +62 -0
  41. package/dist/utils/compositor.js +88 -0
  42. package/dist/utils/diff-renderer.js +92 -4
  43. package/dist/utils/floating-panel.d.ts +2 -0
  44. package/dist/utils/floating-panel.js +30 -14
  45. package/dist/utils/handler-registry.d.ts +26 -10
  46. package/dist/utils/handler-registry.js +52 -16
  47. package/dist/utils/line-editor.d.ts +23 -3
  48. package/dist/utils/line-editor.js +180 -42
  49. package/dist/utils/markdown.d.ts +1 -0
  50. package/dist/utils/markdown.js +1 -1
  51. package/dist/utils/message-utils.d.ts +35 -0
  52. package/dist/utils/message-utils.js +75 -0
  53. package/dist/utils/terminal-buffer.d.ts +5 -1
  54. package/dist/utils/terminal-buffer.js +18 -2
  55. package/dist/utils/tool-interactive.d.ts +12 -0
  56. package/dist/utils/tool-interactive.js +53 -0
  57. package/examples/extensions/ash-acp-bridge/README.md +39 -0
  58. package/examples/extensions/ash-acp-bridge/package.json +23 -0
  59. package/examples/extensions/ash-acp-bridge/src/index.ts +571 -0
  60. package/examples/extensions/ash-acp-bridge/tsconfig.json +14 -0
  61. package/examples/extensions/ash-mcp-bridge/README.md +72 -0
  62. package/examples/extensions/ash-mcp-bridge/index.ts +154 -0
  63. package/examples/extensions/ash-mcp-bridge/package.json +9 -0
  64. package/examples/extensions/interactive-prompts.ts +82 -110
  65. package/examples/extensions/overlay-agent.ts +84 -38
  66. package/examples/extensions/peer-mesh.ts +450 -0
  67. package/examples/extensions/questionnaire.ts +249 -0
  68. package/examples/extensions/tmux-pane.ts +307 -0
  69. package/examples/extensions/web-access.ts +327 -0
  70. package/package.json +9 -1
  71. package/dist/extensions/overlay-agent.d.ts +0 -14
  72. package/dist/extensions/overlay-agent.js +0 -147
  73. package/examples/extensions/terminal-buffer.ts +0 -184
  74. /package/dist/{token-budget.d.ts → agent/token-budget.d.ts} +0 -0
package/dist/index.js CHANGED
@@ -1,16 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import { spawn } from "node:child_process";
3
3
  import * as path from "node:path";
4
- import { Shell } from "./shell.js";
4
+ import { Shell } from "./shell/shell.js";
5
5
  import { createCore } from "./core.js";
6
6
  import { palette as p } from "./utils/palette.js";
7
- import tuiRenderer from "./extensions/tui-renderer.js";
8
- import slashCommands from "./extensions/slash-commands.js";
9
- import fileAutocomplete from "./extensions/file-autocomplete.js";
10
- import shellRecall from "./extensions/shell-recall.js";
11
- import commandSuggest from "./extensions/command-suggest.js";
12
- import terminalBuffer from "./extensions/terminal-buffer.js";
13
- import overlayAgent from "./extensions/overlay-agent.js";
7
+ import { loadBuiltinExtensions } from "./extensions/index.js";
14
8
  import { loadExtensions } from "./extension-loader.js";
15
9
  import { getSettings } from "./settings.js";
16
10
  import { discoverSkills } from "./agent/skills.js";
@@ -160,6 +154,13 @@ async function main() {
160
154
  const shellEnv = await captureShellEnvAsync(shellPath);
161
155
  if (Object.keys(shellEnv).length > 0) {
162
156
  Object.assign(baseEnv, mergeShellEnv(baseEnv, shellEnv));
157
+ // Expose captured env vars to process.env so extensions can read them.
158
+ // Only add vars not already present to avoid clobbering runtime state.
159
+ for (const [k, v] of Object.entries(baseEnv)) {
160
+ if (process.env[k] === undefined) {
161
+ process.env[k] = v;
162
+ }
163
+ }
163
164
  if (process.env.DEBUG) {
164
165
  console.error('[agent-sh] Shell environment captured');
165
166
  }
@@ -195,6 +196,7 @@ async function main() {
195
196
  await new Promise(resolve => setTimeout(resolve, 100));
196
197
  const shell = new Shell({
197
198
  bus,
199
+ handlers: core.handlers,
198
200
  cols,
199
201
  rows,
200
202
  shell: config.shell || process.env.SHELL || "/bin/bash",
@@ -203,9 +205,6 @@ async function main() {
203
205
  if (agentInfo) {
204
206
  return { info: `${p.dim}${agentInfo.name}${agentInfo.model ? ` (${agentInfo.model})` : ""}${p.reset}` };
205
207
  }
206
- if (core.llmClient) {
207
- return { info: `${p.dim}agent-sh (${core.llmClient.model})${p.reset}` };
208
- }
209
208
  return { info: "" };
210
209
  },
211
210
  });
@@ -229,13 +228,8 @@ async function main() {
229
228
  console.error('[agent-sh] Setting up extensions...');
230
229
  }
231
230
  const extCtx = core.extensionContext({ quit: cleanup });
232
- tuiRenderer(extCtx);
233
- slashCommands(extCtx);
234
- fileAutocomplete(extCtx);
235
- shellRecall(extCtx);
236
- commandSuggest(extCtx);
237
- terminalBuffer(extCtx);
238
- overlayAgent(extCtx);
231
+ // Load built-in extensions (individually disableable via settings.disabledBuiltins)
232
+ await loadBuiltinExtensions(extCtx, getSettings().disabledBuiltins);
239
233
  // Load user extensions (may register alternative agent backends)
240
234
  if (process.env.DEBUG) {
241
235
  console.error('[agent-sh] Loading extensions...');
@@ -264,8 +258,8 @@ async function main() {
264
258
  const bannerW = Math.min(termW, 60);
265
259
  const productName = `${p.accent}${p.bold}agent-sh${p.reset}`;
266
260
  const info = agentInfo;
267
- const backendName = info?.name ?? "agent-sh";
268
- const model = info?.model ?? core.llmClient?.model;
261
+ const backendName = info?.name ?? "ash";
262
+ const model = info?.model;
269
263
  const provider = info?.provider;
270
264
  const modelValue = model
271
265
  ? provider ? `${model} [${provider}]` : model
@@ -52,18 +52,24 @@ export interface Settings {
52
52
  historyStartupEntries?: number;
53
53
  /** Max nuclear entries kept in-context before flushing to history file (default: 200). */
54
54
  nuclearMaxEntries?: number;
55
+ /** Auto-compact threshold as fraction of conversation budget (0-1, default 0.5). */
56
+ autoCompactThreshold?: number;
55
57
  /** Max command output lines shown inline in TUI. */
56
58
  maxCommandOutputLines?: number;
57
59
  /** Max read tool output lines shown inline in TUI (0 = hide). */
58
60
  readOutputMaxLines?: number;
59
61
  /** Max diff lines shown before "ctrl+o to expand". */
60
62
  diffMaxLines?: number;
63
+ /** Tool protocol: "api" (all tools), "deferred" (extensions via meta-tool), "inline" (text). */
64
+ toolMode?: "api" | "deferred" | "inline";
61
65
  /** Additional directories to scan for skills (supports ~ expansion). */
62
66
  skillPaths?: string[];
63
67
  /** Show a startup banner when agent-sh launches. */
64
68
  startupBanner?: boolean;
65
69
  /** Show a subtle agent-sh indicator in the shell prompt. */
66
70
  promptIndicator?: boolean;
71
+ /** Names of built-in extensions to disable (e.g. ["command-suggest"]). */
72
+ disabledBuiltins?: string[];
67
73
  }
68
74
  declare const DEFAULTS: Required<Settings>;
69
75
  /** Load settings from disk (cached after first call). */
package/dist/settings.js CHANGED
@@ -14,7 +14,8 @@ const DEFAULTS = {
14
14
  historySize: 500,
15
15
  providers: {},
16
16
  defaultProvider: undefined,
17
- defaultBackend: "agent-sh",
17
+ defaultBackend: "ash",
18
+ toolMode: "api",
18
19
  contextWindowSize: 20,
19
20
  contextBudget: 16384,
20
21
  shellTruncateThreshold: 10,
@@ -25,12 +26,14 @@ const DEFAULTS = {
25
26
  historyMaxBytes: 102400,
26
27
  historyStartupEntries: 50,
27
28
  nuclearMaxEntries: 200,
29
+ autoCompactThreshold: 0.5,
28
30
  maxCommandOutputLines: 3,
29
31
  readOutputMaxLines: 10,
30
32
  diffMaxLines: 20,
31
33
  skillPaths: [],
32
34
  startupBanner: true,
33
35
  promptIndicator: true,
36
+ disabledBuiltins: [],
34
37
  };
35
38
  let cached = null;
36
39
  /** Load settings from disk (cached after first call). */
@@ -1,4 +1,4 @@
1
- import type { EventBus } from "./event-bus.js";
1
+ import type { EventBus } from "../event-bus.js";
2
2
  /**
3
3
  * Narrow contract between InputHandler and its host (Shell).
4
4
  * InputHandler never touches the PTY or EventBus directly —
@@ -1,9 +1,9 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
- import { visibleLen } from "./utils/ansi.js";
4
- import { palette as p } from "./utils/palette.js";
5
- import { LineEditor } from "./utils/line-editor.js";
6
- import { CONFIG_DIR, getSettings } from "./settings.js";
3
+ import { visibleLen } from "../utils/ansi.js";
4
+ import { palette as p } from "../utils/palette.js";
5
+ import { LineEditor } from "../utils/line-editor.js";
6
+ import { CONFIG_DIR, getSettings } from "../settings.js";
7
7
  const HISTORY_FILE = path.join(CONFIG_DIR, "history");
8
8
  export class InputHandler {
9
9
  ctx;
@@ -86,22 +86,24 @@ export class InputHandler {
86
86
  const icon = this.activeMode?.promptIcon ?? "❯";
87
87
  const promptPrefix = infoPrefix + p.warning + p.bold + icon + " " + p.reset;
88
88
  const promptVisLen = visibleLen(infoPrefix) + visibleLen(icon) + 1; // icon + space
89
- if (!showBuffer || !this.editor.buffer.includes("\n")) {
89
+ const display = showBuffer ? this.editor.displayText : "";
90
+ const dCursor = showBuffer ? this.editor.displayCursor : 0;
91
+ if (!showBuffer || !display.includes("\n")) {
90
92
  // Single-line: simple rendering
91
- const bufferText = showBuffer ? p.accent + this.editor.buffer + p.reset : "";
93
+ const bufferText = showBuffer ? p.accent + display + p.reset : "";
92
94
  process.stdout.write(promptPrefix + bufferText);
93
- const bufferVisLen = showBuffer ? this.editor.buffer.length : 0;
95
+ const bufferVisLen = display.length;
94
96
  const totalVisLen = promptVisLen + bufferVisLen;
95
97
  this.promptWrappedLines = totalVisLen > 0 ? Math.floor((totalVisLen - 1) / termW) : 0;
96
98
  // Position cursor within the buffer
97
- if (showBuffer && this.editor.cursor < this.editor.buffer.length) {
98
- const charsAfterCursor = this.editor.buffer.length - this.editor.cursor;
99
+ if (showBuffer && dCursor < display.length) {
100
+ const charsAfterCursor = display.length - dCursor;
99
101
  process.stdout.write(`\x1b[${charsAfterCursor}D`);
100
102
  }
101
103
  }
102
104
  else {
103
105
  // Multi-line: render each line with continuation indent
104
- const lines = this.editor.buffer.split("\n");
106
+ const lines = display.split("\n");
105
107
  const indent = " ".repeat(promptVisLen);
106
108
  let totalTermLines = 0;
107
109
  for (let li = 0; li < lines.length; li++) {
@@ -116,8 +118,8 @@ export class InputHandler {
116
118
  totalTermLines += lineVisLen > 0 ? Math.ceil(lineVisLen / termW) : 1;
117
119
  }
118
120
  this.promptWrappedLines = totalTermLines - 1;
119
- // Position cursor: find which line and column the cursor is on
120
- let charsRemaining = this.editor.cursor;
121
+ // Position cursor: find which display line and column the cursor is on
122
+ let charsRemaining = dCursor;
121
123
  let cursorLine = 0;
122
124
  for (let li = 0; li < lines.length; li++) {
123
125
  if (charsRemaining <= lines[li].length) {
@@ -127,13 +129,31 @@ export class InputHandler {
127
129
  charsRemaining -= lines[li].length + 1; // +1 for \n
128
130
  cursorLine = li + 1;
129
131
  }
130
- // Move from end position to cursor position
131
- const linesFromEnd = lines.length - 1 - cursorLine;
132
- if (linesFromEnd > 0) {
133
- process.stdout.write(`\x1b[${linesFromEnd}A`);
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`);
134
156
  }
135
- const cursorCol = (cursorLine === 0 ? promptVisLen : promptVisLen) + charsRemaining;
136
- process.stdout.write(`\r\x1b[${cursorCol}C`);
137
157
  }
138
158
  }
139
159
  handleInput(data) {
@@ -249,16 +269,17 @@ export class InputHandler {
249
269
  this.activeMode = mode;
250
270
  this.editor.clear();
251
271
  // Enable kitty keyboard protocol (progressive enhancement flag 1)
252
- // so Shift+Enter sends \x1b[13;2u instead of plain \r
253
- process.stdout.write("\x1b[>1u");
272
+ // so Shift+Enter sends \x1b[13;2u instead of plain \r.
273
+ // Enable bracket paste mode so pasted text doesn't trigger submit.
274
+ process.stdout.write("\x1b[>1u\x1b[?2004h");
254
275
  this.writeModePromptLine(false);
255
276
  }
256
277
  exitMode() {
257
278
  this.dismissAutocomplete();
258
279
  this.activeMode = null;
259
280
  this.editor.clear();
260
- // Disable kitty keyboard protocol
261
- process.stdout.write("\x1b[<u");
281
+ // Disable kitty keyboard protocol and bracket paste mode
282
+ process.stdout.write("\x1b[<u\x1b[?2004l");
262
283
  this.clearPromptArea();
263
284
  this.printPrompt();
264
285
  }
@@ -294,7 +315,7 @@ export class InputHandler {
294
315
  this.updateAutocomplete();
295
316
  }
296
317
  updateAutocomplete() {
297
- const buf = this.editor.buffer;
318
+ const buf = this.editor.text;
298
319
  let command = null;
299
320
  let commandArgs = null;
300
321
  if (buf.startsWith("/")) {
@@ -350,7 +371,7 @@ export class InputHandler {
350
371
  : `${indicator} `;
351
372
  const icon = this.activeMode?.promptIcon ?? "❯";
352
373
  const promptVisLen = visibleLen(infoPrefix) + visibleLen(icon) + 1;
353
- const col = promptVisLen + this.editor.cursor;
374
+ const col = promptVisLen + this.editor.displayCursor;
354
375
  process.stdout.write(`\r\x1b[${col}C`);
355
376
  }
356
377
  applyAutocomplete() {
@@ -359,18 +380,16 @@ export class InputHandler {
359
380
  const selected = this.autocompleteItems[this.autocompleteIndex];
360
381
  if (!selected)
361
382
  return;
362
- const atPos = this.editor.buffer.lastIndexOf("@");
383
+ const atPos = this.editor.text.lastIndexOf("@");
363
384
  const isFileAc = atPos >= 0 &&
364
- (atPos === 0 || this.editor.buffer[atPos - 1] === " ") &&
365
- !this.editor.buffer.slice(atPos + 1).includes(" ");
385
+ (atPos === 0 || this.editor.text[atPos - 1] === " ") &&
386
+ !this.editor.text.slice(atPos + 1).includes(" ");
366
387
  if (isFileAc) {
367
- this.editor.buffer =
368
- this.editor.buffer.slice(0, atPos) + "@" + selected.name;
388
+ this.editor.setText(this.editor.text.slice(0, atPos) + "@" + selected.name);
369
389
  }
370
390
  else {
371
- this.editor.buffer = selected.name;
391
+ this.editor.setText(selected.name);
372
392
  }
373
- this.editor.cursor = this.editor.buffer.length;
374
393
  this.clearAutocompleteLines();
375
394
  this.autocompleteActive = false;
376
395
  this.autocompleteItems = [];
@@ -419,8 +438,8 @@ export class InputHandler {
419
438
  switch (act.action) {
420
439
  case "changed": {
421
440
  // If the buffer is exactly a trigger char for a different mode, switch to it
422
- const switchMode = this.modes.get(this.editor.buffer);
423
- if (this.editor.buffer.length === 1 && switchMode && switchMode !== this.activeMode) {
441
+ const switchMode = this.modes.get(this.editor.text);
442
+ if (this.editor.text.length === 1 && switchMode && switchMode !== this.activeMode) {
424
443
  this.dismissAutocomplete();
425
444
  this.clearPromptArea();
426
445
  this.activeMode = switchMode;
@@ -437,10 +456,10 @@ export class InputHandler {
437
456
  if (this.autocompleteActive) {
438
457
  this.applyAutocomplete();
439
458
  }
440
- // Use editor.buffer (not act.buffer) so autocomplete selections
459
+ // Use editor.text (not act.buffer) so autocomplete selections
441
460
  // take effect — act.buffer is a stale snapshot from before
442
- // applyAutocomplete() updated the buffer.
443
- const query = this.editor.buffer.trim();
461
+ // applyAutocomplete() updated the editor.
462
+ const query = this.editor.text.trim();
444
463
  if (query) {
445
464
  // Add to history (avoid consecutive duplicates)
446
465
  if (this.history.length === 0 || this.history[this.history.length - 1] !== query) {
@@ -452,7 +471,7 @@ export class InputHandler {
452
471
  this.savedBuffer = "";
453
472
  this.clearAutocompleteLines();
454
473
  this.clearPromptArea();
455
- process.stdout.write("\x1b[<u"); // disable kitty keyboard protocol
474
+ process.stdout.write("\x1b[<u\x1b[?2004l"); // disable kitty + bracket paste
456
475
  const currentMode = this.activeMode;
457
476
  this.activeMode = null;
458
477
  this.editor.clear();
@@ -506,14 +525,13 @@ export class InputHandler {
506
525
  }
507
526
  else if (this.history.length > 0) {
508
527
  if (this.historyIndex === -1) {
509
- this.savedBuffer = this.editor.buffer;
528
+ this.savedBuffer = this.editor.text;
510
529
  this.historyIndex = this.history.length - 1;
511
530
  }
512
531
  else if (this.historyIndex > 0) {
513
532
  this.historyIndex--;
514
533
  }
515
- this.editor.buffer = this.history[this.historyIndex];
516
- this.editor.cursor = this.editor.buffer.length;
534
+ this.editor.setText(this.history[this.historyIndex]);
517
535
  this.clearAutocompleteLines();
518
536
  this.writeModePromptLine();
519
537
  }
@@ -531,13 +549,12 @@ export class InputHandler {
531
549
  else if (this.historyIndex !== -1) {
532
550
  if (this.historyIndex < this.history.length - 1) {
533
551
  this.historyIndex++;
534
- this.editor.buffer = this.history[this.historyIndex];
552
+ this.editor.setText(this.history[this.historyIndex]);
535
553
  }
536
554
  else {
537
555
  this.historyIndex = -1;
538
- this.editor.buffer = this.savedBuffer;
556
+ this.editor.setText(this.savedBuffer);
539
557
  }
540
- this.editor.cursor = this.editor.buffer.length;
541
558
  this.clearAutocompleteLines();
542
559
  this.writeModePromptLine();
543
560
  }
@@ -1,4 +1,4 @@
1
- import type { EventBus } from "./event-bus.js";
1
+ import type { EventBus } from "../event-bus.js";
2
2
  /**
3
3
  * Parses PTY output to detect command boundaries, track cwd,
4
4
  * and emit shell events. Owns the command lifecycle state.
@@ -1,4 +1,4 @@
1
- import { stripAnsi } from "./utils/ansi.js";
1
+ import { stripAnsi } from "../utils/ansi.js";
2
2
  /**
3
3
  * Parses PTY output to detect command boundaries, track cwd,
4
4
  * and emit shell events. Owns the command lifecycle state.
@@ -1,8 +1,13 @@
1
- import type { EventBus } from "./event-bus.js";
1
+ import type { EventBus } from "../event-bus.js";
2
2
  import { type InputContext } from "./input-handler.js";
3
+ export interface ShellHandlers {
4
+ define: (name: string, fn: (...args: any[]) => any) => void;
5
+ call: (name: string, ...args: any[]) => any;
6
+ }
3
7
  export declare class Shell implements InputContext {
4
8
  private ptyProcess;
5
9
  private bus;
10
+ private handlers;
6
11
  private inputHandler;
7
12
  private outputParser;
8
13
  private paused;
@@ -14,6 +19,7 @@ export declare class Shell implements InputContext {
14
19
  private tmpDir?;
15
20
  constructor(opts: {
16
21
  bus: EventBus;
22
+ handlers: ShellHandlers;
17
23
  onShowAgentInfo?: () => {
18
24
  info: string;
19
25
  model?: string;
@@ -43,7 +49,7 @@ export declare class Shell implements InputContext {
43
49
  * Routed through shell:redraw-prompt pipe so extensions (e.g. overlay)
44
50
  * can suppress it by setting `handled: true`.
45
51
  */
46
- freshPrompt(): void;
52
+ freshPrompt(): boolean;
47
53
  onCommandEntered(command: string, cwd: string): void;
48
54
  private setupOutput;
49
55
  private setupInput;
@@ -4,11 +4,12 @@ import * as path from "path";
4
4
  import * as pty from "node-pty";
5
5
  import { InputHandler } from "./input-handler.js";
6
6
  import { OutputParser } from "./output-parser.js";
7
- import { getSettings } from "./settings.js";
8
- import { RefCounter } from "./utils/output-writer.js";
7
+ import { getSettings } from "../settings.js";
8
+ import { RefCounter } from "../utils/output-writer.js";
9
9
  export class Shell {
10
10
  ptyProcess;
11
11
  bus;
12
+ handlers;
12
13
  inputHandler;
13
14
  outputParser;
14
15
  paused = false;
@@ -139,6 +140,7 @@ export class Shell {
139
140
  }
140
141
  }
141
142
  this.bus = opts.bus;
143
+ this.handlers = opts.handlers;
142
144
  this.outputParser = new OutputParser(opts.bus, opts.cwd);
143
145
  // Ensure temp dir cleanup on abnormal exit (SIGKILL won't fire this,
144
146
  // but it covers uncaught exceptions and normal process.exit paths)
@@ -225,7 +227,9 @@ export class Shell {
225
227
  });
226
228
  if (!result.handled) {
227
229
  this.ptyProcess.write("\n");
230
+ return true;
228
231
  }
232
+ return false;
229
233
  }
230
234
  onCommandEntered(command, cwd) {
231
235
  this.outputParser.onCommandEntered(command, cwd);
@@ -265,18 +269,28 @@ export class Shell {
265
269
  * zero frontend knowledge; any frontend can subscribe to the same events.
266
270
  */
267
271
  setupAgentLifecycle() {
268
- this.bus.on("agent:processing-start", () => {
272
+ // Default agent lifecycle: pause the shell while the agent works,
273
+ // then redraw the prompt when done. Extensions advise these handlers
274
+ // to change behavior (e.g. tmux split keeps the shell interactive).
275
+ this.handlers.define("shell:on-processing-start", () => {
269
276
  this.agentActive = true;
270
277
  this.paused = true;
271
278
  });
272
- this.bus.on("agent:processing-done", () => {
279
+ this.handlers.define("shell:on-processing-done", () => {
273
280
  this.paused = false;
274
281
  this.agentActive = false;
275
- this.echoSkip = true;
276
282
  if (!this.inputHandler.handleProcessingDone()) {
277
- this.freshPrompt();
283
+ if (this.freshPrompt()) {
284
+ this.echoSkip = true;
285
+ }
278
286
  }
279
287
  });
288
+ this.bus.on("agent:processing-start", () => {
289
+ this.handlers.call("shell:on-processing-start");
290
+ });
291
+ this.bus.on("agent:processing-done", () => {
292
+ this.handlers.call("shell:on-processing-done");
293
+ });
280
294
  // Permission prompts need stdout unpaused so the interactive UI renders,
281
295
  // then re-paused after the decision.
282
296
  this.bus.on("permission:request", () => {
package/dist/types.d.ts CHANGED
@@ -1,13 +1,37 @@
1
1
  import type { EventBus } from "./event-bus.js";
2
2
  import type { ContextManager } from "./context-manager.js";
3
- import type { LlmClient } from "./utils/llm-client.js";
4
3
  import type { ColorPalette } from "./utils/palette.js";
5
4
  import type { BlockTransformOptions, FencedBlockTransformOptions } from "./utils/stream-transform.js";
6
5
  import type { ToolDefinition } from "./agent/types.js";
7
6
  import type { TerminalBuffer } from "./utils/terminal-buffer.js";
8
- import type { FloatingPanel, FloatingPanelConfig } from "./utils/floating-panel.js";
7
+ import type { Compositor } from "./utils/compositor.js";
9
8
  export type { ContentBlock } from "./event-bus.js";
10
9
  export type { BlockTransformOptions, FencedBlockTransformOptions } from "./utils/stream-transform.js";
10
+ export type { RenderSurface } from "./utils/compositor.js";
11
+ export interface RemoteSessionOptions {
12
+ /** The surface to render agent output to. */
13
+ surface: import("./utils/compositor.js").RenderSurface;
14
+ /** Suppress response borders (default: true). */
15
+ suppressBorders?: boolean;
16
+ /** Suppress user query box (default: false).
17
+ * True for sessions with their own input (rsplit, overlay).
18
+ * False for sessions where input comes from the main shell (split). */
19
+ suppressQueryBox?: boolean;
20
+ /** Suppress usage stats line (default: true). */
21
+ suppressUsage?: boolean;
22
+ /** Set interactive-session dynamic context (default: false). */
23
+ interactive?: boolean;
24
+ }
25
+ export interface RemoteSession {
26
+ /** Submit a query to the agent from this session. */
27
+ submit(query: string): void;
28
+ /** The surface this session renders to. */
29
+ readonly surface: import("./utils/compositor.js").RenderSurface;
30
+ /** Whether this session is currently active. */
31
+ readonly active: boolean;
32
+ /** Tear down — restores all routing and advisors. */
33
+ close(): void;
34
+ }
11
35
  /** A model entry in the cycling list, optionally tied to a provider. */
12
36
  export interface AgentMode {
13
37
  model: string;
@@ -45,8 +69,8 @@ export interface AgentShellConfig {
45
69
  export interface ExtensionContext {
46
70
  bus: EventBus;
47
71
  contextManager: ContextManager;
48
- /** LLM client for fast-path features (null in ACP mode). */
49
- llmClient: LlmClient | null;
72
+ /** Stable per-instance identifier (4-char hex). */
73
+ readonly instanceId: string;
50
74
  quit: () => void;
51
75
  /** Override color palette slots for theming. */
52
76
  setPalette: (overrides: Partial<ColorPalette>) => void;
@@ -60,12 +84,18 @@ export interface ExtensionContext {
60
84
  registerCommand: (name: string, description: string, handler: (args: string) => Promise<void> | void) => void;
61
85
  /** Register a tool for the built-in agent. No-op when using bridge backends. */
62
86
  registerTool: (tool: ToolDefinition) => void;
87
+ /** Unregister a tool by name. */
88
+ unregisterTool: (name: string) => void;
63
89
  /** Get all registered tools (for subagent tool subsets). Returns [] when using bridge backends. */
64
90
  getTools: () => ToolDefinition[];
91
+ /** Register a named instruction block for the agent's system prompt. */
92
+ registerInstruction: (name: string, text: string) => void;
93
+ /** Remove a named instruction block from the system prompt. */
94
+ removeInstruction: (name: string) => void;
65
95
  /** Register a named handler. */
66
96
  define: (name: string, fn: (...args: any[]) => any) => void;
67
- /** Wrap a named handler. Receives `next` (original) + args. */
68
- advise: (name: string, wrapper: (next: (...args: any[]) => any, ...args: any[]) => any) => void;
97
+ /** Wrap a named handler. Receives `next` (original) + args. Returns an unadvise function. */
98
+ advise: (name: string, wrapper: (next: (...args: any[]) => any, ...args: any[]) => any) => () => void;
69
99
  /** Call a named handler. */
70
100
  call: (name: string, ...args: any[]) => any;
71
101
  /**
@@ -74,11 +104,20 @@ export interface ExtensionContext {
74
104
  */
75
105
  terminalBuffer: TerminalBuffer | null;
76
106
  /**
77
- * Create a floating panel overlay. The panel composites a bordered box
78
- * over the terminal with input routing, dimmed background, and
79
- * handler-based customization.
107
+ * Routes named render streams ("agent", "query", "status") to surfaces.
108
+ * Extensions use `compositor.redirect()` to capture output (e.g. overlay panels).
109
+ */
110
+ compositor: Compositor;
111
+ /**
112
+ * Create a remote session that routes agent output to a surface and
113
+ * optionally accepts queries. Handles all compositor routing, shell
114
+ * lifecycle advisors, and chrome suppression.
115
+ *
116
+ * const session = ctx.createRemoteSession({ surface, interactive: true });
117
+ * session.submit("what's on screen?");
118
+ * session.close(); // restores everything
80
119
  */
81
- createFloatingPanel: (config: FloatingPanelConfig) => FloatingPanel;
120
+ createRemoteSession: (opts: RemoteSessionOptions) => RemoteSession;
82
121
  }
83
122
  /**
84
123
  * Configuration for a registered input mode.
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Compositor — routes named render streams to surfaces.
3
+ *
4
+ * Components write to named streams ("agent", "query", "status").
5
+ * The compositor decides where each stream actually goes based on
6
+ * the current routing table. Extensions override routing with
7
+ * `redirect()` to capture output (e.g. overlay panels).
8
+ *
9
+ * Streams are hierarchical: "agent:diff" falls back to "agent" if
10
+ * no override or default is registered for "agent:diff" specifically.
11
+ * This enables fine-grained interception — redirect just diffs into
12
+ * a panel, or just a subagent's output ("agent:sub:abc123"), while
13
+ * everything else flows to the parent stream's surface.
14
+ *
15
+ * // tui-renderer registers default surfaces
16
+ * compositor.setDefault("agent", stdoutSurface);
17
+ *
18
+ * // overlay-agent redirects when active
19
+ * const restore = compositor.redirect("agent", panelSurface);
20
+ * // ... later ...
21
+ * restore(); // back to stdout
22
+ *
23
+ * // fine-grained: redirect only diffs to a viewer panel
24
+ * compositor.redirect("agent:diff", diffPanelSurface);
25
+ * // "agent:text", "agent:tool" etc. still go to stdout
26
+ */
27
+ /**
28
+ * A surface accepts rendered output. Stdout is a surface.
29
+ * A floating panel's content area is a surface. A test buffer is a surface.
30
+ */
31
+ export interface RenderSurface {
32
+ /** Raw write — supports \r, partial lines, escape codes. */
33
+ write(text: string): void;
34
+ /** Convenience: write + newline. */
35
+ writeLine(line: string): void;
36
+ /** Available width in columns. */
37
+ readonly columns: number;
38
+ }
39
+ export interface Compositor {
40
+ /** Get the currently active surface for a stream. */
41
+ surface(stream: string): RenderSurface;
42
+ /** Override routing: redirect a stream to a different surface.
43
+ * Returns a restore function that undoes the redirect. */
44
+ redirect(stream: string, target: RenderSurface): () => void;
45
+ /** Register the default surface for a stream. */
46
+ setDefault(stream: string, target: RenderSurface): void;
47
+ }
48
+ /** Silent sink — drops all output. Used when no surface is registered. */
49
+ export declare const nullSurface: RenderSurface;
50
+ /** Surface backed by process.stdout. */
51
+ export declare class StdoutSurface implements RenderSurface {
52
+ write(text: string): void;
53
+ writeLine(line: string): void;
54
+ get columns(): number;
55
+ }
56
+ export declare class DefaultCompositor implements Compositor {
57
+ private defaults;
58
+ private overrides;
59
+ surface(stream: string): RenderSurface;
60
+ redirect(stream: string, target: RenderSurface): () => void;
61
+ setDefault(stream: string, target: RenderSurface): void;
62
+ }