agent-sh 0.14.1 → 0.14.3

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 (65) hide show
  1. package/dist/agent/agent-loop.d.ts +1 -1
  2. package/dist/agent/agent-loop.js +42 -31
  3. package/dist/agent/conversation-state.d.ts +3 -2
  4. package/dist/agent/conversation-state.js +20 -3
  5. package/dist/agent/events.d.ts +2 -0
  6. package/dist/agent/host-types.d.ts +3 -0
  7. package/dist/agent/index.js +2 -1
  8. package/dist/agent/subagent.d.ts +1 -1
  9. package/dist/agent/subagent.js +5 -1
  10. package/dist/agent/tool-protocol.d.ts +2 -2
  11. package/dist/agent/tool-protocol.js +5 -4
  12. package/dist/agent/tools/glob.d.ts +1 -1
  13. package/dist/agent/tools/glob.js +4 -2
  14. package/dist/agent/tools/grep.d.ts +1 -1
  15. package/dist/agent/tools/grep.js +4 -2
  16. package/dist/agent/tools/ls.d.ts +1 -1
  17. package/dist/agent/tools/ls.js +4 -2
  18. package/dist/agent/tools/read-file.d.ts +1 -1
  19. package/dist/agent/tools/read-file.js +30 -2
  20. package/dist/agent/types.d.ts +11 -1
  21. package/dist/agent/types.js +6 -1
  22. package/dist/cli/index.js +0 -0
  23. package/dist/core/index.d.ts +1 -1
  24. package/dist/core/settings.d.ts +3 -0
  25. package/dist/core/settings.js +2 -2
  26. package/dist/shell/events.d.ts +2 -0
  27. package/dist/shell/index.d.ts +6 -0
  28. package/dist/shell/index.js +10 -10
  29. package/dist/shell/output-parser.d.ts +11 -22
  30. package/dist/shell/output-parser.js +16 -34
  31. package/dist/shell/shell-context.d.ts +3 -6
  32. package/dist/shell/shell-context.js +15 -7
  33. package/dist/shell/shell.d.ts +4 -0
  34. package/dist/shell/shell.js +18 -30
  35. package/dist/shell/strategies/types.d.ts +6 -0
  36. package/dist/shell/strategies/zsh.js +7 -0
  37. package/dist/shell/terminal.d.ts +33 -0
  38. package/dist/shell/terminal.js +62 -0
  39. package/examples/extensions/ash-scheme/index.ts +2170 -0
  40. package/examples/extensions/ash-scheme/package.json +11 -0
  41. package/examples/extensions/ash-scheme-render.ts +58 -0
  42. package/examples/extensions/ashi/README.md +36 -26
  43. package/examples/extensions/ashi/package.json +9 -1
  44. package/examples/extensions/ashi/src/capture.ts +1 -0
  45. package/examples/extensions/ashi/src/cli.ts +53 -11
  46. package/examples/extensions/ashi/src/commands.ts +2 -20
  47. package/examples/extensions/ashi/src/compaction.ts +25 -96
  48. package/examples/extensions/ashi/src/components.ts +64 -166
  49. package/examples/extensions/ashi/src/default-schema-renderers.ts +232 -0
  50. package/examples/extensions/ashi/src/display-config.ts +21 -22
  51. package/examples/extensions/ashi/src/frontend.ts +355 -118
  52. package/examples/extensions/ashi/src/hooks.ts +47 -63
  53. package/examples/extensions/ashi/src/multi-session-store.ts +44 -3
  54. package/examples/extensions/ashi/src/schema.ts +386 -0
  55. package/examples/extensions/ashi/src/session-store.ts +115 -17
  56. package/examples/extensions/ashi/src/shell-mode.ts +52 -0
  57. package/examples/extensions/ashi/src/status-footer.ts +41 -6
  58. package/examples/extensions/ashi/src/theme.ts +2 -1
  59. package/examples/extensions/ashi-compact-llm.ts +93 -0
  60. package/examples/extensions/claude-code-bridge/index.ts +2 -0
  61. package/examples/extensions/opencode-bridge/index.ts +3 -0
  62. package/examples/extensions/opencode-provider.ts +252 -0
  63. package/examples/extensions/pi-bridge/index.ts +1 -0
  64. package/package.json +16 -1
  65. package/examples/extensions/ashi/src/default-renderers.ts +0 -171
@@ -5,12 +5,13 @@
5
5
  */
6
6
  import "./events.js"; // augments BusEvents with shell-owned events
7
7
  import { Shell } from "./shell.js";
8
- import { DefaultCompositor, StdoutSurface } from "../utils/compositor.js";
8
+ import { DefaultCompositor } from "../utils/compositor.js";
9
9
  import { TerminalBuffer } from "../utils/terminal-buffer.js";
10
10
  import { setPalette } from "../utils/palette.js";
11
11
  import * as streamTransform from "../utils/stream-transform.js";
12
12
  import activateShellContext from "./shell-context.js";
13
13
  import activateTuiRenderer from "./tui-renderer.js";
14
+ import { processTerminal, surfaceFromTerminal } from "./terminal.js";
14
15
  /**
15
16
  * Register shell-owned handlers extensions can `ctx.call`, and attach
16
17
  * the shell surface to ctx. Must run before `loadExtensions` so user
@@ -77,10 +78,11 @@ export function registerShellHandlers(ctx) {
77
78
  * `src/cli/index.ts`) uses to drive lifecycle from process-level events.
78
79
  */
79
80
  export function activateShell(ctx, opts) {
80
- const stdoutSurface = new StdoutSurface();
81
- ctx.shell.compositor.setDefault("agent", stdoutSurface);
82
- ctx.shell.compositor.setDefault("query", stdoutSurface);
83
- ctx.shell.compositor.setDefault("status", stdoutSurface);
81
+ const terminal = opts.terminal ?? processTerminal();
82
+ const surface = surfaceFromTerminal(terminal);
83
+ ctx.shell.compositor.setDefault("agent", surface);
84
+ ctx.shell.compositor.setDefault("query", surface);
85
+ ctx.shell.compositor.setDefault("status", surface);
84
86
  const shell = new Shell({
85
87
  bus: ctx.bus,
86
88
  handlers: { define: ctx.define, call: ctx.call },
@@ -90,13 +92,11 @@ export function activateShell(ctx, opts) {
90
92
  cwd: opts.cwd,
91
93
  instanceId: ctx.instanceId,
92
94
  onShowAgentInfo: opts.onShowAgentInfo,
95
+ terminal,
93
96
  });
94
- const onResize = () => {
95
- shell.resize(process.stdout.columns || 80, process.stdout.rows || 24);
96
- };
97
- process.stdout.on("resize", onResize);
97
+ const offResize = terminal.onResize((cols, rows) => shell.resize(cols, rows));
98
98
  ctx.onDispose(() => {
99
- process.stdout.off("resize", onResize);
99
+ offResize();
100
100
  shell.kill();
101
101
  });
102
102
  return {
@@ -1,42 +1,31 @@
1
1
  import type { EventBus } from "../core/event-bus.js";
2
- /**
3
- * Parses PTY output to detect command boundaries, track cwd,
4
- * and emit shell events. Owns the command lifecycle state.
5
- */
2
+ export interface OutputParserOpts {
3
+ /** Optional shell-specific cleanup applied to raw output before stripAnsi. */
4
+ cleanOutput?(raw: string): string;
5
+ }
6
6
  export declare class OutputParser {
7
7
  private bus;
8
8
  private cwd;
9
9
  private ownTag;
10
+ private cleanOutput;
10
11
  private currentOutputCapture;
11
12
  private lastCommand;
12
13
  private foregroundBusy;
13
14
  private promptReady;
14
- constructor(bus: EventBus, initialCwd: string, ownTag: string);
15
- /** Process a chunk of PTY output data. */
15
+ constructor(bus: EventBus, initialCwd: string, ownTag: string, opts?: OutputParserOpts);
16
16
  processData(data: string): void;
17
- /** Called when user presses Enter on a non-empty line. */
18
17
  onCommandEntered(command: string, cwd: string): void;
19
- /** Whether the shell's prompt is fully rendered and ready for input. */
20
18
  isPromptReady(): boolean;
21
19
  isForegroundBusy(): boolean;
22
20
  getCwd(): string;
23
- /**
24
- * Detect preexec marker (OSC 9997) emitted by the shell's preexec hook.
25
- * This carries the actual command text from the shell more reliable than
26
- * the InputHandler's lineBuffer which can't track history recall or tab
27
- * completion. Returns data with the OSC stripped out.
28
- */
21
+ /** Pulls the actual command from the shell's OSC 9997 preexec marker —
22
+ * more reliable than the InputHandler's lineBuffer, which can't track
23
+ * history recall or tab completion. Returns data with the OSC stripped. */
29
24
  private handlePreexec;
30
25
  private parseOSC7;
31
- /**
32
- * Detect our custom prompt marker (OSC 9999) in the PTY stream.
33
- * Each time a prompt appears, we finalize the previous command's output.
34
- */
26
+ /** OSC 9999 marker — each occurrence finalizes the previous command's output. */
35
27
  private parsePromptMarker;
36
- /**
37
- * Detect end-of-prompt marker (OSC 9998). The prompt is fully rendered
38
- * and the shell is ready for input.
39
- */
28
+ /** OSC 9998 — prompt is fully rendered and the shell is ready for input. */
40
29
  private parsePromptEnd;
41
30
  private removeEchoedCommand;
42
31
  }
@@ -4,32 +4,27 @@ import { stripAnsi } from "../utils/ansi.js";
4
4
  const PROMPT_RE = /\x1b\]9999;(?:id=([a-f0-9]+);)?PROMPT\x07/;
5
5
  const PREEXEC_RE = /\x1b\]9997;(?:id=([a-f0-9]+);)?([^\x07]*)\x07/;
6
6
  const READY_RE = /\x1b\]9998;(?:id=([a-f0-9]+);)?READY\x07/;
7
- /**
8
- * Parses PTY output to detect command boundaries, track cwd,
9
- * and emit shell events. Owns the command lifecycle state.
10
- */
11
7
  export class OutputParser {
12
8
  bus;
13
9
  cwd;
14
10
  ownTag;
11
+ cleanOutput;
15
12
  currentOutputCapture = "";
16
13
  lastCommand = "";
17
14
  foregroundBusy = false;
18
15
  promptReady = false;
19
- constructor(bus, initialCwd, ownTag) {
16
+ constructor(bus, initialCwd, ownTag, opts = {}) {
20
17
  this.bus = bus;
21
18
  this.cwd = initialCwd;
22
- // Strip the "id=" prefix; we compare the value alone.
23
19
  this.ownTag = ownTag.startsWith("id=") ? ownTag.slice(3) : ownTag;
20
+ this.cleanOutput = opts.cleanOutput ?? ((raw) => raw);
24
21
  }
25
- /** Process a chunk of PTY output data. */
26
22
  processData(data) {
27
23
  this.parseOSC7(data);
28
24
  data = this.handlePreexec(data);
29
25
  this.parsePromptMarker(data);
30
26
  this.parsePromptEnd(data);
31
27
  }
32
- /** Called when user presses Enter on a non-empty line. */
33
28
  onCommandEntered(command, cwd) {
34
29
  this.lastCommand = command;
35
30
  this.currentOutputCapture = "";
@@ -39,7 +34,6 @@ export class OutputParser {
39
34
  this.bus.emit("shell:foreground-busy", { busy: true });
40
35
  }
41
36
  }
42
- /** Whether the shell's prompt is fully rendered and ready for input. */
43
37
  isPromptReady() {
44
38
  return this.promptReady;
45
39
  }
@@ -49,13 +43,9 @@ export class OutputParser {
49
43
  getCwd() {
50
44
  return this.cwd;
51
45
  }
52
- // ── Parsing ─────────────────────────────────────────────────
53
- /**
54
- * Detect preexec marker (OSC 9997) emitted by the shell's preexec hook.
55
- * This carries the actual command text from the shell — more reliable than
56
- * the InputHandler's lineBuffer which can't track history recall or tab
57
- * completion. Returns data with the OSC stripped out.
58
- */
46
+ /** Pulls the actual command from the shell's OSC 9997 preexec marker —
47
+ * more reliable than the InputHandler's lineBuffer, which can't track
48
+ * history recall or tab completion. Returns data with the OSC stripped. */
59
49
  handlePreexec(data) {
60
50
  const match = PREEXEC_RE.exec(data);
61
51
  if (!match)
@@ -66,7 +56,8 @@ export class OutputParser {
66
56
  }
67
57
  const command = match[2];
68
58
  this.lastCommand = command;
69
- this.currentOutputCapture = ""; // discard echo accumulated before preexec
59
+ // Discard echo accumulated before preexec.
60
+ this.currentOutputCapture = "";
70
61
  if (!this.foregroundBusy) {
71
62
  this.foregroundBusy = true;
72
63
  this.bus.emit("shell:foreground-busy", { busy: true });
@@ -84,21 +75,16 @@ export class OutputParser {
84
75
  }
85
76
  }
86
77
  }
87
- /**
88
- * Detect our custom prompt marker (OSC 9999) in the PTY stream.
89
- * Each time a prompt appears, we finalize the previous command's output.
90
- */
78
+ /** OSC 9999 marker — each occurrence finalizes the previous command's output. */
91
79
  parsePromptMarker(data) {
92
80
  const match = PROMPT_RE.exec(data);
93
81
  if (match) {
94
82
  if (match[1] !== this.ownTag) {
95
- // Nested instance or untagged foreign emission treat as opaque
96
- // foreground output, do not finalize our own command.
83
+ // Nested or untagged emission: keep as opaque foreground output.
97
84
  this.currentOutputCapture += data;
98
85
  return;
99
86
  }
100
87
  const markerIdx = match.index;
101
- // Capture any output that arrived in the same chunk before the marker
102
88
  if (markerIdx > 0) {
103
89
  this.currentOutputCapture += data.slice(0, markerIdx);
104
90
  }
@@ -108,7 +94,8 @@ export class OutputParser {
108
94
  this.bus.emit("shell:foreground-busy", { busy: false });
109
95
  }
110
96
  if (this.lastCommand) {
111
- const output = stripAnsi(this.currentOutputCapture).trim();
97
+ const raw = this.cleanOutput(this.currentOutputCapture);
98
+ const output = stripAnsi(raw).trim();
112
99
  const cleaned = this.removeEchoedCommand(output, this.lastCommand);
113
100
  this.bus.emit("shell:command-done", {
114
101
  command: this.lastCommand,
@@ -121,21 +108,16 @@ export class OutputParser {
121
108
  this.currentOutputCapture = "";
122
109
  }
123
110
  else {
124
- // Cap capture buffer to avoid unbounded growth when a foreground
125
- // program (tmux, vim, etc.) produces output without prompt markers.
126
- // Keep only the tail — the final output is what matters for
127
- // command-done context.
128
- const MAX_CAPTURE = 128 * 1024; // 128 KB
111
+ // Cap to the tail so a long-running foreground program (tmux, vim)
112
+ // emitting output without prompt markers can't grow this unboundedly.
113
+ const MAX_CAPTURE = 128 * 1024;
129
114
  this.currentOutputCapture += data;
130
115
  if (this.currentOutputCapture.length > MAX_CAPTURE) {
131
116
  this.currentOutputCapture = this.currentOutputCapture.slice(-MAX_CAPTURE);
132
117
  }
133
118
  }
134
119
  }
135
- /**
136
- * Detect end-of-prompt marker (OSC 9998). The prompt is fully rendered
137
- * and the shell is ready for input.
138
- */
120
+ /** OSC 9998 — prompt is fully rendered and the shell is ready for input. */
139
121
  parsePromptEnd(data) {
140
122
  const match = READY_RE.exec(data);
141
123
  if (!match)
@@ -1,8 +1,5 @@
1
- /**
2
- * Tracks PTY commands and cwd, spills long outputs, contributes the
3
- * per-query `<cwd>` (always) and `<shell_events>` (when there are fresh
4
- * user-shell exchanges) signals. Frontends without a PTY skip this
5
- * built-in and the agent runs cwd-aware via core's process.cwd() default.
6
- */
1
+ /** Tracks PTY commands and cwd, spills long outputs, contributes per-query
2
+ * `<cwd>` (always) and `<shell_events>` (fresh user exchanges). Frontends
3
+ * without a PTY skip this and fall back to core's process.cwd() default. */
7
4
  import type { ExtensionContext } from "./host-types.js";
8
5
  export default function activate(ctx: ExtensionContext): void;
@@ -6,12 +6,13 @@ export default function activate(ctx) {
6
6
  let nextId = 1;
7
7
  let currentCwd = process.cwd();
8
8
  let agentShellActive = false;
9
+ let nextUserExcluded = false;
9
10
  let lastSeq = 0;
10
11
  bus.on("shell:command-done", (e) => {
11
12
  const lines = e.output.split("\n");
12
13
  const s = getSettings();
13
- // Long outputs spill to a tempfile so the agent can `read_file` them
14
- // on demand instead of carrying the full text in LLM context.
14
+ // Long outputs spill to a tempfile the agent can `read_file` instead of
15
+ // carrying the full text in LLM context.
15
16
  let output = e.output;
16
17
  let spillPath;
17
18
  if (lines.length > s.shellTruncateThreshold) {
@@ -25,6 +26,13 @@ export default function activate(ctx) {
25
26
  spillPath = undefined;
26
27
  }
27
28
  }
29
+ const source = agentShellActive
30
+ ? "agent"
31
+ : nextUserExcluded
32
+ ? "user-excluded"
33
+ : "user";
34
+ if (nextUserExcluded)
35
+ nextUserExcluded = false;
28
36
  exchanges.push({
29
37
  id: nextId++,
30
38
  timestamp: Date.now(),
@@ -34,22 +42,22 @@ export default function activate(ctx) {
34
42
  exitCode: e.exitCode,
35
43
  outputLines: lines.length,
36
44
  outputBytes: e.output.length,
37
- source: agentShellActive ? "agent" : "user",
45
+ source,
38
46
  spillPath,
39
47
  });
40
48
  });
41
49
  bus.on("shell:cwd-change", (e) => { currentCwd = e.cwd; });
42
50
  bus.on("shell:agent-exec-start", () => { agentShellActive = true; });
43
51
  bus.on("shell:agent-exec-done", () => { agentShellActive = false; });
44
- // Override core's process.cwd() default with the PTY-tracked value.
52
+ bus.on("shell:user-exec-exclude-next", () => { nextUserExcluded = true; });
45
53
  ctx.advise("cwd", () => currentCwd);
46
- // Advises the core handler directly: shell-context loads before the
47
- // agent host attaches `ctx.agent`, so the sugar isn't available yet.
54
+ // Advise the core handler directly: this loads before the agent host
55
+ // attaches `ctx.agent`, so the sugar isn't available yet.
48
56
  ctx.advise("query-context:build", (next) => {
49
57
  const base = next();
50
58
  const part = (() => {
51
59
  const cwdTag = `<cwd>${currentCwd}</cwd>`;
52
- const fresh = exchanges.filter((ex) => ex.id > lastSeq && ex.source !== "agent");
60
+ const fresh = exchanges.filter((ex) => ex.id > lastSeq && ex.source === "user");
53
61
  if (fresh.length === 0)
54
62
  return cwdTag;
55
63
  lastSeq = exchanges[exchanges.length - 1].id;
@@ -1,5 +1,6 @@
1
1
  import type { EventBus } from "../core/event-bus.js";
2
2
  import { type InputContext } from "./input-handler.js";
3
+ import { type Terminal } from "./terminal.js";
3
4
  export interface ShellHandlers {
4
5
  define: (name: string, fn: (...args: any[]) => any) => void;
5
6
  call: (name: string, ...args: any[]) => any;
@@ -19,6 +20,8 @@ export declare class Shell implements InputContext {
19
20
  private handlers;
20
21
  private inputHandler;
21
22
  private outputParser;
23
+ private terminal;
24
+ private inputDispose;
22
25
  private hardMuteScopes;
23
26
  private softMuteScopes;
24
27
  private unmuteScopes;
@@ -38,6 +41,7 @@ export declare class Shell implements InputContext {
38
41
  shell: string;
39
42
  cwd: string;
40
43
  instanceId: string;
44
+ terminal?: Terminal;
41
45
  });
42
46
  /** Compositing-layer claim — overrides any unmute. */
43
47
  acquireHardMute(reason: string): ShellScope;
@@ -5,6 +5,7 @@ import { InputHandler } from "./input-handler.js";
5
5
  import { OutputParser } from "./output-parser.js";
6
6
  import { getSettings } from "../core/settings.js";
7
7
  import { clearOpost } from "../utils/tty.js";
8
+ import { processTerminal } from "./terminal.js";
8
9
  import { pickStrategy, FALLBACK_STRATEGY, SUPPORTED_SHELL_NAMES, } from "./strategies/index.js";
9
10
  export class Shell {
10
11
  ptyProcess;
@@ -12,6 +13,8 @@ export class Shell {
12
13
  handlers;
13
14
  inputHandler;
14
15
  outputParser;
16
+ terminal;
17
+ inputDispose = null;
15
18
  // hardMute is unconditional (overlay compositing); softMute is overridable
16
19
  // by unmute (terminal_keys, permission UI). Gate: hard wins; otherwise
17
20
  // muted iff softMute held without an unmute.
@@ -23,6 +26,7 @@ export class Shell {
23
26
  strategy;
24
27
  tmpDir;
25
28
  constructor(opts) {
29
+ this.terminal = opts.terminal ?? processTerminal();
26
30
  // Build environment — filter out undefined values (node-pty's native
27
31
  // posix_spawnp fails if any env value is undefined)
28
32
  const env = {};
@@ -58,18 +62,10 @@ export class Shell {
58
62
  this.tmpDir = spawnConfig.tmpDir;
59
63
  Object.assign(env, spawnConfig.envOverrides);
60
64
  const shellArgs = spawnConfig.args;
61
- // Pause stdin before spawning PTY to avoid TTY contention on macOS.
62
- // The PTY will become the controlling terminal for the child shell.
63
- const wasRaw = process.stdin.isTTY && process.stdin.isRaw;
64
- if (process.stdin.isTTY) {
65
- try {
66
- process.stdin.setRawMode(false);
67
- process.stdin.pause();
68
- }
69
- catch {
70
- // Ignore
71
- }
72
- }
65
+ // The PTY will become the controlling terminal for the child shell;
66
+ // suspend the host terminal's input around spawn to avoid TTY contention
67
+ // on macOS. Headless terminals make this a no-op.
68
+ const suspended = this.terminal.suspendInput?.();
73
69
  this.ptyProcess = pty.spawn(shellBin, shellArgs, {
74
70
  name: "xterm-256color",
75
71
  cols: opts.cols,
@@ -77,22 +73,13 @@ export class Shell {
77
73
  cwd: opts.cwd,
78
74
  env,
79
75
  });
80
- // Restore stdin after PTY is created
81
- if (process.stdin.isTTY) {
82
- try {
83
- process.stdin.resume();
84
- if (wasRaw) {
85
- process.stdin.setRawMode(true);
86
- }
87
- }
88
- catch {
89
- // Ignore - will be set up later in index.ts
90
- }
91
- }
76
+ suspended?.resume();
92
77
  clearOpost();
93
78
  this.bus = opts.bus;
94
79
  this.handlers = opts.handlers;
95
- this.outputParser = new OutputParser(opts.bus, opts.cwd, instanceTag);
80
+ this.outputParser = new OutputParser(opts.bus, opts.cwd, instanceTag, {
81
+ cleanOutput: this.strategy.cleanOutput?.bind(this.strategy),
82
+ });
96
83
  // Ensure temp dir cleanup on abnormal exit (SIGKILL won't fire this,
97
84
  // but it covers uncaught exceptions and normal process.exit paths)
98
85
  if (this.tmpDir) {
@@ -259,15 +246,14 @@ export class Shell {
259
246
  this.pendingEchoSkips--;
260
247
  const rest = data.slice(nlIdx + 1);
261
248
  if (rest)
262
- process.stdout.write(rest);
249
+ this.terminal.write(rest);
263
250
  return;
264
251
  }
265
- process.stdout.write(data);
252
+ this.terminal.write(data);
266
253
  });
267
254
  }
268
255
  setupInput() {
269
- process.stdin.on("data", (data) => {
270
- const str = data.toString("utf-8");
256
+ this.inputDispose = this.terminal.onInput((str) => {
271
257
  this.inputHandler.handleInput(str);
272
258
  });
273
259
  }
@@ -304,7 +290,7 @@ export class Shell {
304
290
  this.bus.onPipeAsync("shell:exec-request", async (payload) => {
305
291
  const visible = this.acquireUnmute("exec-request");
306
292
  this.skipNextLine();
307
- process.stdout.write("\r\n");
293
+ this.terminal.write("\r\n");
308
294
  this.bus.emit("shell:agent-exec-start", {});
309
295
  try {
310
296
  const output = await new Promise((resolve, reject) => {
@@ -347,6 +333,8 @@ export class Shell {
347
333
  this.ptyProcess.onExit(callback);
348
334
  }
349
335
  kill() {
336
+ this.inputDispose?.();
337
+ this.inputDispose = null;
350
338
  this.ptyProcess.kill();
351
339
  if (this.tmpDir) {
352
340
  fs.rmSync(this.tmpDir, { recursive: true, force: true });
@@ -47,4 +47,10 @@ export interface ShellStrategy {
47
47
  * Returns null if the shell can't redraw — caller falls back to freshPrompt.
48
48
  */
49
49
  redrawEscape(): string | null;
50
+ /**
51
+ * Strip shell-specific artifacts from raw PTY output before stripAnsi
52
+ * collapses SGR codes (e.g. zsh's PROMPT_SP inverse-video `%`). Default
53
+ * is identity — most shells need no cleanup.
54
+ */
55
+ cleanOutput?(raw: string): string;
50
56
  }
@@ -69,4 +69,11 @@ export const zshStrategy = {
69
69
  redrawEscape() {
70
70
  return "\x1b[9999~";
71
71
  },
72
+ cleanOutput(raw) {
73
+ return raw.replace(PROMPT_SP_RE, "");
74
+ },
72
75
  };
76
+ /** PROMPT_SP marker (inverse-video PROMPT_EOL_MARK, default `%`) zsh prints
77
+ * before a prompt when prior output didn't end at column 0. Matching the
78
+ * inverse-video wrapper preserves legitimate trailing `%`. */
79
+ const PROMPT_SP_RE = /\x1b\[7m.\x1b\[(?:0|27)m/g;
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Terminal — the user-facing I/O endpoint that a Shell talks to.
3
+ *
4
+ * Shell wraps a *pseudo*-terminal (the PTY the child shell sees). This
5
+ * interface is the *real* terminal (or its substitute) on the other end:
6
+ * bytes in, bytes out, dimensions, resize notifications. The default
7
+ * factory wires it to process.stdin/stdout for the CLI; headless hosts
8
+ * (multi-session web hubs, tests) supply their own.
9
+ */
10
+ import type { RenderSurface } from "../utils/compositor.js";
11
+ export interface Terminal {
12
+ write(data: string): void;
13
+ onInput(cb: (data: string) => void): () => void;
14
+ onResize(cb: (cols: number, rows: number) => void): () => void;
15
+ cols(): number;
16
+ rows(): number;
17
+ /**
18
+ * Called around PTY spawn to avoid TTY contention: the child PTY becomes
19
+ * the controlling tty for the spawned shell. No-op when the terminal
20
+ * isn't a real tty.
21
+ */
22
+ suspendInput?(): {
23
+ resume(): void;
24
+ };
25
+ }
26
+ /** Default Terminal: wraps process.stdin/stdout. */
27
+ export declare function processTerminal(): Terminal;
28
+ /**
29
+ * Adapt a Terminal to a RenderSurface (the compositor's sink type). Adds
30
+ * the OPOST-cleared `\n` → `\r\n` translation that StdoutSurface applies,
31
+ * since the PTY has OPOST disabled.
32
+ */
33
+ export declare function surfaceFromTerminal(terminal: Terminal): RenderSurface;
@@ -0,0 +1,62 @@
1
+ /** Default Terminal: wraps process.stdin/stdout. */
2
+ export function processTerminal() {
3
+ return {
4
+ write(data) {
5
+ if (process.stdout.writable) {
6
+ try {
7
+ process.stdout.write(data);
8
+ }
9
+ catch { /* ignore */ }
10
+ }
11
+ },
12
+ onInput(cb) {
13
+ const handler = (b) => cb(b.toString("utf-8"));
14
+ process.stdin.on("data", handler);
15
+ return () => { process.stdin.off("data", handler); };
16
+ },
17
+ onResize(cb) {
18
+ const handler = () => cb(process.stdout.columns || 80, process.stdout.rows || 24);
19
+ process.stdout.on("resize", handler);
20
+ return () => { process.stdout.off("resize", handler); };
21
+ },
22
+ cols() { return process.stdout.columns || 80; },
23
+ rows() { return process.stdout.rows || 24; },
24
+ suspendInput() {
25
+ const wasRaw = process.stdin.isTTY && process.stdin.isRaw;
26
+ if (process.stdin.isTTY) {
27
+ try {
28
+ process.stdin.setRawMode(false);
29
+ process.stdin.pause();
30
+ }
31
+ catch { /* ignore */ }
32
+ }
33
+ return {
34
+ resume() {
35
+ if (process.stdin.isTTY) {
36
+ try {
37
+ process.stdin.resume();
38
+ if (wasRaw)
39
+ process.stdin.setRawMode(true);
40
+ }
41
+ catch { /* ignore */ }
42
+ }
43
+ },
44
+ };
45
+ },
46
+ };
47
+ }
48
+ /**
49
+ * Adapt a Terminal to a RenderSurface (the compositor's sink type). Adds
50
+ * the OPOST-cleared `\n` → `\r\n` translation that StdoutSurface applies,
51
+ * since the PTY has OPOST disabled.
52
+ */
53
+ export function surfaceFromTerminal(terminal) {
54
+ const write = (text) => terminal.write(text.replace(/(?<!\r)\n/g, "\r\n"));
55
+ return {
56
+ write,
57
+ writeLine: (line) => write(line + "\n"),
58
+ get columns() { return terminal.cols(); },
59
+ get rows() { return terminal.rows(); },
60
+ onResize: (cb) => terminal.onResize(cb),
61
+ };
62
+ }