agent-sh 0.15.0 → 0.15.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 (124) hide show
  1. package/dist/agent/agent-loop.js +11 -8
  2. package/dist/agent/events.d.ts +4 -0
  3. package/docs/README.md +14 -0
  4. package/docs/agent.md +398 -0
  5. package/docs/architecture.md +196 -0
  6. package/docs/context-management.md +200 -0
  7. package/docs/extensions.md +951 -0
  8. package/docs/library.md +84 -0
  9. package/docs/troubleshooting.md +65 -0
  10. package/docs/tui-composition.md +294 -0
  11. package/docs/usage.md +306 -0
  12. package/examples/extensions/ash-scheme/package.json +1 -1
  13. package/examples/extensions/ashi/EXTENDING.md +2 -2
  14. package/examples/extensions/ashi/README.md +2 -2
  15. package/examples/extensions/ashi/docs/ui-surface-protocol.md +1 -1
  16. package/examples/extensions/ashi/package.json +5 -3
  17. package/examples/extensions/ashi/src/chat/tool-group.ts +3 -2
  18. package/examples/extensions/ashi/src/cli.ts +9 -8
  19. package/examples/extensions/ashi/src/dialogs.ts +16 -1
  20. package/examples/extensions/ashi/src/events.ts +1 -0
  21. package/examples/extensions/ashi/src/frontend.ts +26 -6
  22. package/examples/extensions/ashi/src/renderer.ts +24 -4
  23. package/examples/extensions/ashi/src/renderers/pi-tui/schema-mount.ts +4 -3
  24. package/examples/extensions/ashi/src/renderers/pi-tui/tool-group.ts +5 -8
  25. package/examples/extensions/ashi/src/ui.ts +11 -0
  26. package/examples/extensions/ashi-ink/package.json +2 -2
  27. package/examples/extensions/claude-code-bridge/package.json +1 -1
  28. package/examples/extensions/opencode-bridge/package.json +1 -1
  29. package/package.json +3 -1
  30. package/src/agent/agent-loop.ts +1566 -0
  31. package/src/agent/entry-format.ts +19 -0
  32. package/src/agent/events.ts +153 -0
  33. package/src/agent/extensions/rolling-history/constants.ts +1 -0
  34. package/src/agent/extensions/rolling-history/index.ts +202 -0
  35. package/src/agent/extensions/rolling-history/recall.ts +131 -0
  36. package/src/agent/extensions/rolling-history/strategy.ts +404 -0
  37. package/src/agent/host-types.ts +192 -0
  38. package/src/agent/index.ts +591 -0
  39. package/src/agent/live-view.ts +279 -0
  40. package/src/agent/llm-client.ts +111 -0
  41. package/src/agent/llm-facade.ts +43 -0
  42. package/src/agent/normalize-args.ts +61 -0
  43. package/src/agent/nuclear-form.ts +382 -0
  44. package/src/agent/providers/deepseek.ts +39 -0
  45. package/src/agent/providers/ollama.ts +92 -0
  46. package/src/agent/providers/openai-compatible.ts +36 -0
  47. package/src/agent/providers/openai.ts +52 -0
  48. package/src/agent/providers/opencode.ts +142 -0
  49. package/src/agent/providers/openrouter.ts +105 -0
  50. package/src/agent/providers/zai-coding-plan.ts +33 -0
  51. package/src/agent/session-store.ts +336 -0
  52. package/src/agent/skills.ts +228 -0
  53. package/src/agent/store.ts +310 -0
  54. package/src/agent/subagent.ts +305 -0
  55. package/src/agent/system-prompt.ts +151 -0
  56. package/src/agent/token-budget.ts +12 -0
  57. package/src/agent/tool-protocol.ts +722 -0
  58. package/src/agent/tool-registry.ts +66 -0
  59. package/src/agent/tools/bash.ts +95 -0
  60. package/src/agent/tools/edit-file.ts +154 -0
  61. package/src/agent/tools/expand-home.ts +7 -0
  62. package/src/agent/tools/glob.ts +108 -0
  63. package/src/agent/tools/grep.ts +228 -0
  64. package/src/agent/tools/list-skills.ts +37 -0
  65. package/src/agent/tools/ls.ts +81 -0
  66. package/src/agent/tools/pwsh.ts +140 -0
  67. package/src/agent/tools/read-file.ts +164 -0
  68. package/src/agent/tools/write-file.ts +72 -0
  69. package/src/agent/types.ts +149 -0
  70. package/src/cli/args.ts +91 -0
  71. package/src/cli/auth/cli.ts +244 -0
  72. package/src/cli/auth/discover.ts +52 -0
  73. package/src/cli/auth/keys.ts +143 -0
  74. package/src/cli/index.ts +295 -0
  75. package/src/cli/init.ts +74 -0
  76. package/src/cli/install.ts +439 -0
  77. package/src/cli/shell-env.ts +68 -0
  78. package/src/cli/subcommands.ts +24 -0
  79. package/src/core/event-bus.ts +252 -0
  80. package/src/core/extension-loader.ts +347 -0
  81. package/src/core/index.ts +152 -0
  82. package/src/core/settings.ts +398 -0
  83. package/src/core/types.ts +61 -0
  84. package/src/extensions/file-autocomplete.ts +71 -0
  85. package/src/extensions/index.ts +38 -0
  86. package/src/extensions/slash-commands/events.ts +14 -0
  87. package/src/extensions/slash-commands/index.ts +269 -0
  88. package/src/shell/events.ts +73 -0
  89. package/src/shell/host-types.ts +150 -0
  90. package/src/shell/index.ts +159 -0
  91. package/src/shell/input-handler.ts +505 -0
  92. package/src/shell/output-parser.ts +156 -0
  93. package/src/shell/shell-context.ts +193 -0
  94. package/src/shell/shell.ts +414 -0
  95. package/src/shell/strategies/bash.ts +83 -0
  96. package/src/shell/strategies/fish.ts +77 -0
  97. package/src/shell/strategies/index.ts +24 -0
  98. package/src/shell/strategies/types.ts +64 -0
  99. package/src/shell/strategies/zsh.ts +92 -0
  100. package/src/shell/terminal.ts +124 -0
  101. package/src/shell/tui-input-view.ts +222 -0
  102. package/src/shell/tui-renderer.ts +1126 -0
  103. package/src/utils/ansi.ts +140 -0
  104. package/src/utils/box-frame.ts +138 -0
  105. package/src/utils/compositor.ts +157 -0
  106. package/src/utils/diff-renderer.ts +829 -0
  107. package/src/utils/diff.ts +244 -0
  108. package/src/utils/executor.ts +305 -0
  109. package/src/utils/file-watcher.ts +110 -0
  110. package/src/utils/floating-panel.ts +1160 -0
  111. package/src/utils/handler-registry.ts +110 -0
  112. package/src/utils/line-editor.ts +636 -0
  113. package/src/utils/markdown.ts +437 -0
  114. package/src/utils/message-utils.ts +113 -0
  115. package/src/utils/package-version.ts +12 -0
  116. package/src/utils/palette.ts +64 -0
  117. package/src/utils/ref-counter.ts +9 -0
  118. package/src/utils/ripgrep-path.ts +17 -0
  119. package/src/utils/shell-output-spill.ts +76 -0
  120. package/src/utils/stream-transform.ts +292 -0
  121. package/src/utils/terminal-buffer.ts +213 -0
  122. package/src/utils/tool-display.ts +315 -0
  123. package/src/utils/tool-interactive.ts +71 -0
  124. package/src/utils/tty.ts +14 -0
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Cross-cutting built-ins, toggleable via `disabledBuiltins`.
3
+ * Module-owned built-ins activate inline:
4
+ * shell-context, tui-renderer → registerShellHandlers (src/shell/)
5
+ * ash (a specific backend) → activateAgent (src/agent/)
6
+ * rolling-history → activateRollingHistory (src/cli/)
7
+ * backend registry → createCore (src/core/)
8
+ */
9
+ import type { ExtensionContext } from "../shell/host-types.js";
10
+
11
+ type ActivateFn = (ctx: ExtensionContext) => void;
12
+
13
+ export const BUILTIN_EXTENSIONS: Array<{
14
+ name: string;
15
+ load: () => Promise<ActivateFn>;
16
+ }> = [
17
+ { name: "slash-commands", load: () => import("./slash-commands/index.js").then(m => m.default) },
18
+ { name: "file-autocomplete", load: () => import("./file-autocomplete.js").then(m => m.default) },
19
+ ];
20
+
21
+ /**
22
+ * Load built-in extensions sequentially, skipping any in the disabled list.
23
+ * Returns the names of extensions that were loaded.
24
+ */
25
+ export async function loadBuiltinExtensions(
26
+ ctx: ExtensionContext,
27
+ disabled: string[] = [],
28
+ ): Promise<string[]> {
29
+ const disabledSet = new Set(disabled);
30
+ const loaded: string[] = [];
31
+ for (const ext of BUILTIN_EXTENSIONS) {
32
+ if (disabledSet.has(ext.name)) continue;
33
+ const activate = await ext.load();
34
+ activate(ctx);
35
+ loaded.push(ext.name);
36
+ }
37
+ return loaded;
38
+ }
@@ -0,0 +1,14 @@
1
+ /** Events slash-commands owns. */
2
+ declare module "../../core/event-bus.js" {
3
+ interface BusEvents {
4
+ "command:register": {
5
+ name: string;
6
+ description: string;
7
+ handler: (args: string) => Promise<void> | void;
8
+ };
9
+ "command:unregister": { name: string };
10
+ "command:execute": { name: string; args: string };
11
+ }
12
+ }
13
+
14
+ export {};
@@ -0,0 +1,269 @@
1
+ /**
2
+ * Slash commands extension.
3
+ *
4
+ * Registers built-in slash commands on the event bus:
5
+ * - Listens for "command:register" to accept commands from extensions
6
+ * - Responds to "autocomplete:request" pipe for /-prefixed completions
7
+ * - Handles "command:execute" events and dispatches to matching handler
8
+ * - Uses "ui:info"/"ui:error" for user feedback (no direct TUI dependency)
9
+ *
10
+ * Argument completion is composable: any extension can onPipe("autocomplete:request")
11
+ * and check payload.command / payload.commandArgs to add completions for any command.
12
+ */
13
+ import "./events.js";
14
+ import { palette as p } from "../../utils/palette.js";
15
+ import type { ExtensionContext } from "../../shell/host-types.js";
16
+ import { discoverSkills, loadSkillContent, type Skill } from "../../agent/skills.js";
17
+ import { reloadExtensions } from "../../core/extension-loader.js";
18
+
19
+ interface SlashCommand {
20
+ name: string;
21
+ description: string;
22
+ handler: (args: string) => Promise<void> | void;
23
+ }
24
+
25
+ export default function activate(ctx: ExtensionContext): void {
26
+ const { bus } = ctx;
27
+ const commands = new Map<string, SlashCommand>();
28
+
29
+ const register = (cmd: SlashCommand) => {
30
+ const name = cmd.name.startsWith("/") ? cmd.name : `/${cmd.name}`;
31
+ if (commands.has(name)) {
32
+ throw new Error(`Command "${name}" already registered. Use ctx.adviseCommand() to wrap it.`);
33
+ }
34
+ commands.set(name, { ...cmd, name });
35
+ ctx.define(`command:${name}`, cmd.handler);
36
+ };
37
+
38
+ // ── Built-in commands ─────────────────────────────────────────
39
+
40
+ register({
41
+ name: "/help",
42
+ description: "Show available commands",
43
+ handler: () => {
44
+ const maxLen = Math.max(...[...commands.values()].map(c => c.name.length));
45
+ const pad = maxLen + 2;
46
+ const lines = [...commands.values()].map(
47
+ (c) => ` ${p.accent}${c.name.padEnd(pad)}${p.reset} ${c.description}`
48
+ );
49
+ bus.emit("ui:info", { message: "Available commands:\n" + lines.join("\n") });
50
+ },
51
+ });
52
+
53
+ register({
54
+ name: "/model",
55
+ description: "Cycle to next model, or switch to a specific one",
56
+ handler: (args) => {
57
+ const name = args.trim();
58
+ const { models, active } = bus.emitPipe("config:get-models", { models: [], active: null });
59
+ if (!name) {
60
+ const label = active ? `${active.id} [${active.provider}]` : "none";
61
+ bus.emit("ui:info", { message: `Model: ${label}` });
62
+ return;
63
+ }
64
+ const atIdx = name.lastIndexOf("@");
65
+ const id = atIdx > 0 ? name.slice(0, atIdx) : name;
66
+ const providerHint = atIdx > 0 ? name.slice(atIdx + 1) : undefined;
67
+ const found = models.find((m) => m.id === id && (!providerHint || m.provider === providerHint));
68
+ if (!found) {
69
+ bus.emit("ui:error", { message: `Unknown model: ${name}` });
70
+ return;
71
+ }
72
+ bus.emit("config:switch-model", { id: found.id, provider: found.provider });
73
+ },
74
+ });
75
+
76
+ register({
77
+ name: "/thinking",
78
+ description: "Set thinking/reasoning effort level",
79
+ handler: (args) => {
80
+ const level = args.trim();
81
+ if (!level) {
82
+ const { level: current, levels, supported } = bus.emitPipe("config:get-thinking", { level: "off", levels: [], supported: true });
83
+ const status = supported ? current : `${current} (not supported by current model)`;
84
+ bus.emit("ui:info", { message: `Thinking: ${status} (options: ${levels.join(", ")})` });
85
+ } else {
86
+ bus.emit("config:set-thinking", { level });
87
+ }
88
+ },
89
+ });
90
+
91
+ register({
92
+ name: "/backend",
93
+ description: "List or switch agent backend",
94
+ handler: (args) => {
95
+ const name = args.trim();
96
+ if (!name) {
97
+ bus.emit("config:list-backends", {});
98
+ } else {
99
+ bus.emit("config:switch-backend", { name });
100
+ }
101
+ },
102
+ });
103
+
104
+ register({
105
+ name: "/reload",
106
+ description: "Reload user extensions from ~/.agent-sh/extensions/",
107
+ handler: async () => {
108
+ const names = await reloadExtensions(ctx);
109
+ if (names.length > 0) {
110
+ bus.emit("ui:info", { message: `Reloaded: ${names.join(", ")}` });
111
+ } else {
112
+ bus.emit("ui:info", { message: "No extensions to reload." });
113
+ }
114
+ },
115
+ });
116
+
117
+ // Handler form so extensions can trigger reload programmatically
118
+ // (e.g. an ash-callable reload_extensions tool in superash).
119
+ ctx.define("extensions:reload", async () => {
120
+ return await reloadExtensions(ctx);
121
+ });
122
+
123
+ // ── Extension registration ────────────────────────────────────
124
+
125
+ bus.on("command:register", (cmd) => {
126
+ register(cmd);
127
+ });
128
+
129
+ bus.on("command:unregister", ({ name }) => {
130
+ const key = name.startsWith("/") ? name : `/${name}`;
131
+ commands.delete(key);
132
+ // Handler entry retained so external advisors survive a reload of the owner.
133
+ });
134
+
135
+ // ── Skill commands (/skill:<name>) ────────────────────────────
136
+
137
+ const getSkills = (): Skill[] => {
138
+ const cwd = (ctx.call("cwd") as string) ?? process.cwd();
139
+ return discoverSkills(cwd);
140
+ };
141
+
142
+ const handleSkillCommand = (skillName: string, args: string) => {
143
+ const skills = getSkills();
144
+ const skill = skills.find(s => s.name === skillName);
145
+ if (!skill) {
146
+ bus.emit("ui:error", { message: `Unknown skill: ${skillName}` });
147
+ return;
148
+ }
149
+
150
+ const content = loadSkillContent(skill);
151
+ if (!content) {
152
+ bus.emit("ui:error", { message: `Failed to load skill: ${skillName}` });
153
+ return;
154
+ }
155
+
156
+ const query = args.trim()
157
+ ? `${content}\n\n${args.trim()}`
158
+ : content;
159
+ bus.emit("agent:submit", { query });
160
+ };
161
+
162
+ // ── Autocomplete: command names ───────────────────────────────
163
+
164
+ bus.onPipe("autocomplete:request", (payload) => {
165
+ if (!payload.buffer.startsWith("/")) return payload;
166
+ // Argument completion is handled by separate pipe handlers below
167
+ if (payload.command) return payload;
168
+
169
+ const prefix = payload.buffer.toLowerCase();
170
+ const matching = [...commands.values()]
171
+ .filter((c) => c.name.toLowerCase().startsWith(prefix))
172
+ .map((c) => ({ name: c.name, description: c.description }));
173
+
174
+ // Skill commands
175
+ if (prefix.startsWith("/skill:") || "/skill:".startsWith(prefix)) {
176
+ const skills = getSkills();
177
+ for (const skill of skills) {
178
+ const name = `/skill:${skill.name}`;
179
+ if (name.toLowerCase().startsWith(prefix)) {
180
+ matching.push({ name, description: skill.description });
181
+ }
182
+ }
183
+ }
184
+
185
+ if (matching.length === 0) return payload;
186
+ return { ...payload, items: [...payload.items, ...matching] };
187
+ });
188
+
189
+ // ── Autocomplete: /model arguments ─────────────────────────────
190
+
191
+ bus.onPipe("autocomplete:request", (payload) => {
192
+ if (payload.command !== "/model") return payload;
193
+ const partial = (payload.commandArgs ?? "").toLowerCase();
194
+ const { models, active } = bus.emitPipe("config:get-models", { models: [], active: null });
195
+ const counts = new Map<string, number>();
196
+ for (const m of models) counts.set(m.id, (counts.get(m.id) ?? 0) + 1);
197
+ const items = models
198
+ .filter((m) => m.id.toLowerCase().includes(partial))
199
+ .slice(0, 15)
200
+ .map((m) => {
201
+ const ambiguous = (counts.get(m.id) ?? 0) > 1;
202
+ const qualified = ambiguous ? `${m.id}@${m.provider}` : m.id;
203
+ return {
204
+ name: `/model ${qualified}`,
205
+ description: `[${m.provider}]${active && m.id === active.id && m.provider === active.provider ? " (active)" : ""}`,
206
+ };
207
+ });
208
+ if (items.length === 0) return payload;
209
+ return { ...payload, items: [...payload.items, ...items] };
210
+ });
211
+
212
+ // ── Autocomplete: /thinking arguments ─────────────────────────
213
+
214
+ bus.onPipe("autocomplete:request", (payload) => {
215
+ if (payload.command !== "/thinking") return payload;
216
+ const partial = (payload.commandArgs ?? "").toLowerCase();
217
+ const { level: current, levels } = bus.emitPipe("config:get-thinking", { level: "off", levels: [], supported: true });
218
+ const items = levels
219
+ .filter((l) => l.startsWith(partial))
220
+ .map((l) => ({
221
+ name: `/thinking ${l}`,
222
+ description: l === current ? "(active)" : "",
223
+ }));
224
+ if (items.length === 0) return payload;
225
+ return { ...payload, items: [...payload.items, ...items] };
226
+ });
227
+
228
+ // ── Autocomplete: /backend arguments ──────────────────────────
229
+
230
+ bus.onPipe("autocomplete:request", (payload) => {
231
+ if (payload.command !== "/backend") return payload;
232
+ const partial = (payload.commandArgs ?? "").toLowerCase();
233
+ const { names, active } = bus.emitPipe("config:get-backends", { names: [], active: null });
234
+ const items = names
235
+ .filter((n) => n.toLowerCase().startsWith(partial))
236
+ .map((n) => ({
237
+ name: `/backend ${n}`,
238
+ description: n === active ? "(active)" : "",
239
+ }));
240
+ if (items.length === 0) return payload;
241
+ return { ...payload, items: [...payload.items, ...items] };
242
+ });
243
+
244
+ // ── Dispatch ──────────────────────────────────────────────────
245
+
246
+ bus.on("command:execute", (e) => {
247
+ if (e.name.startsWith("/skill:")) {
248
+ const skillName = e.name.slice("/skill:".length);
249
+ handleSkillCommand(skillName, e.args);
250
+ return;
251
+ }
252
+
253
+ const cmd = commands.get(e.name);
254
+ if (cmd) {
255
+ const result = ctx.call(`command:${e.name}`, e.args);
256
+ if (result instanceof Promise) {
257
+ result.catch((err) => {
258
+ bus.emit("ui:error", {
259
+ message: err instanceof Error ? err.message : String(err),
260
+ });
261
+ });
262
+ }
263
+ } else {
264
+ bus.emit("ui:info", {
265
+ message: `Unknown command: ${e.name}. Type /help for available commands.`,
266
+ });
267
+ }
268
+ });
269
+ }
@@ -0,0 +1,73 @@
1
+ /** Events owned by the shell subsystem. */
2
+ declare module "../core/event-bus.js" {
3
+ interface BusEvents {
4
+ "shell:command-start": { command: string; cwd: string };
5
+ "shell:command-done": {
6
+ command: string;
7
+ output: string;
8
+ outputRaw: string;
9
+ cwd: string;
10
+ exitCode: number | null;
11
+ };
12
+ "shell:cwd-change": { cwd: string };
13
+ "shell:foreground-busy": { busy: boolean };
14
+ "shell:agent-exec-start": Record<string, never>;
15
+ "shell:agent-exec-done": Record<string, never>;
16
+
17
+ /** Mark the next user-emitted shell command as excluded from <shell_events>. */
18
+ "shell:user-exec-exclude-next": Record<string, never>;
19
+
20
+ "shell:pty-data": { raw: string };
21
+ "shell:pty-write": { data: string };
22
+ "shell:pty-resize": { cols: number; rows: number };
23
+
24
+ "shell:host-write": { data: string };
25
+
26
+ "shell:buffer-request": Record<string, never>;
27
+ "shell:buffer-snapshot": {
28
+ text: string;
29
+ altScreen: boolean;
30
+ cursor: { x: number; y: number };
31
+ };
32
+
33
+ "shell:stdout-hold": Record<string, never>;
34
+ "shell:stdout-release": Record<string, never>;
35
+ "shell:stdout-show": Record<string, never>;
36
+ "shell:stdout-hide": Record<string, never>;
37
+
38
+ /** Sync pipe: handled=true suppresses default redraw. */
39
+ "shell:redraw-prompt": {
40
+ cwd: string;
41
+ kind: "fresh" | "redraw";
42
+ handled: boolean;
43
+ };
44
+
45
+ /** Async pipe: extension → user's PTY. */
46
+ "shell:exec-request": {
47
+ command: string;
48
+ output: string;
49
+ cwd: string;
50
+ exitCode: number | null;
51
+ done: boolean;
52
+ };
53
+
54
+ "input-mode:register": import("./host-types.js").InputModeConfig;
55
+ "input:keypress": { key: string };
56
+ "input:intercept": { data: string; consumed: boolean };
57
+ "input:redraw": Record<string, never>;
58
+
59
+ "compositor:write": { stream: string; text: string };
60
+
61
+ /** Sync pipe: extensions append items. */
62
+ "autocomplete:request": {
63
+ buffer: string;
64
+ /** "/backend" or null if not a command. */
65
+ command: string | null;
66
+ /** Text after the command name, or null. */
67
+ commandArgs: string | null;
68
+ items: { name: string; description: string }[];
69
+ };
70
+ }
71
+ }
72
+
73
+ export {};
@@ -0,0 +1,150 @@
1
+ import type { EventBus } from "../core/event-bus.js";
2
+ import type { CoreConfig, CoreContext } from "../core/types.js";
3
+ import type { AgentSurface } from "../agent/host-types.js";
4
+ import type { ColorPalette } from "../utils/palette.js";
5
+ import type { Compositor, RenderSurface } from "../utils/compositor.js";
6
+ import type { BlockTransformOptions, FencedBlockTransformOptions } from "../utils/stream-transform.js";
7
+
8
+ export type { BlockTransformOptions, FencedBlockTransformOptions } from "../utils/stream-transform.js";
9
+ export type { RenderSurface } from "../utils/compositor.js";
10
+
11
+ // ── Remote sessions ──────────────────────────────────────────────
12
+
13
+ export interface RemoteSessionOptions {
14
+ /** The surface to render agent output to. */
15
+ surface: RenderSurface;
16
+ /** Suppress response borders (default: true). */
17
+ suppressBorders?: boolean;
18
+ /** Suppress user query box (default: false).
19
+ * True for sessions with their own input (rsplit, overlay).
20
+ * False for sessions where input comes from the main shell (split). */
21
+ suppressQueryBox?: boolean;
22
+ /** Suppress usage stats line (default: true). */
23
+ suppressUsage?: boolean;
24
+ }
25
+
26
+ export interface RemoteSession {
27
+ /** Submit a query to the agent from this session. */
28
+ submit(query: string): void;
29
+ /** The surface this session renders to. */
30
+ readonly surface: RenderSurface;
31
+ /** Whether this session is currently active. */
32
+ readonly active: boolean;
33
+ /** Tear down — restores all routing and advisors. */
34
+ close(): void;
35
+ }
36
+
37
+ // ── Input modes ──────────────────────────────────────────────────
38
+
39
+ /**
40
+ * Configuration for a registered input mode.
41
+ * Extensions emit "input-mode:register" with this shape to add new modes.
42
+ */
43
+ export interface InputModeConfig {
44
+ id: string; // unique identifier, e.g. "agent", "translate"
45
+ trigger: string; // single char trigger at empty line start: "?", ">"
46
+ label: string; // human-readable label shown in prompt
47
+ promptIcon: string; // the chevron/icon character, e.g. "❯", "⟩"
48
+ indicator: string; // status indicator shown before the icon, e.g. "❓", "●"
49
+ onSubmit(query: string, bus: EventBus): void;
50
+ returnToSelf: boolean; // re-enter this mode after agent processing?
51
+ }
52
+
53
+ export interface TerminalSession {
54
+ id: string;
55
+ command: string;
56
+ output: string;
57
+ exitCode: number | null;
58
+ done: boolean;
59
+ resolve?: (value: void) => void;
60
+ }
61
+
62
+ // ── Shell-host surface ───────────────────────────────────────────
63
+
64
+ /**
65
+ * Capabilities the shell host adds to the extension context, exposed
66
+ * on the nested `ctx.shell` field. Available only when the TUI shell
67
+ * frontend is loaded; under headless backends these methods are silent
68
+ * no-ops (bus emits with no listeners).
69
+ */
70
+ export interface ShellSurface {
71
+ /** Routes named render streams ("agent", "query", "status", or any
72
+ * extension-defined name) to terminal surfaces. Frontends register
73
+ * default surfaces during activation; extensions can `redirect()`
74
+ * to capture output. Shell-scoped because today only the TUI uses
75
+ * it — bus events are the wire for other frontends. */
76
+ compositor: Compositor;
77
+
78
+ /** Override color palette slots for theming. */
79
+ setPalette: (overrides: Partial<ColorPalette>) => void;
80
+
81
+ /** Register a delimiter-based content transform (e.g. $$...$$ → image). */
82
+ createBlockTransform: (opts: BlockTransformOptions) => void;
83
+ /** Register a fenced block transform (e.g. ```lang...``` → code-block). */
84
+ createFencedBlockTransform: (opts: FencedBlockTransformOptions) => void;
85
+
86
+ /** Wrap an input mode's `onSubmit`. Lets extensions transform queries
87
+ * on the way to the agent (logging, redaction, vetoing). The mode
88
+ * must already be registered via the `input-mode:register` bus event. */
89
+ adviseInputMode: (
90
+ id: string,
91
+ advisor: (
92
+ next: (query: string, bus: EventBus) => void,
93
+ query: string,
94
+ bus: EventBus,
95
+ ) => void,
96
+ ) => () => void;
97
+
98
+ /** Create a remote session that routes agent output to a surface and
99
+ * optionally accepts queries. Handles compositor routing, shell
100
+ * lifecycle advisors, and chrome suppression. */
101
+ createRemoteSession: (opts: RemoteSessionOptions) => RemoteSession;
102
+ }
103
+
104
+ /** Substrate + shell surface. Use this when an extension only touches
105
+ * shell features (themes, palette, transforms) and doesn't need the
106
+ * agent surface. */
107
+ export type ShellContext = CoreContext & { shell: ShellSurface };
108
+
109
+ // ── Extension-facing context ─────────────────────────────────────
110
+
111
+ /**
112
+ * What extension `activate()` functions receive. Substrate (`CoreContext`)
113
+ * + slash-command registration + host surfaces, which are **optional**
114
+ * because hosts attach them on activation: under headless backends
115
+ * `ctx.shell` is undefined; under bridge backends `ctx.agent` may be
116
+ * undefined too. Extensions guard with `ctx.shell?.foo()` /
117
+ * `if (!ctx.agent) return;`, or type their parameter as the narrower
118
+ * `AgentContext` / `ShellContext` to declare host requirements (those
119
+ * variants make the surface non-optional). When both hosts are required,
120
+ * intersect them at the use site: `ctx: AgentContext & ShellContext`.
121
+ */
122
+ export type ExtensionContext = CoreContext & {
123
+ registerCommand: (name: string, description: string, handler: (args: string) => Promise<void> | void) => void;
124
+ /** Wrap an already-registered command's handler. Name is normalized
125
+ * (leading `/` optional). */
126
+ adviseCommand: (
127
+ name: string,
128
+ advisor: (
129
+ next: (args: string) => Promise<void> | void,
130
+ args: string,
131
+ ) => Promise<void> | void,
132
+ ) => () => void;
133
+ agent?: AgentSurface;
134
+ shell?: ShellSurface;
135
+ };
136
+
137
+ // ── Shell-host config surface ────────────────────────────────────
138
+
139
+ export interface ShellConfigSurface {
140
+ /** Shell binary (e.g. /bin/zsh) launched by the PTY frontend. */
141
+ shell?: string;
142
+ }
143
+
144
+ export type ShellConfig = CoreConfig & ShellConfigSurface;
145
+
146
+ /** The full application config — substrate + agent + shell startup options.
147
+ * Prefer this in CLI/embedder code; layered names (`CoreConfig`,
148
+ * `AgentConfig`, `ShellConfig`) are for code that cares about which
149
+ * host owns which fields. */
150
+ export type AppConfig = CoreConfig & import("../agent/host-types.js").AgentConfigSurface & ShellConfigSurface;