agent-sh 0.14.0 → 0.14.2

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 (64) hide show
  1. package/README.md +7 -18
  2. package/dist/agent/agent-loop.d.ts +1 -1
  3. package/dist/agent/agent-loop.js +42 -31
  4. package/dist/agent/conversation-state.d.ts +3 -2
  5. package/dist/agent/conversation-state.js +20 -3
  6. package/dist/agent/events.d.ts +2 -0
  7. package/dist/agent/host-types.d.ts +3 -0
  8. package/dist/agent/index.js +2 -1
  9. package/dist/agent/llm-client.js +1 -0
  10. package/dist/agent/subagent.d.ts +1 -1
  11. package/dist/agent/subagent.js +5 -1
  12. package/dist/agent/tool-protocol.d.ts +2 -2
  13. package/dist/agent/tool-protocol.js +5 -4
  14. package/dist/agent/tools/glob.d.ts +1 -1
  15. package/dist/agent/tools/glob.js +4 -2
  16. package/dist/agent/tools/grep.d.ts +1 -1
  17. package/dist/agent/tools/grep.js +4 -2
  18. package/dist/agent/tools/ls.d.ts +1 -1
  19. package/dist/agent/tools/ls.js +4 -2
  20. package/dist/agent/tools/read-file.d.ts +1 -1
  21. package/dist/agent/tools/read-file.js +30 -2
  22. package/dist/agent/types.d.ts +13 -3
  23. package/dist/agent/types.js +6 -1
  24. package/dist/cli/args.js +3 -1
  25. package/dist/cli/index.js +0 -0
  26. package/dist/cli/install.d.ts +1 -0
  27. package/dist/cli/install.js +86 -2
  28. package/dist/cli/subcommands.js +4 -1
  29. package/dist/core/index.d.ts +1 -1
  30. package/dist/core/settings.d.ts +3 -0
  31. package/dist/core/settings.js +2 -2
  32. package/dist/shell/index.d.ts +6 -0
  33. package/dist/shell/index.js +10 -10
  34. package/dist/shell/shell.d.ts +4 -0
  35. package/dist/shell/shell.js +15 -29
  36. package/dist/shell/terminal.d.ts +33 -0
  37. package/dist/shell/terminal.js +62 -0
  38. package/dist/utils/tool-interactive.js +4 -2
  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 +25 -8
  46. package/examples/extensions/ashi/src/compaction.ts +25 -96
  47. package/examples/extensions/ashi/src/components.ts +64 -166
  48. package/examples/extensions/ashi/src/default-schema-renderers.ts +229 -0
  49. package/examples/extensions/ashi/src/display-config.ts +21 -22
  50. package/examples/extensions/ashi/src/frontend.ts +64 -65
  51. package/examples/extensions/ashi/src/hooks.ts +47 -63
  52. package/examples/extensions/ashi/src/multi-session-store.ts +44 -3
  53. package/examples/extensions/ashi/src/schema.ts +407 -0
  54. package/examples/extensions/ashi/src/session-store.ts +55 -4
  55. package/examples/extensions/ashi/src/status-footer.ts +27 -6
  56. package/examples/extensions/ashi-compact-llm.ts +93 -0
  57. package/examples/extensions/claude-code-bridge/index.ts +9 -2
  58. package/examples/extensions/claude-code-bridge/package.json +1 -1
  59. package/examples/extensions/opencode-bridge/index.ts +208 -53
  60. package/examples/extensions/opencode-bridge/package.json +1 -1
  61. package/examples/extensions/opencode-provider.ts +252 -0
  62. package/examples/extensions/pi-bridge/index.ts +1 -0
  63. package/package.json +12 -1
  64. package/examples/extensions/ashi/src/default-renderers.ts +0 -171
@@ -75,6 +75,84 @@ function readPackageJson(target) {
75
75
  return null;
76
76
  return JSON.parse(fs.readFileSync(pkgJson, "utf-8"));
77
77
  }
78
+ function hostAgentShVersion() {
79
+ try {
80
+ const pkg = JSON.parse(fs.readFileSync(path.join(PACKAGE_ROOT, "package.json"), "utf-8"));
81
+ return typeof pkg.version === "string" ? pkg.version : null;
82
+ }
83
+ catch {
84
+ return null;
85
+ }
86
+ }
87
+ function satisfies(version, spec) {
88
+ if (spec === version || spec === "*" || spec === "latest")
89
+ return true;
90
+ const [vMaj, vMin, vPatch] = version.split(/[.-]/, 3).map(Number);
91
+ if ([vMaj, vMin, vPatch].some(Number.isNaN))
92
+ return true;
93
+ const m = spec.match(/^([\^~]?)(\d+)\.(\d+)\.(\d+)/);
94
+ if (!m)
95
+ return true;
96
+ const op = m[1];
97
+ const sMaj = Number(m[2]);
98
+ const sMin = Number(m[3]);
99
+ const sPatch = Number(m[4]);
100
+ if (op === "")
101
+ return vMaj === sMaj && vMin === sMin && vPatch === sPatch;
102
+ if (op === "~")
103
+ return vMaj === sMaj && vMin === sMin && vPatch >= sPatch;
104
+ // ^x.y.z: zero-major treats minor as the breaking boundary (npm rule).
105
+ if (sMaj > 0)
106
+ return vMaj === sMaj && (vMin > sMin || (vMin === sMin && vPatch >= sPatch));
107
+ if (sMin > 0)
108
+ return vMaj === 0 && vMin === sMin && vPatch >= sPatch;
109
+ return vMaj === 0 && vMin === 0 && vPatch === sPatch;
110
+ }
111
+ /** Warn when the extension's `agent-sh` pin can't admit the host version;
112
+ * only rewrite when --sync-deps is set. */
113
+ function syncAgentShVersion(target, syncDeps) {
114
+ const hostVersion = hostAgentShVersion();
115
+ if (!hostVersion)
116
+ return;
117
+ // Prerelease hosts aren't on npm; rewriting would leave npm install unable to resolve.
118
+ if (hostVersion.includes("-"))
119
+ return;
120
+ const pkgJson = path.join(target, "package.json");
121
+ if (!fs.existsSync(pkgJson))
122
+ return;
123
+ const raw = fs.readFileSync(pkgJson, "utf-8");
124
+ const pkg = JSON.parse(raw);
125
+ const sections = ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"];
126
+ const name = path.basename(target);
127
+ let changed = false;
128
+ let warned = false;
129
+ for (const section of sections) {
130
+ const deps = pkg[section];
131
+ if (!deps || typeof deps !== "object")
132
+ continue;
133
+ const d = deps;
134
+ const current = d["agent-sh"];
135
+ if (typeof current !== "string")
136
+ continue;
137
+ if (current.startsWith("file:"))
138
+ continue;
139
+ if (satisfies(hostVersion, current))
140
+ continue;
141
+ if (syncDeps) {
142
+ console.log(`agent-sh: rewriting ${name} agent-sh ${current} -> ${hostVersion}.`);
143
+ d["agent-sh"] = hostVersion;
144
+ changed = true;
145
+ }
146
+ else if (!warned) {
147
+ console.warn(`agent-sh: ${name} pins agent-sh ${current}, which doesn't admit host ${hostVersion}. ` +
148
+ `npm install will land an older agent-sh inside the extension and drift from the running host. ` +
149
+ `Re-run with --sync-deps to rewrite the pin to ${hostVersion}, or update the bridge's source pin.`);
150
+ warned = true;
151
+ }
152
+ }
153
+ if (changed)
154
+ fs.writeFileSync(pkgJson, `${JSON.stringify(pkg, null, 2)}\n`);
155
+ }
78
156
  /** Relative `file:` deps in bundled extensions (e.g. `"agent-sh": "file:../../.."`)
79
157
  * point at the wrong location after the source is copied into ~/.agent-sh/extensions/.
80
158
  * Resolve them against the original source dir so npm install in the target succeeds. */
@@ -165,7 +243,7 @@ function linkBins(target, pkg) {
165
243
  }
166
244
  export async function runInstall(spec, opts = {}) {
167
245
  if (!spec) {
168
- console.error("Usage: agent-sh install <name|file:|npm:|github:> [--force]\n\n" +
246
+ console.error("Usage: agent-sh install <name|file:|npm:|github:> [--force] [--sync-deps]\n\n" +
169
247
  "Bundled extensions:\n" +
170
248
  listBundled()
171
249
  .map((n) => ` ${n}`)
@@ -191,9 +269,15 @@ export async function runInstall(spec, opts = {}) {
191
269
  }
192
270
  let linkedBins = [];
193
271
  if (resolved.isDirectory) {
194
- fs.cpSync(resolved.sourcePath, target, { recursive: true });
272
+ fs.cpSync(resolved.sourcePath, target, {
273
+ recursive: true,
274
+ // Skip source node_modules: maybeNpmInstall short-circuits on
275
+ // existing node_modules, silently leaving the bridge's deps stale.
276
+ filter: (src) => path.basename(src) !== "node_modules",
277
+ });
195
278
  try {
196
279
  rewriteFileDeps(target, resolved.sourcePath);
280
+ syncAgentShVersion(target, opts.syncDeps ?? false);
197
281
  const pkg = readPackageJson(target);
198
282
  if (pkg) {
199
283
  maybeNpmInstall(target, pkg);
@@ -3,7 +3,10 @@ import { runInstall, runUninstall, runList } from "./install.js";
3
3
  import { runAuth } from "./auth/cli.js";
4
4
  const SUBCOMMANDS = {
5
5
  init: (args) => runInit({ force: args.includes("--force") }),
6
- install: (args) => runInstall(args[0] ?? "", { force: args.includes("--force") }),
6
+ install: (args) => runInstall(args[0] ?? "", {
7
+ force: args.includes("--force"),
8
+ syncDeps: args.includes("--sync-deps"),
9
+ }),
7
10
  uninstall: (args) => runUninstall(args[0] ?? ""),
8
11
  list: () => runList(),
9
12
  auth: (args) => runAuth(args),
@@ -17,7 +17,7 @@ export type { AgentContext, AgentConfig, AgentSurface, AgentConfigSurface, Agent
17
17
  export type { ShellContext, ShellConfig, ShellSurface, ShellConfigSurface, ExtensionContext, RemoteSession, RemoteSessionOptions, RenderSurface, InputModeConfig, TerminalSession, BlockTransformOptions, FencedBlockTransformOptions, AppConfig } from "../shell/host-types.js";
18
18
  export { palette, setPalette, resetPalette } from "../utils/palette.js";
19
19
  export type { ColorPalette } from "../utils/palette.js";
20
- export type { AgentBackend, ToolDefinition } from "../agent/types.js";
20
+ export type { AgentBackend, ToolDefinition, ImageContent } from "../agent/types.js";
21
21
  export { runSubagent, type SubagentOptions } from "../agent/subagent.js";
22
22
  export { LlmClient } from "../agent/llm-client.js";
23
23
  export { HistoryFile, InMemoryHistory, NoopHistory, type HistoryAdapter } from "../agent/history-file.js";
@@ -13,6 +13,8 @@ export interface ModelCapabilityConfig {
13
13
  maxTokens?: number;
14
14
  /** Echo reasoning_content back on assistant turns. Required by DeepSeek. */
15
15
  echoReasoning?: boolean;
16
+ /** Content modalities the model supports (e.g. ["text", "image"]). */
17
+ modalities?: ("text" | "image")[];
16
18
  }
17
19
  /** Provider profile — a named LLM configuration. */
18
20
  export interface ProviderConfig {
@@ -163,6 +165,7 @@ export interface ResolvedProvider {
163
165
  contextWindow?: number;
164
166
  maxTokens?: number;
165
167
  echoReasoning?: boolean;
168
+ modalities?: ("text" | "image")[];
166
169
  }>;
167
170
  /** Borrow another registered provider's reasoning request shape by id. */
168
171
  reasoningShape?: string;
@@ -150,8 +150,8 @@ export function resolveProvider(name) {
150
150
  }
151
151
  else {
152
152
  modelIds.push(m.id);
153
- if (m.reasoning !== undefined || m.contextWindow !== undefined || m.maxTokens !== undefined || m.echoReasoning !== undefined) {
154
- caps.set(m.id, { reasoning: m.reasoning, contextWindow: m.contextWindow, maxTokens: m.maxTokens, echoReasoning: m.echoReasoning });
153
+ if (m.reasoning !== undefined || m.contextWindow !== undefined || m.maxTokens !== undefined || m.echoReasoning !== undefined || m.modalities !== undefined) {
154
+ caps.set(m.id, { reasoning: m.reasoning, contextWindow: m.contextWindow, maxTokens: m.maxTokens, echoReasoning: m.echoReasoning, modalities: m.modalities });
155
155
  }
156
156
  }
157
157
  }
@@ -5,6 +5,7 @@
5
5
  */
6
6
  import "./events.js";
7
7
  import type { ExtensionContext } from "./host-types.js";
8
+ import { type Terminal } from "./terminal.js";
8
9
  export interface ShellActivateOptions {
9
10
  cols: number;
10
11
  rows: number;
@@ -16,6 +17,11 @@ export interface ShellActivateOptions {
16
17
  info: string;
17
18
  model?: string;
18
19
  };
20
+ /**
21
+ * Host-side I/O endpoint. Defaults to processTerminal() so the CLI
22
+ * works unchanged; headless callers (web hubs, tests) supply their own.
23
+ */
24
+ terminal?: Terminal;
19
25
  }
20
26
  export interface ShellHandle {
21
27
  /** Terminate the PTY. */
@@ -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,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,18 +73,7 @@ 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;
@@ -259,15 +244,14 @@ export class Shell {
259
244
  this.pendingEchoSkips--;
260
245
  const rest = data.slice(nlIdx + 1);
261
246
  if (rest)
262
- process.stdout.write(rest);
247
+ this.terminal.write(rest);
263
248
  return;
264
249
  }
265
- process.stdout.write(data);
250
+ this.terminal.write(data);
266
251
  });
267
252
  }
268
253
  setupInput() {
269
- process.stdin.on("data", (data) => {
270
- const str = data.toString("utf-8");
254
+ this.inputDispose = this.terminal.onInput((str) => {
271
255
  this.inputHandler.handleInput(str);
272
256
  });
273
257
  }
@@ -304,7 +288,7 @@ export class Shell {
304
288
  this.bus.onPipeAsync("shell:exec-request", async (payload) => {
305
289
  const visible = this.acquireUnmute("exec-request");
306
290
  this.skipNextLine();
307
- process.stdout.write("\r\n");
291
+ this.terminal.write("\r\n");
308
292
  this.bus.emit("shell:agent-exec-start", {});
309
293
  try {
310
294
  const output = await new Promise((resolve, reject) => {
@@ -347,6 +331,8 @@ export class Shell {
347
331
  this.ptyProcess.onExit(callback);
348
332
  }
349
333
  kill() {
334
+ this.inputDispose?.();
335
+ this.inputDispose = null;
350
336
  this.ptyProcess.kill();
351
337
  if (this.tmpDir) {
352
338
  fs.rmSync(this.tmpDir, { recursive: true, force: true });
@@ -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
+ }
@@ -14,7 +14,6 @@ export function createToolUI(bus, surface) {
14
14
  if (finished)
15
15
  return;
16
16
  finished = true;
17
- clearLines(surface, prevLineCount);
18
17
  bus.offPipe("input:intercept", interceptor);
19
18
  bus.emit("shell:stdout-hide", {});
20
19
  bus.emit("tool:interactive-end", {});
@@ -45,7 +44,10 @@ export function createToolUI(bus, surface) {
45
44
  bus.emit("tool:interactive-start", {});
46
45
  bus.emit("shell:stdout-show", {});
47
46
  bus.onPipe("input:intercept", interceptor);
48
- session.onMount?.(() => render());
47
+ // Drop to a fresh row in case the cursor was mid-line; uncounted
48
+ // so clearLines on dismiss stops at the gap, not above it.
49
+ surface.write("\n");
50
+ session.onMount?.(() => render(), done);
49
51
  render();
50
52
  });
51
53
  },