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
@@ -44,21 +44,20 @@ export function createLsTool(getCwd) {
44
44
  const entries = await fs.readdir(absPath, {
45
45
  withFileTypes: true,
46
46
  });
47
- const lines = [];
48
- for (const e of entries) {
47
+ const items = await Promise.all(entries.map(async (e) => {
49
48
  const fullPath = path.join(absPath, e.name);
50
49
  try {
51
50
  const stat = await fs.stat(fullPath);
52
51
  const size = e.isDirectory() ? "-" : formatSize(stat.size);
53
52
  const mtime = stat.mtime.toISOString().slice(0, 16).replace("T", " ");
54
- lines.push(`${mtime} ${size.padStart(8)} ${e.isDirectory() ? e.name + "/" : e.name}`);
53
+ return `${mtime} ${size.padStart(8)} ${e.isDirectory() ? e.name + "/" : e.name}`;
55
54
  }
56
55
  catch {
57
- lines.push(`${"?".padStart(16)} ${"?".padStart(8)} ${e.isDirectory() ? e.name + "/" : e.name}`);
56
+ return `${"?".padStart(16)} ${"?".padStart(8)} ${e.isDirectory() ? e.name + "/" : e.name}`;
58
57
  }
59
- }
58
+ }));
60
59
  return {
61
- content: lines.join("\n") || "(empty directory)",
60
+ content: items.join("\n") || "(empty directory)",
62
61
  exitCode: 0,
63
62
  isError: false,
64
63
  };
@@ -38,7 +38,7 @@ export type ToolResultBody = {
38
38
  maxLines?: number;
39
39
  };
40
40
  export interface ToolDisplayInfo {
41
- kind: "read" | "write" | "execute" | "search" | "display";
41
+ kind: "read" | "write" | "execute" | "search";
42
42
  locations?: {
43
43
  path: string;
44
44
  line?: number | null;
@@ -47,13 +47,33 @@ export interface ToolDisplayInfo {
47
47
  * icon + detail only. When absent, the tool name is shown alongside the detail. */
48
48
  icon?: string;
49
49
  }
50
+ /** Interactive UI session — imperative control over rendering + input. */
51
+ export interface InteractiveSession<T> {
52
+ /** Return lines to render. Called on mount and after each input. */
53
+ render(width: number): string[];
54
+ /** Handle raw input. Call done(result) to finish the session. */
55
+ handleInput(data: string, done: (result: T) => void): void;
56
+ /** Called when session starts. Receives invalidate() for async re-renders. */
57
+ onMount?(invalidate: () => void): void;
58
+ /** Called when session ends (cleanup). */
59
+ onUnmount?(): void;
60
+ }
61
+ /** Interactive UI capability passed to tools during execution. */
62
+ export interface ToolUI {
63
+ /** Present a custom interactive UI and wait for the user's response. */
64
+ custom<T>(session: InteractiveSession<T>): Promise<T>;
65
+ }
66
+ /** Context passed to tool execute() as optional third parameter. */
67
+ export interface ToolExecutionContext {
68
+ ui: ToolUI;
69
+ }
50
70
  export interface ToolDefinition {
51
71
  name: string;
52
72
  /** Short label for TUI display (e.g. "search" instead of "ads_search"). Defaults to name. */
53
73
  displayName?: string;
54
74
  description: string;
55
75
  input_schema: Record<string, unknown>;
56
- execute(args: Record<string, unknown>, onChunk?: (chunk: string) => void): Promise<ToolResult>;
76
+ execute(args: Record<string, unknown>, onChunk?: (chunk: string) => void, ctx?: ToolExecutionContext): Promise<ToolResult>;
57
77
  /** Whether to stream tool output to the TUI (default: true). */
58
78
  showOutput?: boolean;
59
79
  /** Whether this tool may modify files — triggers file watcher (default: false). */
@@ -24,6 +24,23 @@ export declare class ContextManager {
24
24
  * Optional start/end restrict to a line range (1-indexed).
25
25
  */
26
26
  expand(ids: number[], start?: number, end?: number): string;
27
+ /**
28
+ * Return shell events with id > afterId, formatted as an incremental
29
+ * delta suitable for injection into conversation history. Skips
30
+ * agent-source commands (already visible in tool results). Returns
31
+ * null when nothing new exists.
32
+ *
33
+ * The motivation: resending the full <shell_context> every turn wastes
34
+ * tokens — N turns × full history = O(N²) cost for O(N) information.
35
+ * Instead we inject only new events as regular conversation messages,
36
+ * so the provider's prefix cache amortizes them to O(N).
37
+ */
38
+ getEventsSince(afterId: number): {
39
+ text: string;
40
+ lastSeq: number;
41
+ } | null;
42
+ /** Highest exchange id seen so far (0 if none). */
43
+ lastSeq(): number;
27
44
  /**
28
45
  * One-line summaries of last N exchanges.
29
46
  */
@@ -140,6 +140,43 @@ export class ContextManager {
140
140
  }
141
141
  return results.join("\n\n");
142
142
  }
143
+ /**
144
+ * Return shell events with id > afterId, formatted as an incremental
145
+ * delta suitable for injection into conversation history. Skips
146
+ * agent-source commands (already visible in tool results). Returns
147
+ * null when nothing new exists.
148
+ *
149
+ * The motivation: resending the full <shell_context> every turn wastes
150
+ * tokens — N turns × full history = O(N²) cost for O(N) information.
151
+ * Instead we inject only new events as regular conversation messages,
152
+ * so the provider's prefix cache amortizes them to O(N).
153
+ */
154
+ getEventsSince(afterId) {
155
+ const fresh = this.exchanges.filter((e) => e.id > afterId && !(e.type === "shell_command" && e.source === "agent"));
156
+ if (fresh.length === 0)
157
+ return null;
158
+ const lastSeq = this.exchanges[this.exchanges.length - 1].id;
159
+ // Apply per-type truncation so giant outputs don't blow up the turn.
160
+ const truncated = fresh.map((ex) => {
161
+ if (ex.type === "shell_command") {
162
+ const s = getSettings();
163
+ return {
164
+ ...ex,
165
+ output: truncateOutput(ex.output, s.shellTruncateThreshold, s.shellHeadLines, s.shellTailLines, ex.id),
166
+ };
167
+ }
168
+ return { ...ex };
169
+ });
170
+ const body = truncated.map((ex) => this.formatExchangeTruncated(ex)).join("\n");
171
+ return {
172
+ text: `<shell-events>\n${body}</shell-events>`,
173
+ lastSeq,
174
+ };
175
+ }
176
+ /** Highest exchange id seen so far (0 if none). */
177
+ lastSeq() {
178
+ return this.exchanges.length === 0 ? 0 : this.exchanges[this.exchanges.length - 1].id;
179
+ }
143
180
  /**
144
181
  * One-line summaries of last N exchanges.
145
182
  */
@@ -229,13 +266,9 @@ export class ContextManager {
229
266
  out += `The user interacts with a real shell (PTY) and sends you queries inline. You are there to help them with their tasks.\n`;
230
267
  out += `\n`;
231
268
  out += `IMPORTANT tool usage rules:\n`;
232
- out += `- user_shell runs commands in the user's live shell (PTY). The user sees output directly — no summary needed.\n`;
233
269
  out += `- Your internal tools (bash, read, write, ls, etc.) run in an isolated subprocess. The user CANNOT see their output.\n`;
234
- out += `- When the user asks to see, list, view, or display anything, ALWAYS use user_shell. NEVER use internal tools like ls/read/bash for display — the user won't see it.\n`;
235
270
  out += `- Only use internal tools when YOU need to reason about content silently (e.g. reading a file to answer a question about it).\n`;
236
- out += `- After a user_shell command, the user already saw the output. Do NOT repeat or summarize it.\n`;
237
271
  out += `- You can browse or search shell history with shell_recall.\n`;
238
- out += `- You can browse or search evicted conversation turns with conversation_recall.\n`;
239
272
  out += `\n`;
240
273
  this.firstPrompt = false;
241
274
  }
package/dist/core.d.ts CHANGED
@@ -1,13 +1,13 @@
1
1
  /**
2
2
  * Core kernel — the minimum viable agent-sh.
3
3
  *
4
- * Wires up EventBus + ContextManager + AgentBackend without any frontend.
4
+ * Wires up EventBus + ContextManager without any frontend or agent backend.
5
5
  * Consumers attach their own I/O (Shell, WebSocket, REST, tests) by
6
6
  * subscribing to bus events.
7
7
  *
8
- * The default backend (AgentLoop) is created eagerly but wired lazily —
9
- * extensions can register alternative backends via agent:register-backend
10
- * before activateBackend() is called.
8
+ * Agent backends are loaded as extensions and register themselves via
9
+ * the agent:register-backend bus event. The built-in "ash" backend is
10
+ * loaded from src/extensions/agent-backend.ts.
11
11
  *
12
12
  * Usage:
13
13
  * import { createCore } from "agent-sh";
@@ -18,8 +18,8 @@
18
18
  */
19
19
  import { EventBus } from "./event-bus.js";
20
20
  import { ContextManager } from "./context-manager.js";
21
- import { LlmClient } from "./utils/llm-client.js";
22
21
  import type { AgentShellConfig, ExtensionContext } from "./types.js";
22
+ import { HandlerRegistry } from "./utils/handler-registry.js";
23
23
  export { EventBus } from "./event-bus.js";
24
24
  export type { ShellEvents } from "./event-bus.js";
25
25
  export type { AgentShellConfig, ExtensionContext } from "./types.js";
@@ -31,8 +31,8 @@ export { LlmClient } from "./utils/llm-client.js";
31
31
  export interface AgentShellCore {
32
32
  bus: EventBus;
33
33
  contextManager: ContextManager;
34
- /** LLM client for fast-path features (null when no provider configured). */
35
- llmClient: LlmClient | null;
34
+ /** Handler registry for define/advise/call. */
35
+ handlers: HandlerRegistry;
36
36
  /** Activate the agent backend (call after extensions load). */
37
37
  activateBackend(): void;
38
38
  /** Convenience: emit agent:submit and await the response. */
package/dist/core.js CHANGED
@@ -1,13 +1,13 @@
1
1
  /**
2
2
  * Core kernel — the minimum viable agent-sh.
3
3
  *
4
- * Wires up EventBus + ContextManager + AgentBackend without any frontend.
4
+ * Wires up EventBus + ContextManager without any frontend or agent backend.
5
5
  * Consumers attach their own I/O (Shell, WebSocket, REST, tests) by
6
6
  * subscribing to bus events.
7
7
  *
8
- * The default backend (AgentLoop) is created eagerly but wired lazily —
9
- * extensions can register alternative backends via agent:register-backend
10
- * before activateBackend() is called.
8
+ * Agent backends are loaded as extensions and register themselves via
9
+ * the agent:register-backend bus event. The built-in "ash" backend is
10
+ * loaded from src/extensions/agent-backend.ts.
11
11
  *
12
12
  * Usage:
13
13
  * import { createCore } from "agent-sh";
@@ -18,15 +18,17 @@
18
18
  */
19
19
  import { EventBus } from "./event-bus.js";
20
20
  import { ContextManager } from "./context-manager.js";
21
- import { AgentLoop } from "./agent/agent-loop.js";
22
- import { LlmClient } from "./utils/llm-client.js";
23
21
  import { setPalette } from "./utils/palette.js";
24
22
  import * as streamTransform from "./utils/stream-transform.js";
25
23
  import * as settingsMod from "./settings.js";
26
- import { resolveProvider, getProviderNames } from "./settings.js";
27
24
  import { HandlerRegistry } from "./utils/handler-registry.js";
28
25
  import { TerminalBuffer } from "./utils/terminal-buffer.js";
29
- import { FloatingPanel } from "./utils/floating-panel.js";
26
+ import crypto from "node:crypto";
27
+ import * as fs from "node:fs";
28
+ import * as path from "node:path";
29
+ import * as os from "node:os";
30
+ import { DefaultCompositor, StdoutSurface } from "./utils/compositor.js";
31
+ const STORAGE_ROOT = path.join(os.homedir(), ".agent-sh");
30
32
  // Re-export types that library consumers need
31
33
  export { EventBus } from "./event-bus.js";
32
34
  export { palette, setPalette, resetPalette } from "./utils/palette.js";
@@ -36,92 +38,29 @@ export function createCore(config) {
36
38
  const bus = new EventBus();
37
39
  const handlers = new HandlerRegistry();
38
40
  const contextManager = new ContextManager(bus, handlers);
39
- // ── Resolve provider ─────────────────────────────────────────
41
+ // 3 bytes = 6 hex chars, ~16M values — ample for per-lineage uniqueness and
42
+ // short enough to read/remember. Legacy content may have 16-char iids; any
43
+ // parsers should accept ≥6 hex chars.
44
+ const instanceId = crypto.randomBytes(3).toString("hex");
40
45
  const settings = settingsMod.getSettings();
41
- let activeProvider = null;
42
- const providerRegistry = new Map();
43
- for (const name of getProviderNames()) {
44
- const p = resolveProvider(name);
45
- if (p)
46
- providerRegistry.set(name, p);
47
- }
48
- const providerName = config.provider ?? settings.defaultProvider;
49
- if (providerName) {
50
- activeProvider = providerRegistry.get(providerName) ?? null;
51
- }
52
- // Build flat modes list across all providers
53
- const buildModes = () => {
54
- const allModes = [];
55
- for (const [id, p] of providerRegistry) {
56
- if (!p.apiKey)
57
- continue;
58
- for (const model of p.models) {
59
- const mc = p.modelCapabilities?.get(model);
60
- allModes.push({
61
- model,
62
- provider: id,
63
- providerConfig: { apiKey: p.apiKey, baseURL: p.baseURL },
64
- contextWindow: mc?.contextWindow ?? p.contextWindow,
65
- reasoning: mc?.reasoning,
66
- supportsReasoningEffort: p.supportsReasoningEffort,
67
- });
68
- }
69
- }
70
- return allModes;
71
- };
72
- const effectiveApiKey = config.apiKey ?? activeProvider?.apiKey;
73
- const effectiveBaseURL = config.baseURL ?? activeProvider?.baseURL;
74
- const effectiveModel = config.model ?? activeProvider?.defaultModel;
75
- let modes = buildModes();
76
- if (modes.length === 0 && effectiveApiKey && effectiveModel) {
77
- modes = [{ model: effectiveModel }];
78
- }
79
- const initialModeIndex = Math.max(0, modes.findIndex((m) => m.model === effectiveModel && (!activeProvider || m.provider === activeProvider.id)));
80
- // Shared LLM client — used by agent loop AND fast-path features
81
- let llmClient = null;
82
- if (effectiveApiKey) {
83
- if (!effectiveModel) {
84
- throw new Error("No model specified. Use --model or configure a provider with defaultModel in ~/.agent-sh/settings.json");
85
- }
86
- llmClient = new LlmClient({
87
- apiKey: effectiveApiKey,
88
- baseURL: effectiveBaseURL,
89
- model: effectiveModel,
90
- });
91
- }
92
- // Create AgentLoop (unwired — tools only, no bus subscriptions yet)
93
- const agentLoop = llmClient
94
- ? new AgentLoop(bus, contextManager, llmClient, handlers, modes, initialModeIndex)
95
- : null;
46
+ // Expose raw CLI config so the agent backend extension can resolve
47
+ // providers and create the LLM client.
48
+ handlers.define("config:get-shell-config", () => config);
96
49
  const backends = new Map();
97
50
  let activeBackendName = null;
98
51
  const activateByName = async (name, silent = false) => {
99
- const backend = name === "agent-sh" ? null : backends.get(name);
100
- if (name !== "agent-sh" && !backend) {
52
+ const backend = backends.get(name);
53
+ if (!backend) {
101
54
  bus.emit("ui:error", { message: `Unknown backend: ${name}` });
102
55
  return;
103
56
  }
104
57
  // Deactivate current backend
105
- if (activeBackendName === "agent-sh") {
106
- agentLoop?.unwire();
107
- }
108
- else if (activeBackendName) {
58
+ if (activeBackendName) {
109
59
  backends.get(activeBackendName)?.kill();
110
60
  }
111
61
  // Activate new backend
112
- if (name === "agent-sh") {
113
- if (!agentLoop) {
114
- bus.emit("ui:error", { message: "No LLM provider configured for built-in backend" });
115
- return;
116
- }
117
- agentLoop.wire();
118
- activeBackendName = "agent-sh";
119
- bus.emit("agent:info", { name: "agent-sh", version: "0.4", model: llmClient?.model, provider: activeProvider?.id, contextWindow: activeProvider?.contextWindow });
120
- }
121
- else {
122
- await backend.start?.();
123
- activeBackendName = name;
124
- }
62
+ await backend.start?.();
63
+ activeBackendName = name;
125
64
  if (!silent) {
126
65
  bus.emit("ui:info", { message: `Backend: ${name}` });
127
66
  }
@@ -131,107 +70,30 @@ export function createCore(config) {
131
70
  backends.set(backend.name, backend);
132
71
  });
133
72
  bus.on("config:switch-backend", ({ name }) => {
134
- activateByName(name);
73
+ activateByName(name).then(() => {
74
+ if (activeBackendName === name) {
75
+ settingsMod.updateSettings({ defaultBackend: name });
76
+ bus.emit("ui:info", { message: `Saved '${name}' as default backend.` });
77
+ }
78
+ });
135
79
  });
136
80
  bus.on("config:list-backends", () => {
137
- const names = [];
138
- if (agentLoop)
139
- names.push("agent-sh");
140
- for (const name of backends.keys())
141
- names.push(name);
81
+ const names = [...backends.keys()];
142
82
  const list = names
143
83
  .map((n) => n === activeBackendName ? `${n} (active)` : n)
144
84
  .join(", ");
145
85
  bus.emit("ui:info", { message: `Backends: ${list}` });
146
86
  });
147
- bus.onPipe("config:get-backends", (payload) => {
148
- const names = [];
149
- if (agentLoop)
150
- names.push("agent-sh");
151
- for (const name of backends.keys())
152
- names.push(name);
87
+ bus.onPipe("config:get-backends", () => {
88
+ const names = [...backends.keys()];
153
89
  return { names, active: activeBackendName };
154
90
  });
155
- // ── Runtime provider management ──────────────────────────────
156
- bus.on("provider:register", (p) => {
157
- const rawModels = p.models ?? (p.defaultModel ? [p.defaultModel] : []);
158
- const modelIds = [];
159
- const caps = new Map();
160
- for (const m of rawModels) {
161
- if (typeof m === "string") {
162
- modelIds.push(m);
163
- }
164
- else {
165
- modelIds.push(m.id);
166
- caps.set(m.id, { reasoning: m.reasoning, contextWindow: m.contextWindow });
167
- }
168
- }
169
- providerRegistry.set(p.id, {
170
- id: p.id,
171
- apiKey: p.apiKey,
172
- baseURL: p.baseURL,
173
- defaultModel: p.defaultModel,
174
- models: modelIds,
175
- supportsReasoningEffort: p.supportsReasoningEffort,
176
- modelCapabilities: caps.size > 0 ? caps : undefined,
177
- });
178
- // Push registered models into the agent loop so they appear in
179
- // autocomplete and are selectable via /model.
180
- const addModes = modelIds.map((m) => {
181
- const mc = caps.get(m);
182
- return {
183
- model: m,
184
- provider: p.id,
185
- providerConfig: { apiKey: p.apiKey ?? "", baseURL: p.baseURL },
186
- contextWindow: mc?.contextWindow,
187
- reasoning: mc?.reasoning,
188
- supportsReasoningEffort: p.supportsReasoningEffort,
189
- };
190
- });
191
- bus.emit("config:add-modes", { modes: addModes });
192
- });
193
- bus.on("config:switch-provider", ({ provider: name }) => {
194
- const p = providerRegistry.get(name);
195
- if (!p) {
196
- bus.emit("ui:error", { message: `Unknown provider: ${name}` });
197
- return;
198
- }
199
- if (!llmClient) {
200
- bus.emit("ui:error", { message: `Provider switching requires internal agent mode` });
201
- return;
202
- }
203
- const newApiKey = p.apiKey;
204
- if (!newApiKey) {
205
- bus.emit("ui:error", { message: `Provider "${name}" has no API key configured` });
206
- return;
207
- }
208
- const switchModel = p.defaultModel ?? p.models[0];
209
- if (!switchModel) {
210
- bus.emit("ui:error", { message: `Provider "${name}" has no models configured` });
211
- return;
212
- }
213
- llmClient.reconfigure({
214
- apiKey: newApiKey,
215
- baseURL: p.baseURL,
216
- model: switchModel,
217
- });
218
- const newModes = p.models.map((m) => {
219
- const mc = p.modelCapabilities?.get(m);
220
- return {
221
- model: m,
222
- provider: name,
223
- providerConfig: { apiKey: newApiKey, baseURL: p.baseURL },
224
- contextWindow: mc?.contextWindow ?? p.contextWindow,
225
- reasoning: mc?.reasoning,
226
- supportsReasoningEffort: p.supportsReasoningEffort,
227
- };
228
- });
229
- bus.emit("config:set-modes", { modes: newModes });
230
- activeProvider = p;
231
- bus.emit("agent:info", { name: "agent-sh", version: "0.4", model: switchModel, provider: name, contextWindow: p.contextWindow });
232
- bus.emit("ui:info", { message: `Switched to ${name} (${switchModel})` });
233
- bus.emit("config:changed", {});
234
- });
91
+ // ── Compositor ──────────────────────────────────────────────
92
+ const compositor = new DefaultCompositor(bus);
93
+ const stdoutSurface = new StdoutSurface();
94
+ compositor.setDefault("agent", stdoutSurface);
95
+ compositor.setDefault("query", stdoutSurface);
96
+ compositor.setDefault("status", stdoutSurface);
235
97
  // ── Lazy singleton terminal buffer ──────────────────────────
236
98
  let terminalBufferSingleton; // undefined = not yet created
237
99
  const getTerminalBuffer = () => {
@@ -243,23 +105,17 @@ export function createCore(config) {
243
105
  return {
244
106
  bus,
245
107
  contextManager,
246
- llmClient,
108
+ handlers,
247
109
  activateBackend() {
248
110
  // Silent — backend info is shown in the startup banner.
249
111
  // Runtime switches (config:switch-backend) still emit ui:info.
112
+ if (backends.size === 0)
113
+ return;
250
114
  const preferred = settings.defaultBackend;
251
115
  if (preferred && backends.has(preferred)) {
252
116
  activateByName(preferred, true);
253
117
  }
254
- else if (backends.size > 0 && !agentLoop) {
255
- activateByName(backends.keys().next().value, true);
256
- }
257
- else if (agentLoop) {
258
- agentLoop.wire();
259
- activeBackendName = "agent-sh";
260
- bus.emit("agent:info", { name: "agent-sh", version: "0.4", model: llmClient?.model, provider: activeProvider?.id, contextWindow: activeProvider?.contextWindow });
261
- }
262
- else if (backends.size > 0) {
118
+ else {
263
119
  activateByName(backends.keys().next().value, true);
264
120
  }
265
121
  },
@@ -301,33 +157,80 @@ export function createCore(config) {
301
157
  bus.emit("agent:cancel-request", {});
302
158
  },
303
159
  extensionContext(opts) {
304
- return {
160
+ const ctx = {
305
161
  bus,
306
162
  contextManager,
307
- llmClient,
163
+ instanceId,
308
164
  quit: opts.quit,
309
165
  setPalette,
310
166
  createBlockTransform: (o) => streamTransform.createBlockTransform(bus, o),
311
167
  createFencedBlockTransform: (o) => streamTransform.createFencedBlockTransform(bus, o),
312
168
  getExtensionSettings: settingsMod.getExtensionSettings,
169
+ getStoragePath: (namespace) => {
170
+ const dir = path.join(STORAGE_ROOT, namespace);
171
+ fs.mkdirSync(dir, { recursive: true });
172
+ return dir;
173
+ },
313
174
  registerCommand: (name, description, handler) => bus.emit("command:register", { name, description, handler }),
314
- registerTool: (tool) => agentLoop?.registerTool(tool),
315
- getTools: () => agentLoop?.getTools() ?? [],
175
+ registerTool: (tool) => bus.emit("agent:register-tool", { tool, extensionName: "" }),
176
+ unregisterTool: (name) => bus.emit("agent:unregister-tool", { name }),
177
+ getTools: () => bus.emitPipe("agent:get-tools", { tools: [] }).tools,
178
+ registerInstruction: (name, text) => bus.emit("agent:register-instruction", { name, text, extensionName: "" }),
179
+ removeInstruction: (name) => bus.emit("agent:remove-instruction", { name }),
180
+ registerSkill: (name, description, filePath) => bus.emit("agent:register-skill", { name, description, filePath, extensionName: "" }),
181
+ removeSkill: (name) => bus.emit("agent:remove-skill", { name }),
316
182
  define: (name, fn) => handlers.define(name, fn),
317
183
  advise: (name, wrapper) => handlers.advise(name, wrapper),
318
184
  call: (name, ...args) => handlers.call(name, ...args),
185
+ list: () => handlers.list(),
319
186
  get terminalBuffer() { return getTerminalBuffer(); },
320
- createFloatingPanel: (config) => {
321
- const tb = config.dimBackground !== false ? getTerminalBuffer() : null;
322
- return new FloatingPanel(bus, { ...config, terminalBuffer: tb ?? undefined });
187
+ compositor,
188
+ createRemoteSession: (opts) => {
189
+ const { surface } = opts;
190
+ const cleanups = [];
191
+ let active = true;
192
+ // Redirect all render streams
193
+ cleanups.push(compositor.redirect("agent", surface));
194
+ cleanups.push(compositor.redirect("query", surface));
195
+ cleanups.push(compositor.redirect("status", surface));
196
+ // Keep shell interactive
197
+ cleanups.push(handlers.advise("shell:on-processing-start", (next) => active ? undefined : next()));
198
+ cleanups.push(handlers.advise("shell:on-processing-done", (next) => active ? undefined : next()));
199
+ // Suppress chrome
200
+ if (opts.suppressBorders !== false) {
201
+ cleanups.push(handlers.advise("tui:response-border", (next, ...a) => active ? null : next(...a)));
202
+ }
203
+ if (opts.suppressQueryBox) {
204
+ cleanups.push(handlers.advise("tui:render-user-query", (next, ...a) => active ? [] : next(...a)));
205
+ }
206
+ if (opts.suppressUsage !== false) {
207
+ cleanups.push(handlers.advise("tui:render-usage", (next, ...a) => active ? "" : next(...a)));
208
+ }
209
+ if (opts.interactive) {
210
+ cleanups.push(handlers.advise("dynamic-context:build", (next) => {
211
+ const base = next();
212
+ return active ? base + "\ninteractive-session: true\n" : base;
213
+ }));
214
+ }
215
+ return {
216
+ submit(query) { bus.emit("agent:submit", { query }); },
217
+ get surface() { return surface; },
218
+ get active() { return active; },
219
+ close() {
220
+ if (!active)
221
+ return;
222
+ active = false;
223
+ for (const fn of cleanups.reverse())
224
+ fn();
225
+ cleanups.length = 0;
226
+ },
227
+ };
323
228
  },
324
229
  };
230
+ return ctx;
325
231
  },
326
232
  kill() {
327
- if (activeBackendName === "agent-sh") {
328
- agentLoop?.kill();
329
- }
330
- else if (activeBackendName) {
233
+ if (activeBackendName) {
331
234
  backends.get(activeBackendName)?.kill();
332
235
  }
333
236
  },