agent-sh 0.8.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/README.md +27 -43
  2. package/dist/agent/agent-loop.d.ts +69 -6
  3. package/dist/agent/agent-loop.js +954 -153
  4. package/dist/agent/conversation-state.d.ts +74 -21
  5. package/dist/agent/conversation-state.js +361 -150
  6. package/dist/agent/history-file.d.ts +13 -4
  7. package/dist/agent/history-file.js +110 -36
  8. package/dist/agent/nuclear-form.d.ts +28 -3
  9. package/dist/agent/nuclear-form.js +88 -6
  10. package/dist/agent/skills.d.ts +2 -4
  11. package/dist/agent/skills.js +10 -4
  12. package/dist/agent/subagent.d.ts +23 -0
  13. package/dist/agent/subagent.js +53 -11
  14. package/dist/agent/system-prompt.d.ts +37 -5
  15. package/dist/agent/system-prompt.js +100 -67
  16. package/dist/{token-budget.d.ts → agent/token-budget.d.ts} +5 -4
  17. package/dist/{token-budget.js → agent/token-budget.js} +15 -20
  18. package/dist/agent/tool-protocol.d.ts +105 -0
  19. package/dist/agent/tool-protocol.js +551 -0
  20. package/dist/agent/tools/bash.js +3 -3
  21. package/dist/agent/tools/edit-file.js +9 -6
  22. package/dist/agent/tools/glob.js +4 -2
  23. package/dist/agent/tools/grep.js +27 -3
  24. package/dist/agent/tools/ls.js +5 -6
  25. package/dist/agent/types.d.ts +22 -2
  26. package/dist/context-manager.d.ts +17 -0
  27. package/dist/context-manager.js +37 -4
  28. package/dist/core.d.ts +7 -7
  29. package/dist/core.js +99 -196
  30. package/dist/event-bus.d.ts +85 -2
  31. package/dist/event-bus.js +20 -1
  32. package/dist/executor.d.ts +4 -3
  33. package/dist/executor.js +18 -15
  34. package/dist/extension-loader.d.ts +5 -0
  35. package/dist/extension-loader.js +143 -19
  36. package/dist/extensions/agent-backend.d.ts +14 -0
  37. package/dist/extensions/agent-backend.js +188 -0
  38. package/dist/extensions/command-suggest.d.ts +3 -3
  39. package/dist/extensions/command-suggest.js +4 -3
  40. package/dist/extensions/index.d.ts +19 -0
  41. package/dist/extensions/index.js +24 -0
  42. package/dist/extensions/slash-commands.d.ts +1 -1
  43. package/dist/extensions/slash-commands.js +30 -10
  44. package/dist/extensions/tui-renderer.js +117 -113
  45. package/dist/index.js +39 -26
  46. package/dist/settings.d.ts +40 -3
  47. package/dist/settings.js +57 -10
  48. package/dist/{input-handler.d.ts → shell/input-handler.d.ts} +3 -2
  49. package/dist/{input-handler.js → shell/input-handler.js} +111 -85
  50. package/dist/{output-parser.d.ts → shell/output-parser.d.ts} +1 -1
  51. package/dist/{output-parser.js → shell/output-parser.js} +1 -1
  52. package/dist/{shell.d.ts → shell/shell.d.ts} +8 -2
  53. package/dist/{shell.js → shell/shell.js} +39 -8
  54. package/dist/types.d.ts +61 -10
  55. package/dist/utils/ansi.d.ts +5 -0
  56. package/dist/utils/ansi.js +1 -1
  57. package/dist/utils/compositor.d.ts +67 -0
  58. package/dist/utils/compositor.js +116 -0
  59. package/dist/utils/diff-renderer.d.ts +9 -0
  60. package/dist/utils/diff-renderer.js +312 -146
  61. package/dist/utils/diff.d.ts +21 -2
  62. package/dist/utils/diff.js +165 -89
  63. package/dist/utils/floating-panel.d.ts +2 -0
  64. package/dist/utils/floating-panel.js +30 -14
  65. package/dist/utils/handler-registry.d.ts +31 -10
  66. package/dist/utils/handler-registry.js +58 -16
  67. package/dist/utils/line-editor.d.ts +33 -3
  68. package/dist/utils/line-editor.js +221 -44
  69. package/dist/utils/markdown.d.ts +1 -0
  70. package/dist/utils/markdown.js +1 -1
  71. package/dist/utils/message-utils.d.ts +35 -0
  72. package/dist/utils/message-utils.js +75 -0
  73. package/dist/utils/terminal-buffer.d.ts +5 -1
  74. package/dist/utils/terminal-buffer.js +18 -2
  75. package/dist/utils/tool-display.d.ts +1 -1
  76. package/dist/utils/tool-display.js +4 -4
  77. package/dist/utils/tool-interactive.d.ts +12 -0
  78. package/dist/utils/tool-interactive.js +53 -0
  79. package/examples/extensions/ash-acp-bridge/README.md +39 -0
  80. package/examples/extensions/ash-acp-bridge/package.json +23 -0
  81. package/examples/extensions/ash-acp-bridge/src/index.ts +574 -0
  82. package/examples/extensions/ash-acp-bridge/tsconfig.json +14 -0
  83. package/examples/extensions/ash-mcp-bridge/README.md +72 -0
  84. package/examples/extensions/ash-mcp-bridge/index.ts +164 -0
  85. package/examples/extensions/ash-mcp-bridge/package.json +9 -0
  86. package/examples/extensions/claude-code-bridge/index.ts +198 -51
  87. package/examples/extensions/claude-code-bridge/package.json +1 -0
  88. package/examples/extensions/interactive-prompts.ts +98 -112
  89. package/examples/extensions/overlay-agent.ts +84 -38
  90. package/examples/extensions/peer-mesh.ts +565 -0
  91. package/examples/extensions/pi-bridge/index.ts +2 -2
  92. package/examples/extensions/questionnaire.ts +260 -0
  93. package/examples/extensions/subagents.ts +19 -4
  94. package/examples/extensions/terminal-buffer.ts +32 -53
  95. package/examples/extensions/tmux-pane.ts +307 -0
  96. package/examples/extensions/user-shell.ts +136 -0
  97. package/examples/extensions/web-access.ts +335 -0
  98. package/package.json +44 -2
  99. package/dist/agent/tools/display.d.ts +0 -13
  100. package/dist/agent/tools/display.js +0 -70
  101. package/dist/agent/tools/user-shell.d.ts +0 -13
  102. package/dist/agent/tools/user-shell.js +0 -87
  103. package/dist/extensions/overlay-agent.d.ts +0 -14
  104. package/dist/extensions/overlay-agent.js +0 -147
  105. package/dist/extensions/terminal-buffer.d.ts +0 -14
  106. package/dist/extensions/terminal-buffer.js +0 -125
@@ -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)
@@ -195,6 +197,11 @@ export class Shell {
195
197
  * For bash, falls back to sending \n for a fresh prompt cycle.
196
198
  */
197
199
  redrawPrompt() {
200
+ // A stale echoSkip or paused flag (left over from handleProcessingDone
201
+ // re-entering a mode) would swallow the redrawn prompt and make the
202
+ // terminal appear frozen. Reset both before emitting.
203
+ this.echoSkip = false;
204
+ this.paused = false;
198
205
  const result = this.bus.emitPipe("shell:redraw-prompt", {
199
206
  cwd: this.outputParser.getCwd(),
200
207
  handled: false,
@@ -225,7 +232,9 @@ export class Shell {
225
232
  });
226
233
  if (!result.handled) {
227
234
  this.ptyProcess.write("\n");
235
+ return true;
228
236
  }
237
+ return false;
229
238
  }
230
239
  onCommandEntered(command, cwd) {
231
240
  this.outputParser.onCommandEntered(command, cwd);
@@ -265,18 +274,32 @@ export class Shell {
265
274
  * zero frontend knowledge; any frontend can subscribe to the same events.
266
275
  */
267
276
  setupAgentLifecycle() {
268
- this.bus.on("agent:processing-start", () => {
277
+ // Default agent lifecycle: pause the shell while the agent works,
278
+ // then redraw the prompt when done. Extensions advise these handlers
279
+ // to change behavior (e.g. tmux split keeps the shell interactive).
280
+ this.handlers.define("shell:on-processing-start", () => {
269
281
  this.agentActive = true;
270
282
  this.paused = true;
271
283
  });
272
- this.bus.on("agent:processing-done", () => {
273
- this.paused = false;
284
+ this.handlers.define("shell:on-processing-done", () => {
274
285
  this.agentActive = false;
275
- this.echoSkip = true;
286
+ // If handleProcessingDone re-entered a mode, leave stdout paused so
287
+ // stale PTY output doesn't overwrite the mode prompt (exitMode →
288
+ // redrawPrompt will unpause). Setting echoSkip here would swallow
289
+ // that PTY output since no \n was sent.
276
290
  if (!this.inputHandler.handleProcessingDone()) {
277
- this.freshPrompt();
291
+ this.paused = false;
292
+ if (this.freshPrompt()) {
293
+ this.echoSkip = true;
294
+ }
278
295
  }
279
296
  });
297
+ this.bus.on("agent:processing-start", () => {
298
+ this.handlers.call("shell:on-processing-start");
299
+ });
300
+ this.bus.on("agent:processing-done", () => {
301
+ this.handlers.call("shell:on-processing-done");
302
+ });
280
303
  // Permission prompts need stdout unpaused so the interactive UI renders,
281
304
  // then re-paused after the decision.
282
305
  this.bus.on("permission:request", () => {
@@ -303,11 +326,19 @@ export class Shell {
303
326
  const handler = (e) => {
304
327
  clearTimeout(timeout);
305
328
  this.bus.off("shell:command-done", handler);
329
+ // Re-pause stdout so the prompt text following the marker doesn't
330
+ // leak to the terminal while the agent is still processing.
331
+ this.paused = true;
306
332
  resolve({ output: e.output, cwd: e.cwd, exitCode: e.exitCode });
307
333
  };
308
334
  this.bus.on("shell:command-done", handler);
309
335
  this.outputParser.onCommandEntered(payload.command, this.outputParser.getCwd());
310
- this.ptyProcess.write(payload.command + "\r");
336
+ // Collapse literal newlines to spaces so the PTY receives a single-line
337
+ // command. Multi-line commands (e.g. git commit -m "...\n...") would
338
+ // cause the shell to execute prematurely, producing garbled output from
339
+ // syntax highlighting plugins (zsh syntax highlighting, etc).
340
+ const oneLine = payload.command.replace(/\n/g, " ");
341
+ this.ptyProcess.write(oneLine + "\r");
311
342
  });
312
343
  this.paused = true;
313
344
  this.echoSkip = false;
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;
@@ -56,29 +80,56 @@ export interface ExtensionContext {
56
80
  createFencedBlockTransform: (opts: FencedBlockTransformOptions) => void;
57
81
  /** Read extension-namespaced settings from ~/.agent-sh/settings.json. */
58
82
  getExtensionSettings: <T extends Record<string, unknown>>(namespace: string, defaults: T) => T;
83
+ /**
84
+ * Get (and lazily create) a per-extension storage directory under
85
+ * ~/.agent-sh/<namespace>/. Returns the absolute path. Lets extensions
86
+ * persist state without each one re-deriving the location.
87
+ */
88
+ getStoragePath: (namespace: string) => string;
59
89
  /** Register a slash command available in any input mode. */
60
90
  registerCommand: (name: string, description: string, handler: (args: string) => Promise<void> | void) => void;
61
91
  /** Register a tool for the built-in agent. No-op when using bridge backends. */
62
92
  registerTool: (tool: ToolDefinition) => void;
93
+ /** Unregister a tool by name. */
94
+ unregisterTool: (name: string) => void;
63
95
  /** Get all registered tools (for subagent tool subsets). Returns [] when using bridge backends. */
64
96
  getTools: () => ToolDefinition[];
97
+ /** Register a named instruction block for the agent's system prompt. */
98
+ registerInstruction: (name: string, text: string) => void;
99
+ /** Remove a named instruction block from the system prompt. */
100
+ removeInstruction: (name: string) => void;
101
+ /** Register a skill (on-demand reference material) for the agent. */
102
+ registerSkill: (name: string, description: string, filePath: string) => void;
103
+ /** Remove a registered skill by name. */
104
+ removeSkill: (name: string) => void;
65
105
  /** Register a named handler. */
66
106
  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;
107
+ /** Wrap a named handler. Receives `next` (original) + args. Returns an unadvise function. */
108
+ advise: (name: string, wrapper: (next: (...args: any[]) => any, ...args: any[]) => any) => () => void;
69
109
  /** Call a named handler. */
70
110
  call: (name: string, ...args: any[]) => any;
111
+ /** Names of all registered handlers — for diagnostic / introspection use. */
112
+ list: () => string[];
71
113
  /**
72
114
  * Shared headless terminal buffer mirroring PTY output.
73
115
  * Lazily created on first access. Returns null if @xterm/headless is not installed.
74
116
  */
75
117
  terminalBuffer: TerminalBuffer | null;
76
118
  /**
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.
119
+ * Routes named render streams ("agent", "query", "status") to surfaces.
120
+ * Extensions use `compositor.redirect()` to capture output (e.g. overlay panels).
121
+ */
122
+ compositor: Compositor;
123
+ /**
124
+ * Create a remote session that routes agent output to a surface and
125
+ * optionally accepts queries. Handles all compositor routing, shell
126
+ * lifecycle advisors, and chrome suppression.
127
+ *
128
+ * const session = ctx.createRemoteSession({ surface, interactive: true });
129
+ * session.submit("what's on screen?");
130
+ * session.close(); // restores everything
80
131
  */
81
- createFloatingPanel: (config: FloatingPanelConfig) => FloatingPanel;
132
+ createRemoteSession: (opts: RemoteSessionOptions) => RemoteSession;
82
133
  }
83
134
  /**
84
135
  * Configuration for a registered input mode.
@@ -6,6 +6,11 @@ export declare const RED = "\u001B[31m";
6
6
  export declare const GRAY = "\u001B[90m";
7
7
  export declare const BOLD = "\u001B[1m";
8
8
  export declare const RESET = "\u001B[0m";
9
+ /**
10
+ * Check if a Unicode code point is a wide character (CJK, fullwidth, emoji, etc.)
11
+ * Returns 2 for wide chars, 1 for normal chars.
12
+ */
13
+ export declare function charWidth(codePoint: number): number;
9
14
  /**
10
15
  * Measure visible string length in terminal columns.
11
16
  * Excludes SGR (color/style) sequences and accounts for CJK double-width chars.
@@ -12,7 +12,7 @@ export const RESET = "\x1b[0m";
12
12
  * Check if a Unicode code point is a wide character (CJK, fullwidth, emoji, etc.)
13
13
  * Returns 2 for wide chars, 1 for normal chars.
14
14
  */
15
- function charWidth(codePoint) {
15
+ export function charWidth(codePoint) {
16
16
  // CJK Unified Ideographs
17
17
  if (codePoint >= 0x4e00 && codePoint <= 0x9fff)
18
18
  return 2;
@@ -0,0 +1,67 @@
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
+ import type { EventBus } from "../event-bus.js";
28
+ /**
29
+ * A surface accepts rendered output. Stdout is a surface.
30
+ * A floating panel's content area is a surface. A test buffer is a surface.
31
+ */
32
+ export interface RenderSurface {
33
+ /** Raw write — supports \r, partial lines, escape codes. */
34
+ write(text: string): void;
35
+ /** Convenience: write + newline. */
36
+ writeLine(line: string): void;
37
+ /** Available width in columns. */
38
+ readonly columns: number;
39
+ }
40
+ export interface Compositor {
41
+ /** Get the currently active surface for a stream. */
42
+ surface(stream: string): RenderSurface;
43
+ /** Override routing: redirect a stream to a different surface.
44
+ * Returns a restore function that undoes the redirect. */
45
+ redirect(stream: string, target: RenderSurface): () => void;
46
+ /** Register the default surface for a stream. */
47
+ setDefault(stream: string, target: RenderSurface): void;
48
+ }
49
+ /** Silent sink — drops all output. Used when no surface is registered. */
50
+ export declare const nullSurface: RenderSurface;
51
+ /** Surface backed by process.stdout. */
52
+ export declare class StdoutSurface implements RenderSurface {
53
+ write(text: string): void;
54
+ writeLine(line: string): void;
55
+ get columns(): number;
56
+ }
57
+ export declare class DefaultCompositor implements Compositor {
58
+ private defaults;
59
+ private overrides;
60
+ private readonly bus?;
61
+ constructor(bus?: EventBus);
62
+ surface(stream: string): RenderSurface;
63
+ redirect(stream: string, target: RenderSurface): () => void;
64
+ setDefault(stream: string, target: RenderSurface): void;
65
+ /** Wrap a surface so writes emit `compositor:write` before delegating. */
66
+ private wrap;
67
+ }
@@ -0,0 +1,116 @@
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
+ /** Silent sink — drops all output. Used when no surface is registered. */
28
+ export const nullSurface = {
29
+ write() { },
30
+ writeLine() { },
31
+ get columns() { return 80; },
32
+ };
33
+ /** Surface backed by process.stdout. */
34
+ export class StdoutSurface {
35
+ write(text) {
36
+ if (process.stdout.writable) {
37
+ try {
38
+ process.stdout.write(text);
39
+ }
40
+ catch { /* ignore */ }
41
+ }
42
+ }
43
+ writeLine(line) {
44
+ this.write(line + "\n");
45
+ }
46
+ get columns() {
47
+ return process.stdout.columns || 80;
48
+ }
49
+ }
50
+ export class DefaultCompositor {
51
+ defaults = new Map();
52
+ overrides = new Map();
53
+ bus;
54
+ constructor(bus) {
55
+ this.bus = bus;
56
+ }
57
+ surface(stream) {
58
+ const stack = this.overrides.get(stream);
59
+ if (stack && stack.length > 0)
60
+ return stack[stack.length - 1];
61
+ if (this.defaults.has(stream))
62
+ return this.defaults.get(stream);
63
+ // Hierarchical fallback: "agent:diff" → "agent"
64
+ const colon = stream.lastIndexOf(":");
65
+ if (colon !== -1)
66
+ return this.surface(stream.slice(0, colon));
67
+ return nullSurface;
68
+ }
69
+ redirect(stream, target) {
70
+ const wrapped = this.wrap(stream, target);
71
+ let stack = this.overrides.get(stream);
72
+ if (!stack) {
73
+ stack = [];
74
+ this.overrides.set(stream, stack);
75
+ }
76
+ stack.push(wrapped);
77
+ let restored = false;
78
+ return () => {
79
+ if (restored)
80
+ return;
81
+ restored = true;
82
+ const s = this.overrides.get(stream);
83
+ if (!s)
84
+ return;
85
+ const idx = s.indexOf(wrapped);
86
+ if (idx !== -1)
87
+ s.splice(idx, 1);
88
+ };
89
+ }
90
+ setDefault(stream, target) {
91
+ this.defaults.set(stream, this.wrap(stream, target));
92
+ }
93
+ /** Wrap a surface so writes emit `compositor:write` before delegating. */
94
+ wrap(stream, target) {
95
+ const bus = this.bus;
96
+ if (!bus)
97
+ return target;
98
+ return {
99
+ write: (text) => {
100
+ try {
101
+ bus.emit("compositor:write", { stream, text });
102
+ }
103
+ catch { }
104
+ target.write(text);
105
+ },
106
+ writeLine: (line) => {
107
+ try {
108
+ bus.emit("compositor:write", { stream, text: line + "\n" });
109
+ }
110
+ catch { }
111
+ target.writeLine(line);
112
+ },
113
+ get columns() { return target.columns; },
114
+ };
115
+ }
116
+ }
@@ -18,3 +18,12 @@ export interface DiffRenderOptions {
18
18
  export declare function selectMode(width: number): DiffDisplayMode;
19
19
  /** Render a diff result as an array of ANSI-formatted terminal lines. */
20
20
  export declare function renderDiff(diff: DiffResult, opts: DiffRenderOptions): string[];
21
+ /**
22
+ * Async variant of renderDiff that yields to the event loop between hunks.
23
+ * Use when rendering in a context where a spinner or other UI needs to stay
24
+ * responsive (e.g. showing a large diff during a permission prompt).
25
+ *
26
+ * @param onLines - Callback invoked with each batch of rendered lines as they
27
+ * are produced. Allows progressive/streaming display.
28
+ */
29
+ export declare function renderDiffAsync(diff: DiffResult, opts: DiffRenderOptions, onLines: (lines: string[]) => void): Promise<void>;