agent-sh 0.7.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 (86) hide show
  1. package/README.md +28 -33
  2. package/dist/agent/agent-loop.d.ts +31 -8
  3. package/dist/agent/agent-loop.js +277 -66
  4. package/dist/agent/conversation-state.d.ts +41 -9
  5. package/dist/agent/conversation-state.js +340 -17
  6. package/dist/agent/history-file.d.ts +36 -0
  7. package/dist/agent/history-file.js +167 -0
  8. package/dist/agent/nuclear-form.d.ts +41 -0
  9. package/dist/agent/nuclear-form.js +176 -0
  10. package/dist/agent/system-prompt.d.ts +4 -5
  11. package/dist/agent/system-prompt.js +16 -11
  12. package/dist/agent/token-budget.d.ts +13 -0
  13. package/dist/agent/token-budget.js +50 -0
  14. package/dist/agent/tool-protocol.d.ts +83 -0
  15. package/dist/agent/tool-protocol.js +386 -0
  16. package/dist/agent/tools/user-shell.js +4 -1
  17. package/dist/agent/types.d.ts +21 -1
  18. package/dist/context-manager.d.ts +0 -1
  19. package/dist/context-manager.js +5 -110
  20. package/dist/core.d.ts +7 -7
  21. package/dist/core.js +76 -180
  22. package/dist/event-bus.d.ts +40 -0
  23. package/dist/event-bus.js +20 -1
  24. package/dist/extension-loader.d.ts +5 -0
  25. package/dist/extension-loader.js +104 -17
  26. package/dist/extensions/agent-backend.d.ts +13 -0
  27. package/dist/extensions/agent-backend.js +167 -0
  28. package/dist/extensions/command-suggest.d.ts +3 -3
  29. package/dist/extensions/command-suggest.js +4 -3
  30. package/dist/extensions/index.d.ts +19 -0
  31. package/dist/extensions/index.js +25 -0
  32. package/dist/extensions/slash-commands.d.ts +1 -1
  33. package/dist/extensions/slash-commands.js +44 -1
  34. package/dist/extensions/terminal-buffer.d.ts +1 -1
  35. package/dist/extensions/terminal-buffer.js +22 -8
  36. package/dist/extensions/tui-renderer.js +177 -122
  37. package/dist/index.js +14 -20
  38. package/dist/settings.d.ts +25 -2
  39. package/dist/settings.js +25 -4
  40. package/dist/{input-handler.d.ts → shell/input-handler.d.ts} +1 -1
  41. package/dist/{input-handler.js → shell/input-handler.js} +60 -43
  42. package/dist/{output-parser.d.ts → shell/output-parser.d.ts} +1 -1
  43. package/dist/{output-parser.js → shell/output-parser.js} +1 -1
  44. package/dist/{shell.d.ts → shell/shell.d.ts} +8 -2
  45. package/dist/{shell.js → shell/shell.js} +24 -6
  46. package/dist/types.d.ts +49 -32
  47. package/dist/utils/ansi.d.ts +10 -0
  48. package/dist/utils/ansi.js +27 -0
  49. package/dist/utils/compositor.d.ts +62 -0
  50. package/dist/utils/compositor.js +88 -0
  51. package/dist/utils/diff-renderer.js +92 -4
  52. package/dist/utils/floating-panel.d.ts +34 -3
  53. package/dist/utils/floating-panel.js +315 -82
  54. package/dist/utils/handler-registry.d.ts +26 -10
  55. package/dist/utils/handler-registry.js +52 -16
  56. package/dist/utils/line-editor.d.ts +32 -3
  57. package/dist/utils/line-editor.js +218 -36
  58. package/dist/utils/markdown.d.ts +1 -0
  59. package/dist/utils/markdown.js +4 -4
  60. package/dist/utils/message-utils.d.ts +35 -0
  61. package/dist/utils/message-utils.js +75 -0
  62. package/dist/utils/terminal-buffer.d.ts +9 -1
  63. package/dist/utils/terminal-buffer.js +31 -2
  64. package/dist/utils/tool-display.d.ts +1 -0
  65. package/dist/utils/tool-display.js +1 -1
  66. package/dist/utils/tool-interactive.d.ts +12 -0
  67. package/dist/utils/tool-interactive.js +53 -0
  68. package/examples/extensions/ash-acp-bridge/README.md +39 -0
  69. package/examples/extensions/ash-acp-bridge/package.json +23 -0
  70. package/examples/extensions/ash-acp-bridge/src/index.ts +571 -0
  71. package/examples/extensions/ash-acp-bridge/tsconfig.json +14 -0
  72. package/examples/extensions/ash-mcp-bridge/README.md +72 -0
  73. package/examples/extensions/ash-mcp-bridge/index.ts +154 -0
  74. package/examples/extensions/ash-mcp-bridge/package.json +9 -0
  75. package/examples/extensions/claude-code-bridge/index.ts +77 -1
  76. package/examples/extensions/interactive-prompts.ts +82 -110
  77. package/examples/extensions/overlay-agent.ts +84 -38
  78. package/examples/extensions/peer-mesh.ts +450 -0
  79. package/examples/extensions/pi-bridge/index.ts +87 -2
  80. package/examples/extensions/questionnaire.ts +249 -0
  81. package/examples/extensions/tmux-pane.ts +307 -0
  82. package/examples/extensions/web-access.ts +327 -0
  83. package/package.json +9 -1
  84. package/dist/extensions/overlay-agent.d.ts +0 -11
  85. package/dist/extensions/overlay-agent.js +0 -43
  86. package/examples/extensions/terminal-buffer.ts +0 -184
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
@@ -1,4 +1,13 @@
1
1
  export declare const CONFIG_DIR: string;
2
+ /** Per-model capability overrides. */
3
+ export interface ModelCapabilityConfig {
4
+ /** Model identifier. */
5
+ id: string;
6
+ /** Whether the model supports reasoning/thinking tokens. */
7
+ reasoning?: boolean;
8
+ /** Context window size in tokens for this specific model. */
9
+ contextWindow?: number;
10
+ }
2
11
  /** Provider profile — a named LLM configuration. */
3
12
  export interface ProviderConfig {
4
13
  /** API key (supports $ENV_VAR syntax for runtime expansion). */
@@ -7,8 +16,8 @@ export interface ProviderConfig {
7
16
  baseURL?: string;
8
17
  /** Default model to use. Falls back to first entry in models list. */
9
18
  defaultModel?: string;
10
- /** Models available for cycling. */
11
- models?: string[];
19
+ /** Models available for cycling. Plain strings or objects with capabilities. */
20
+ models?: (string | ModelCapabilityConfig)[];
12
21
  /** Context window size in tokens (e.g. 128000). Used for usage display. */
13
22
  contextWindow?: number;
14
23
  }
@@ -35,18 +44,32 @@ export interface Settings {
35
44
  shellTailLines?: number;
36
45
  /** Max lines for recall expand before requiring line ranges. */
37
46
  recallExpandMaxLines?: number;
47
+ /** Fraction of content budget allocated to shell context (0-1, default 0.35). */
48
+ shellContextRatio?: number;
49
+ /** Max history file size in bytes (default: 102400 = 100KB). */
50
+ historyMaxBytes?: number;
51
+ /** Number of prior history entries to load on startup (default: 50). */
52
+ historyStartupEntries?: number;
53
+ /** Max nuclear entries kept in-context before flushing to history file (default: 200). */
54
+ nuclearMaxEntries?: number;
55
+ /** Auto-compact threshold as fraction of conversation budget (0-1, default 0.5). */
56
+ autoCompactThreshold?: number;
38
57
  /** Max command output lines shown inline in TUI. */
39
58
  maxCommandOutputLines?: number;
40
59
  /** Max read tool output lines shown inline in TUI (0 = hide). */
41
60
  readOutputMaxLines?: number;
42
61
  /** Max diff lines shown before "ctrl+o to expand". */
43
62
  diffMaxLines?: number;
63
+ /** Tool protocol: "api" (all tools), "deferred" (extensions via meta-tool), "inline" (text). */
64
+ toolMode?: "api" | "deferred" | "inline";
44
65
  /** Additional directories to scan for skills (supports ~ expansion). */
45
66
  skillPaths?: string[];
46
67
  /** Show a startup banner when agent-sh launches. */
47
68
  startupBanner?: boolean;
48
69
  /** Show a subtle agent-sh indicator in the shell prompt. */
49
70
  promptIndicator?: boolean;
71
+ /** Names of built-in extensions to disable (e.g. ["command-suggest"]). */
72
+ disabledBuiltins?: string[];
50
73
  }
51
74
  declare const DEFAULTS: Required<Settings>;
52
75
  /** Load settings from disk (cached after first call). */
package/dist/settings.js CHANGED
@@ -14,19 +14,26 @@ 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,
21
22
  shellHeadLines: 5,
22
23
  shellTailLines: 5,
23
24
  recallExpandMaxLines: 100,
25
+ shellContextRatio: 0.35,
26
+ historyMaxBytes: 102400,
27
+ historyStartupEntries: 50,
28
+ nuclearMaxEntries: 200,
29
+ autoCompactThreshold: 0.5,
24
30
  maxCommandOutputLines: 3,
25
31
  readOutputMaxLines: 10,
26
32
  diffMaxLines: 20,
27
33
  skillPaths: [],
28
34
  startupBanner: true,
29
35
  promptIndicator: true,
36
+ disabledBuiltins: [],
30
37
  };
31
38
  let cached = null;
32
39
  /** Load settings from disk (cached after first call). */
@@ -86,15 +93,29 @@ export function resolveProvider(name) {
86
93
  const provider = settings.providers?.[name];
87
94
  if (!provider)
88
95
  return null;
89
- const models = provider.models ?? (provider.defaultModel ? [provider.defaultModel] : []);
90
- const defaultModel = provider.defaultModel ?? models[0];
96
+ const rawModels = provider.models ?? (provider.defaultModel ? [provider.defaultModel] : []);
97
+ const modelIds = [];
98
+ const caps = new Map();
99
+ for (const m of rawModels) {
100
+ if (typeof m === "string") {
101
+ modelIds.push(m);
102
+ }
103
+ else {
104
+ modelIds.push(m.id);
105
+ if (m.reasoning !== undefined || m.contextWindow !== undefined) {
106
+ caps.set(m.id, { reasoning: m.reasoning, contextWindow: m.contextWindow });
107
+ }
108
+ }
109
+ }
110
+ const defaultModel = provider.defaultModel ?? modelIds[0];
91
111
  return {
92
112
  id: name,
93
113
  apiKey: provider.apiKey ? expandEnvVars(provider.apiKey) : undefined,
94
114
  baseURL: provider.baseURL,
95
115
  defaultModel,
96
- models: models.length ? models : (defaultModel ? [defaultModel] : []),
116
+ models: modelIds.length ? modelIds : (defaultModel ? [defaultModel] : []),
97
117
  contextWindow: provider.contextWindow,
118
+ modelCapabilities: caps.size > 0 ? caps : undefined,
98
119
  };
99
120
  }
100
121
  /** Get all configured provider names. */
@@ -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)
@@ -163,6 +165,10 @@ export class Shell {
163
165
  this.bus.on("shell:pty-write", ({ data }) => {
164
166
  this.ptyProcess.write(data);
165
167
  });
168
+ // Allow extensions to resize the PTY (sends SIGWINCH to child)
169
+ this.bus.on("shell:pty-resize", ({ cols, rows }) => {
170
+ this.ptyProcess.resize(cols, rows);
171
+ });
166
172
  // Ref-counted stdout hold — overlay extensions suppress PTY output
167
173
  this.bus.on("shell:stdout-hold", () => { this.stdoutHold.increment(); });
168
174
  this.bus.on("shell:stdout-release", () => { this.stdoutHold.decrement(); });
@@ -221,7 +227,9 @@ export class Shell {
221
227
  });
222
228
  if (!result.handled) {
223
229
  this.ptyProcess.write("\n");
230
+ return true;
224
231
  }
232
+ return false;
225
233
  }
226
234
  onCommandEntered(command, cwd) {
227
235
  this.outputParser.onCommandEntered(command, cwd);
@@ -261,18 +269,28 @@ export class Shell {
261
269
  * zero frontend knowledge; any frontend can subscribe to the same events.
262
270
  */
263
271
  setupAgentLifecycle() {
264
- 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", () => {
265
276
  this.agentActive = true;
266
277
  this.paused = true;
267
278
  });
268
- this.bus.on("agent:processing-done", () => {
279
+ this.handlers.define("shell:on-processing-done", () => {
269
280
  this.paused = false;
270
281
  this.agentActive = false;
271
- this.echoSkip = true;
272
282
  if (!this.inputHandler.handleProcessingDone()) {
273
- this.freshPrompt();
283
+ if (this.freshPrompt()) {
284
+ this.echoSkip = true;
285
+ }
274
286
  }
275
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
+ });
276
294
  // Permission prompts need stdout unpaused so the interactive UI renders,
277
295
  // then re-paused after the decision.
278
296
  this.bus.on("permission:request", () => {