agent-sh 0.15.0 → 0.15.1

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 (116) hide show
  1. package/docs/README.md +14 -0
  2. package/docs/agent.md +398 -0
  3. package/docs/architecture.md +196 -0
  4. package/docs/context-management.md +200 -0
  5. package/docs/extensions.md +951 -0
  6. package/docs/library.md +84 -0
  7. package/docs/troubleshooting.md +65 -0
  8. package/docs/tui-composition.md +294 -0
  9. package/docs/usage.md +306 -0
  10. package/examples/extensions/ash-scheme/package.json +1 -1
  11. package/examples/extensions/ashi/EXTENDING.md +2 -2
  12. package/examples/extensions/ashi/README.md +2 -2
  13. package/examples/extensions/ashi/docs/ui-surface-protocol.md +1 -1
  14. package/examples/extensions/ashi/package.json +5 -3
  15. package/examples/extensions/ashi/src/cli.ts +6 -5
  16. package/examples/extensions/ashi/src/renderer.ts +22 -2
  17. package/examples/extensions/ashi/src/renderers/pi-tui/tool-group.ts +5 -8
  18. package/examples/extensions/ashi-ink/package.json +2 -2
  19. package/examples/extensions/claude-code-bridge/package.json +1 -1
  20. package/examples/extensions/opencode-bridge/package.json +1 -1
  21. package/package.json +3 -1
  22. package/src/agent/agent-loop.ts +1563 -0
  23. package/src/agent/entry-format.ts +19 -0
  24. package/src/agent/events.ts +151 -0
  25. package/src/agent/extensions/rolling-history/constants.ts +1 -0
  26. package/src/agent/extensions/rolling-history/index.ts +202 -0
  27. package/src/agent/extensions/rolling-history/recall.ts +131 -0
  28. package/src/agent/extensions/rolling-history/strategy.ts +404 -0
  29. package/src/agent/host-types.ts +192 -0
  30. package/src/agent/index.ts +591 -0
  31. package/src/agent/live-view.ts +279 -0
  32. package/src/agent/llm-client.ts +111 -0
  33. package/src/agent/llm-facade.ts +43 -0
  34. package/src/agent/normalize-args.ts +61 -0
  35. package/src/agent/nuclear-form.ts +382 -0
  36. package/src/agent/providers/deepseek.ts +39 -0
  37. package/src/agent/providers/ollama.ts +92 -0
  38. package/src/agent/providers/openai-compatible.ts +36 -0
  39. package/src/agent/providers/openai.ts +52 -0
  40. package/src/agent/providers/opencode.ts +142 -0
  41. package/src/agent/providers/openrouter.ts +105 -0
  42. package/src/agent/providers/zai-coding-plan.ts +33 -0
  43. package/src/agent/session-store.ts +336 -0
  44. package/src/agent/skills.ts +228 -0
  45. package/src/agent/store.ts +310 -0
  46. package/src/agent/subagent.ts +305 -0
  47. package/src/agent/system-prompt.ts +151 -0
  48. package/src/agent/token-budget.ts +12 -0
  49. package/src/agent/tool-protocol.ts +722 -0
  50. package/src/agent/tool-registry.ts +66 -0
  51. package/src/agent/tools/bash.ts +95 -0
  52. package/src/agent/tools/edit-file.ts +154 -0
  53. package/src/agent/tools/expand-home.ts +7 -0
  54. package/src/agent/tools/glob.ts +108 -0
  55. package/src/agent/tools/grep.ts +228 -0
  56. package/src/agent/tools/list-skills.ts +37 -0
  57. package/src/agent/tools/ls.ts +81 -0
  58. package/src/agent/tools/pwsh.ts +140 -0
  59. package/src/agent/tools/read-file.ts +164 -0
  60. package/src/agent/tools/write-file.ts +72 -0
  61. package/src/agent/types.ts +149 -0
  62. package/src/cli/args.ts +91 -0
  63. package/src/cli/auth/cli.ts +244 -0
  64. package/src/cli/auth/discover.ts +52 -0
  65. package/src/cli/auth/keys.ts +143 -0
  66. package/src/cli/index.ts +295 -0
  67. package/src/cli/init.ts +74 -0
  68. package/src/cli/install.ts +439 -0
  69. package/src/cli/shell-env.ts +68 -0
  70. package/src/cli/subcommands.ts +24 -0
  71. package/src/core/event-bus.ts +252 -0
  72. package/src/core/extension-loader.ts +347 -0
  73. package/src/core/index.ts +152 -0
  74. package/src/core/settings.ts +398 -0
  75. package/src/core/types.ts +61 -0
  76. package/src/extensions/file-autocomplete.ts +71 -0
  77. package/src/extensions/index.ts +38 -0
  78. package/src/extensions/slash-commands/events.ts +14 -0
  79. package/src/extensions/slash-commands/index.ts +269 -0
  80. package/src/shell/events.ts +73 -0
  81. package/src/shell/host-types.ts +150 -0
  82. package/src/shell/index.ts +159 -0
  83. package/src/shell/input-handler.ts +505 -0
  84. package/src/shell/output-parser.ts +156 -0
  85. package/src/shell/shell-context.ts +193 -0
  86. package/src/shell/shell.ts +414 -0
  87. package/src/shell/strategies/bash.ts +83 -0
  88. package/src/shell/strategies/fish.ts +77 -0
  89. package/src/shell/strategies/index.ts +24 -0
  90. package/src/shell/strategies/types.ts +64 -0
  91. package/src/shell/strategies/zsh.ts +92 -0
  92. package/src/shell/terminal.ts +124 -0
  93. package/src/shell/tui-input-view.ts +222 -0
  94. package/src/shell/tui-renderer.ts +1126 -0
  95. package/src/utils/ansi.ts +140 -0
  96. package/src/utils/box-frame.ts +138 -0
  97. package/src/utils/compositor.ts +157 -0
  98. package/src/utils/diff-renderer.ts +829 -0
  99. package/src/utils/diff.ts +244 -0
  100. package/src/utils/executor.ts +305 -0
  101. package/src/utils/file-watcher.ts +110 -0
  102. package/src/utils/floating-panel.ts +1160 -0
  103. package/src/utils/handler-registry.ts +110 -0
  104. package/src/utils/line-editor.ts +636 -0
  105. package/src/utils/markdown.ts +437 -0
  106. package/src/utils/message-utils.ts +113 -0
  107. package/src/utils/package-version.ts +12 -0
  108. package/src/utils/palette.ts +64 -0
  109. package/src/utils/ref-counter.ts +9 -0
  110. package/src/utils/ripgrep-path.ts +17 -0
  111. package/src/utils/shell-output-spill.ts +76 -0
  112. package/src/utils/stream-transform.ts +292 -0
  113. package/src/utils/terminal-buffer.ts +213 -0
  114. package/src/utils/tool-display.ts +315 -0
  115. package/src/utils/tool-interactive.ts +71 -0
  116. package/src/utils/tty.ts +14 -0
@@ -0,0 +1,84 @@
1
+ # Using agent-sh as a Library
2
+
3
+ ## Library vs Extension
4
+
5
+ agent-sh has two integration points. The difference: **extensions customize the existing TUI**, while **library mode lets you build your own frontend**.
6
+
7
+ | | Extension | Library |
8
+ |---|---|---|
9
+ | **Use when** | You want to add features to the interactive terminal — themes, custom renderers, input modes, content transforms | You're building something else entirely — a REST API, Electron app, test harness, CI pipeline |
10
+ | **You get** | An `ExtensionContext` — substrate (bus, handlers, lifecycle, compositor) + slash-command registration + optional host surfaces. `ctx.agent` (LLM, tools, instructions) and `ctx.shell` (palette, transforms, remote sessions) are attached by their hosts during activation; under headless backends, the missing surface is `undefined`. Narrower types (`AgentContext`, `ShellContext`, or their intersection) let extensions declare which hosts they require. | `AgentShellCore` — bus, handler registry, lifecycle control (`activateBackend`, `kill`) |
11
+ | **Who controls the frontend?** | The built-in TUI does; you decorate it | You do; there is no TUI |
12
+ | **How to use** | Export an `activate` function, load with `-e` | Import `createCore()`, load extensions, wire your own I/O |
13
+
14
+ If you're adding a Mermaid renderer or a custom slash command, write an extension. If you're building a web server that talks to an LLM, use the library.
15
+
16
+ Two real frontends are built this way: [**ashi**](../examples/extensions/ashi/) (published as `@guanyilun/ashi`) drives `createCore()` into a standalone chat-style TUI with no shell underneath, and [**asHub**](https://github.com/firslov/asHub) wraps the same kernel in an Electron desktop app. Both reuse the ash backend, tools, and providers — only the frontend differs.
17
+
18
+ ## Quick Start
19
+
20
+ ```typescript
21
+ import { createCore } from "agent-sh";
22
+ import { activateAgent } from "agent-sh/agent";
23
+ import { loadBuiltinExtensions } from "agent-sh/extensions";
24
+
25
+ const core = createCore({
26
+ apiKey: process.env.OPENAI_API_KEY,
27
+ model: "gpt-4o",
28
+ });
29
+
30
+ const ctx = core.extensionContext({ quit: () => process.exit(0) });
31
+
32
+ activateAgent(ctx);
33
+ const loaded = await loadBuiltinExtensions(ctx);
34
+ core.bus.emit("core:extensions-loaded", { names: loaded });
35
+
36
+ core.bus.on("agent:response-chunk", ({ blocks }) => {
37
+ for (const b of blocks) if (b.type === "text") process.stdout.write(b.text);
38
+ });
39
+ core.bus.on("agent:processing-done", () => console.log("\n[done]"));
40
+
41
+ await core.activateBackend();
42
+ core.bus.emit("agent:submit", { query: "explain this codebase" });
43
+ ```
44
+
45
+ `createCore()` returns a headless kernel — the event bus and handler registry, with no terminal, shell, LLM, or agent attached. `activateAgent(ctx)` attaches the agent surface (tools, LLM client, providers) and registers the built-in `ash` backend; `loadBuiltinExtensions(ctx)` adds the abstract backend registry, slash commands, and file autocomplete. `core:extensions-loaded` triggers provider resolution; `activateBackend()` then starts ash (or whichever backend is configured). Send queries by emitting `agent:submit` and consume responses by listening to bus events.
46
+
47
+ Tools run without confirmation by default; to gate them, register tool advisors via `ctx.agent.adviseTool` (see examples/extensions/interactive-prompts.ts).
48
+
49
+ ## AgentShellCore API
50
+
51
+ | Method | Description |
52
+ |---|---|
53
+ | `bus` | The event bus — same one extensions use. See [Extensions: Event Bus](extensions.md#event-bus) |
54
+ | `handlers` | Named handler registry for `define`/`advise`/`call`. Core defines `cwd` (returns `process.cwd()`); shell-context advises it with the PTY-tracked value when loaded |
55
+ | `activateBackend(name?)` | Activates the named (or persisted-default) agent backend. Call after loading extensions and emitting `core:extensions-loaded` |
56
+ | `extensionContext(opts)` | Creates an `ExtensionContext` — use this to load extensions in library mode |
57
+ | `kill()` | Clean shutdown |
58
+
59
+ Send queries with `bus.emit("agent:submit", { query })`; cancel with `bus.emit("agent:cancel-request", { silent: false })`.
60
+
61
+ ## Loading Extensions in Library Mode
62
+
63
+ Extensions aren't loaded automatically in library mode — you get a bare kernel with no agent. You must call `activateAgent` (for the ash agent surface) and load built-ins (for the backend registry):
64
+
65
+ ```typescript
66
+ import { createCore } from "agent-sh";
67
+ import { activateAgent } from "agent-sh/agent";
68
+ import { loadBuiltinExtensions } from "agent-sh/extensions";
69
+ import myTheme from "./my-theme";
70
+
71
+ const core = createCore({ apiKey: "...", model: "gpt-4o" });
72
+ const ctx = core.extensionContext({ quit: () => process.exit(0) });
73
+
74
+ activateAgent(ctx);
75
+ const builtin = await loadBuiltinExtensions(ctx, ["slash-commands"]); // optionally disable
76
+ myTheme(ctx);
77
+ core.bus.emit("core:extensions-loaded", { names: builtin });
78
+
79
+ await core.activateBackend();
80
+ ```
81
+
82
+ This is exactly what the CLI does internally: `createCore()` → `activateAgent()` → `loadBuiltinExtensions()` → user extensions → emit `core:extensions-loaded` → `activateBackend()`. The interactive terminal is just another layer on top of the same kernel.
83
+
84
+ See [Architecture](architecture.md) for details on the core design and EventBus.
@@ -0,0 +1,65 @@
1
+ # Troubleshooting
2
+
3
+ ## Common Issues
4
+
5
+ **Problem**: No response from agent (thinking appears but no text)
6
+
7
+ **Solutions**:
8
+ 1. Check API key: `echo $OPENAI_API_KEY`
9
+ 2. Test the API endpoint directly:
10
+ ```bash
11
+ curl -s "$OPENAI_BASE_URL/models" -H "Authorization: Bearer $OPENAI_API_KEY" | head
12
+ ```
13
+ 3. Verify the model name is correct for your provider
14
+
15
+ **Problem**: "context overflow" or response cut off
16
+
17
+ **Solution**: The conversation is too long. Use `/compact` to manually free up space, or the agent will auto-compact after the error.
18
+
19
+ **Problem**: Tool calls not working (agent responds but doesn't use tools)
20
+
21
+ **Solution**: Some models have limited or no tool/function calling support. Try a more capable model (e.g., gpt-4o, claude-sonnet-4-6 via OpenRouter).
22
+
23
+ **Problem**: Garbled output, startup banner overwritten, or messy prompt rendering
24
+
25
+ **Cause**: Powerlevel10k's **instant prompt** feature races with agent-sh's TUI. Instant prompt caches the previous prompt, displays it immediately, redirects stdout/stderr during shell init, then redraws the prompt using cursor-movement sequences — all of which can overwrite agent-sh's output.
26
+
27
+ **Solution**: Guard the instant prompt block in your `~/.zshrc` so it's skipped inside agent-sh:
28
+
29
+ ```zsh
30
+ # Disable p10k instant prompt inside agent-sh (it races with TUI rendering)
31
+ if [[ -z "$AGENT_SH" && -r "${XDG_CACHE_HOME:-$HOME/.cache}/p10k-instant-prompt-${(%):-%n}.zsh" ]]; then
32
+ source "${XDG_CACHE_HOME:-$HOME/.cache}/p10k-instant-prompt-${(%):-%n}.zsh"
33
+ fi
34
+ ```
35
+
36
+ Your normal p10k prompt still works — only the "flash cached prompt then redraw" behavior is disabled.
37
+
38
+ ## Common Errors
39
+
40
+ **Error**: "API key not found" or "401 Unauthorized"
41
+ - **Cause**: Missing or invalid API key
42
+ - **Solution**: Set the appropriate key: `export OPENAI_API_KEY="your-key"`
43
+
44
+ **Error**: "Invalid model name" or "404 Not Found"
45
+ - **Cause**: Model not available at the configured endpoint
46
+ - **Solution**: Check available models for your provider. Local providers (Ollama, LM Studio) need the model downloaded first.
47
+
48
+ **Error**: Stream errors or disconnections
49
+ - **Cause**: Network issues or provider rate limits
50
+ - **Solution**: The agent will show the error. Try again, or check provider status.
51
+
52
+ ## Debug Mode
53
+
54
+ Enable debug mode for detailed protocol logging:
55
+
56
+ ```bash
57
+ DEBUG=1 agent-sh --api-key "$KEY" --model gpt-4o
58
+ ```
59
+
60
+ ## Getting Help
61
+
62
+ If you encounter issues:
63
+ 1. Check the [Usage Guide](usage.md) for provider configuration
64
+ 2. Try a different model or provider to isolate the problem
65
+ 3. Check [GitHub issues](https://github.com/guanyilun/agent-sh/issues) for known problems
@@ -0,0 +1,294 @@
1
+ # TUI Composition
2
+
3
+ How agent-sh routes rendered output to different surfaces (stdout, floating panels, test buffers) and how extensions intercept or redirect that output.
4
+
5
+ ## Overview
6
+
7
+ The TUI rendering pipeline has three layers:
8
+
9
+ 1. **Components** — know *what* to render. Pure functions that turn state into `string[]`. Examples: `renderBoxFrame()`, `renderDiff()`, `renderToolCall()`.
10
+
11
+ 2. **Surfaces** — know *where* output goes. A surface accepts lines and raw writes. Stdout is a surface. A floating panel's content area is a surface.
12
+
13
+ 3. **Compositor** — knows *how to route*. Maps named streams to surfaces. Extensions override routing with `redirect()` to capture output.
14
+
15
+ ```
16
+ ┌─────────────────┐
17
+ EventBus events │ tui-renderer │ (subscribes to agent:* events,
18
+ agent:query ───► │ │ manages state, calls components)
19
+ agent:chunk ───► │ RenderState │
20
+ agent:tool-* ───► │ MarkdownRender │
21
+ └───────┬─────────┘
22
+ │ writes to named streams
23
+
24
+ ┌─────────────────┐
25
+ │ Compositor │ routes streams → surfaces
26
+ │ │
27
+ │ "agent" ──► ? │
28
+ │ "query" ──► ? │
29
+ │ "status" ──► ? │
30
+ └───────┬─────────┘
31
+ │ resolves to active surface
32
+ ┌───────┴─────────────────────┐
33
+ ▼ ▼
34
+ ┌─────────────┐ ┌──────────────┐
35
+ │ StdoutSurface│ │ PanelSurface │
36
+ │ (default) │ │ (override) │
37
+ └─────────────┘ └──────────────┘
38
+ ```
39
+
40
+ ## Surfaces
41
+
42
+ A `RenderSurface` is anything that can accept rendered output:
43
+
44
+ ```typescript
45
+ interface RenderSurface {
46
+ write(text: string): void; // raw — supports \r, escape codes
47
+ writeLine(line: string): void; // line + newline
48
+ readonly columns: number; // available width
49
+ }
50
+ ```
51
+
52
+ Built-in surfaces:
53
+
54
+ | Surface | Description |
55
+ |---|---|
56
+ | `StdoutSurface` | Default. Writes to `process.stdout`. |
57
+ | `nullSurface` | Drops all output silently. Used when no route exists. |
58
+
59
+ Extensions create their own surfaces. For example, a floating panel surface:
60
+
61
+ ```typescript
62
+ const panelSurface: RenderSurface = {
63
+ write(text) {
64
+ if (text.startsWith("\r")) {
65
+ // Handle spinner \r overwrites
66
+ const cleaned = text.replace(/^\r/, "").replace(/\x1b\[\d*K/g, "");
67
+ if (cleaned.trim()) panel.updateLastLine(() => cleaned);
68
+ return;
69
+ }
70
+ panel.appendText(text);
71
+ },
72
+ writeLine(line) { panel.appendLine(line); },
73
+ get columns() { return panel.computeGeometry().contentW; },
74
+ };
75
+ ```
76
+
77
+ ## Compositor
78
+
79
+ The compositor maps named streams to surfaces. Components write to streams — they never know (or care) which surface they end up on.
80
+
81
+ ```typescript
82
+ interface Compositor {
83
+ surface(stream: string): RenderSurface;
84
+ redirect(stream: string, target: RenderSurface): () => void;
85
+ setDefault(stream: string, target: RenderSurface): void;
86
+ }
87
+ ```
88
+
89
+ ### Default streams
90
+
91
+ | Stream | Content |
92
+ |---|---|
93
+ | `"agent"` | Agent response: markdown, tool calls, spinner, diffs, code blocks |
94
+ | `"query"` | User query display (the bordered input box) |
95
+ | `"status"` | Info messages, errors, suggestions |
96
+
97
+ The shell frontend (`src/shell/`) sets all three to `StdoutSurface` during `activateShell`. A library or web consumer that doesn't load the shell frontend has no defaults — it must call `compositor.setDefault(...)` itself.
98
+
99
+ ### Redirecting a stream
100
+
101
+ `redirect()` returns a restore function. Redirects are stack-based — multiple redirects on the same stream nest correctly:
102
+
103
+ ```typescript
104
+ const restore = compositor.redirect("agent", panelSurface);
105
+ // ... agent output now goes to the panel ...
106
+ restore(); // back to previous surface
107
+ ```
108
+
109
+ ### Hierarchical streams
110
+
111
+ Stream names are hierarchical, separated by `:`. When resolving a surface, the compositor walks up the hierarchy until it finds an override or default:
112
+
113
+ ```
114
+ "agent:sub:abc123" → "agent:sub" → "agent" → nullSurface
115
+ ```
116
+
117
+ This enables fine-grained interception without registering defaults for every sub-stream:
118
+
119
+ ```typescript
120
+ // All agent output goes to stdout (the "agent" default)
121
+ compositor.setDefault("agent", stdoutSurface);
122
+
123
+ // Redirect just diffs to a viewer panel — everything else unaffected
124
+ compositor.redirect("agent:diff", diffPanelSurface);
125
+
126
+ // Redirect a specific subagent to its own panel
127
+ compositor.redirect("agent:sub:abc123", subagentPanelSurface);
128
+ ```
129
+
130
+ The tui-renderer writes to the appropriate sub-stream. If no override exists for that sub-stream, output falls through to the parent — which is typically stdout.
131
+
132
+ ## Writing an extension that uses the compositor
133
+
134
+ ### Example: overlay agent (full redirect)
135
+
136
+ The overlay agent redirects *all* render streams to a floating panel when active. This is the simplest pattern — whole-sale capture:
137
+
138
+ ```typescript
139
+ import { FloatingPanel } from "agent-sh/utils/floating-panel";
140
+
141
+ export default function activate(ctx: ExtensionContext): void {
142
+ const { bus, compositor } = ctx;
143
+ const terminalBuffer = ctx.call("terminal-buffer");
144
+
145
+ const panel = new FloatingPanel(bus, { trigger: "\x1c", terminalBuffer: terminalBuffer ?? undefined });
146
+ const panelSurface = createPanelSurface(panel);
147
+
148
+ let restoreAgent: (() => void) | null = null;
149
+ let restoreQuery: (() => void) | null = null;
150
+
151
+ panel.handlers.advise("panel:submit", (_next, query: string) => {
152
+ restoreAgent = compositor.redirect("agent", panelSurface);
153
+ restoreQuery = compositor.redirect("query", panelSurface);
154
+ panel.setActive();
155
+ bus.emit("agent:submit", { query });
156
+ });
157
+
158
+ panel.handlers.advise("panel:dismiss", (next) => {
159
+ next();
160
+ restoreAgent?.(); restoreAgent = null;
161
+ restoreQuery?.(); restoreQuery = null;
162
+ });
163
+ }
164
+ ```
165
+
166
+ Because the full tui-renderer pipeline still runs — it just writes to the panel surface instead of stdout — the overlay gets markdown rendering, tool grouping, diffs, and syntax highlighting for free.
167
+
168
+ ### Example: diff viewer (sub-stream redirect)
169
+
170
+ An extension that captures just diff output into a separate panel:
171
+
172
+ ```typescript
173
+ import { FloatingPanel } from "agent-sh/utils/floating-panel";
174
+
175
+ export default function activate(ctx: ExtensionContext): void {
176
+ const { bus, compositor } = ctx;
177
+ const terminalBuffer = ctx.call("terminal-buffer");
178
+
179
+ const panel = new FloatingPanel(bus, { trigger: "\x04", terminalBuffer: terminalBuffer ?? undefined });
180
+ const surface = createPanelSurface(panel);
181
+
182
+ panel.handlers.advise("panel:show", (_next) => {
183
+ // Redirect just the diff sub-stream
184
+ compositor.redirect("agent:diff", surface);
185
+ });
186
+ }
187
+ ```
188
+
189
+ Main agent output (text, tools, spinner) continues on stdout. Only diffs route to the panel.
190
+
191
+ ### Example: subagent panel
192
+
193
+ A panel that shows a subagent's work separately from the main agent:
194
+
195
+ ```typescript
196
+ function onSubagentSpawn(id: string, ctx: ExtensionContext): void {
197
+ const panel = new FloatingPanel(ctx.bus, { dimBackground: false });
198
+ const surface = createPanelSurface(panel);
199
+
200
+ // This subagent's output goes to its own panel
201
+ const restore = ctx.shell.compositor.redirect(`agent:sub:${id}`, surface);
202
+
203
+ panel.open();
204
+ // When done, restore routing
205
+ ctx.bus.on("agent:processing-done", () => {
206
+ restore();
207
+ panel.setDone();
208
+ });
209
+ }
210
+ ```
211
+
212
+ ## How tui-renderer uses the compositor
213
+
214
+ The tui-renderer gets the compositor from `ExtensionContext` and writes to named streams instead of stdout directly:
215
+
216
+ ```typescript
217
+ export default function activate(ctx: ExtensionContext): void {
218
+ const { compositor } = ctx;
219
+
220
+ // Shorthand — get the current agent surface
221
+ function out(): RenderSurface {
222
+ return compositor.surface("agent");
223
+ }
224
+
225
+ // Drain markdown renderer lines to the active surface
226
+ function drain(): void {
227
+ const surface = out();
228
+ for (const line of renderer.drainLines()) {
229
+ surface.writeLine(line);
230
+ }
231
+ }
232
+
233
+ // Spinner writes directly to the surface
234
+ setInterval(() => {
235
+ out().write(`\r ${spinnerLine}\x1b[K`);
236
+ }, 80);
237
+ }
238
+ ```
239
+
240
+ The renderer doesn't know whether it's writing to stdout or a panel. The compositor resolves the target on each `surface()` call, so redirects take effect immediately — even mid-response.
241
+
242
+ ## Remote sessions
243
+
244
+ For most extensions that route output to a different surface, use `createRemoteSession()` instead of manual compositor redirects. It bundles compositor routing, shell lifecycle advisors, and chrome suppression into one call:
245
+
246
+ ```typescript
247
+ const session = ctx.shell.createRemoteSession({
248
+ surface: panelSurface,
249
+ suppressQueryBox: true, // session has own input
250
+ });
251
+
252
+ session.submit("what's on screen?");
253
+ session.close(); // restores everything
254
+ ```
255
+
256
+ See [Extensions: Remote Sessions](extensions.md#remote-sessions) for the full API.
257
+
258
+ Use the compositor directly only when you need fine-grained control — e.g. redirecting a single sub-stream like `"agent:diff"` without affecting the rest.
259
+
260
+ ## Observing writes (`compositor:write`)
261
+
262
+ When the core creates the compositor with an `EventBus` attached, every surface write emits a `compositor:write` event:
263
+
264
+ ```typescript
265
+ bus.on("compositor:write", ({ stream, text }) => {
266
+ // `stream` is the named stream (e.g. "agent", "agent:diff"), `text` is the raw write
267
+ });
268
+ ```
269
+
270
+ This lets an extension mirror or inspect rendered output without intercepting stdout. Used by e.g. a compositor-mirror extension that buffers recent agent output and exposes a `compositor_read` tool.
271
+
272
+ ## Relationship to other systems
273
+
274
+ | System | Role | Compositor interaction |
275
+ |---|---|---|
276
+ | **EventBus** | Delivers agent events to tui-renderer | None — events flow regardless of where output goes |
277
+ | **Handler registry** | Advisable render functions (`render:code-block`, etc.) | Handlers produce lines; compositor routes them to surfaces |
278
+ | **FloatingPanel** | Screen compositing, input routing, alt-screen management | Panel provides the surface; compositor routes to it |
279
+ | **MarkdownRenderer** | Streaming markdown → lines | Produces lines; tui-renderer drains them to compositor surface |
280
+ | **RemoteSession** | High-level "route output elsewhere" primitive | Creates compositor redirects + lifecycle advisors in one call |
281
+
282
+ The compositor sits between "produce lines" and "display lines". It doesn't affect *what* gets rendered — only *where*.
283
+
284
+ ## Key files
285
+
286
+ | File | Role |
287
+ |---|---|
288
+ | `src/utils/compositor.ts` | `RenderSurface`, `Compositor`, `DefaultCompositor`, `StdoutSurface` |
289
+ | `src/shell/tui-renderer.ts` | Main renderer — writes to compositor streams |
290
+ | `examples/extensions/overlay-agent.ts` | Uses `ctx.shell.createRemoteSession` to route to floating panel |
291
+ | `src/utils/floating-panel.ts` | Panel screen management and content API |
292
+ | `src/shell/index.ts` | Allocates the compositor, registers default surfaces, implements `createRemoteSession` |
293
+ | `src/shell/host-types.ts` | `ShellSurface.compositor`, `RemoteSession`, `RemoteSessionOptions` |
294
+ | `examples/extensions/tmux-pane.ts` | Tmux side pane — `/split` and `/rsplit` using `ctx.shell.createRemoteSession` |