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.
- package/docs/README.md +14 -0
- package/docs/agent.md +398 -0
- package/docs/architecture.md +196 -0
- package/docs/context-management.md +200 -0
- package/docs/extensions.md +951 -0
- package/docs/library.md +84 -0
- package/docs/troubleshooting.md +65 -0
- package/docs/tui-composition.md +294 -0
- package/docs/usage.md +306 -0
- package/examples/extensions/ash-scheme/package.json +1 -1
- package/examples/extensions/ashi/EXTENDING.md +2 -2
- package/examples/extensions/ashi/README.md +2 -2
- package/examples/extensions/ashi/docs/ui-surface-protocol.md +1 -1
- package/examples/extensions/ashi/package.json +5 -3
- package/examples/extensions/ashi/src/cli.ts +6 -5
- package/examples/extensions/ashi/src/renderer.ts +22 -2
- package/examples/extensions/ashi/src/renderers/pi-tui/tool-group.ts +5 -8
- package/examples/extensions/ashi-ink/package.json +2 -2
- package/examples/extensions/claude-code-bridge/package.json +1 -1
- package/examples/extensions/opencode-bridge/package.json +1 -1
- package/package.json +3 -1
- package/src/agent/agent-loop.ts +1563 -0
- package/src/agent/entry-format.ts +19 -0
- package/src/agent/events.ts +151 -0
- package/src/agent/extensions/rolling-history/constants.ts +1 -0
- package/src/agent/extensions/rolling-history/index.ts +202 -0
- package/src/agent/extensions/rolling-history/recall.ts +131 -0
- package/src/agent/extensions/rolling-history/strategy.ts +404 -0
- package/src/agent/host-types.ts +192 -0
- package/src/agent/index.ts +591 -0
- package/src/agent/live-view.ts +279 -0
- package/src/agent/llm-client.ts +111 -0
- package/src/agent/llm-facade.ts +43 -0
- package/src/agent/normalize-args.ts +61 -0
- package/src/agent/nuclear-form.ts +382 -0
- package/src/agent/providers/deepseek.ts +39 -0
- package/src/agent/providers/ollama.ts +92 -0
- package/src/agent/providers/openai-compatible.ts +36 -0
- package/src/agent/providers/openai.ts +52 -0
- package/src/agent/providers/opencode.ts +142 -0
- package/src/agent/providers/openrouter.ts +105 -0
- package/src/agent/providers/zai-coding-plan.ts +33 -0
- package/src/agent/session-store.ts +336 -0
- package/src/agent/skills.ts +228 -0
- package/src/agent/store.ts +310 -0
- package/src/agent/subagent.ts +305 -0
- package/src/agent/system-prompt.ts +151 -0
- package/src/agent/token-budget.ts +12 -0
- package/src/agent/tool-protocol.ts +722 -0
- package/src/agent/tool-registry.ts +66 -0
- package/src/agent/tools/bash.ts +95 -0
- package/src/agent/tools/edit-file.ts +154 -0
- package/src/agent/tools/expand-home.ts +7 -0
- package/src/agent/tools/glob.ts +108 -0
- package/src/agent/tools/grep.ts +228 -0
- package/src/agent/tools/list-skills.ts +37 -0
- package/src/agent/tools/ls.ts +81 -0
- package/src/agent/tools/pwsh.ts +140 -0
- package/src/agent/tools/read-file.ts +164 -0
- package/src/agent/tools/write-file.ts +72 -0
- package/src/agent/types.ts +149 -0
- package/src/cli/args.ts +91 -0
- package/src/cli/auth/cli.ts +244 -0
- package/src/cli/auth/discover.ts +52 -0
- package/src/cli/auth/keys.ts +143 -0
- package/src/cli/index.ts +295 -0
- package/src/cli/init.ts +74 -0
- package/src/cli/install.ts +439 -0
- package/src/cli/shell-env.ts +68 -0
- package/src/cli/subcommands.ts +24 -0
- package/src/core/event-bus.ts +252 -0
- package/src/core/extension-loader.ts +347 -0
- package/src/core/index.ts +152 -0
- package/src/core/settings.ts +398 -0
- package/src/core/types.ts +61 -0
- package/src/extensions/file-autocomplete.ts +71 -0
- package/src/extensions/index.ts +38 -0
- package/src/extensions/slash-commands/events.ts +14 -0
- package/src/extensions/slash-commands/index.ts +269 -0
- package/src/shell/events.ts +73 -0
- package/src/shell/host-types.ts +150 -0
- package/src/shell/index.ts +159 -0
- package/src/shell/input-handler.ts +505 -0
- package/src/shell/output-parser.ts +156 -0
- package/src/shell/shell-context.ts +193 -0
- package/src/shell/shell.ts +414 -0
- package/src/shell/strategies/bash.ts +83 -0
- package/src/shell/strategies/fish.ts +77 -0
- package/src/shell/strategies/index.ts +24 -0
- package/src/shell/strategies/types.ts +64 -0
- package/src/shell/strategies/zsh.ts +92 -0
- package/src/shell/terminal.ts +124 -0
- package/src/shell/tui-input-view.ts +222 -0
- package/src/shell/tui-renderer.ts +1126 -0
- package/src/utils/ansi.ts +140 -0
- package/src/utils/box-frame.ts +138 -0
- package/src/utils/compositor.ts +157 -0
- package/src/utils/diff-renderer.ts +829 -0
- package/src/utils/diff.ts +244 -0
- package/src/utils/executor.ts +305 -0
- package/src/utils/file-watcher.ts +110 -0
- package/src/utils/floating-panel.ts +1160 -0
- package/src/utils/handler-registry.ts +110 -0
- package/src/utils/line-editor.ts +636 -0
- package/src/utils/markdown.ts +437 -0
- package/src/utils/message-utils.ts +113 -0
- package/src/utils/package-version.ts +12 -0
- package/src/utils/palette.ts +64 -0
- package/src/utils/ref-counter.ts +9 -0
- package/src/utils/ripgrep-path.ts +17 -0
- package/src/utils/shell-output-spill.ts +76 -0
- package/src/utils/stream-transform.ts +292 -0
- package/src/utils/terminal-buffer.ts +213 -0
- package/src/utils/tool-display.ts +315 -0
- package/src/utils/tool-interactive.ts +71 -0
- package/src/utils/tty.ts +14 -0
package/docs/library.md
ADDED
|
@@ -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` |
|