agent-sh 0.12.26 → 0.13.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 (144) hide show
  1. package/README.md +13 -2
  2. package/dist/agent/agent-loop.d.ts +3 -5
  3. package/dist/agent/agent-loop.js +44 -100
  4. package/dist/agent/conversation-state.d.ts +9 -0
  5. package/dist/agent/conversation-state.js +38 -1
  6. package/dist/agent/history-file.d.ts +6 -0
  7. package/dist/agent/history-file.js +1 -1
  8. package/dist/agent/host-types.d.ts +125 -0
  9. package/dist/agent/index.d.ts +12 -4
  10. package/dist/agent/index.js +357 -6
  11. package/dist/agent/nuclear-form.d.ts +7 -0
  12. package/dist/{extensions → agent}/providers/deepseek.d.ts +2 -2
  13. package/dist/{extensions → agent}/providers/deepseek.js +5 -4
  14. package/dist/{extensions → agent}/providers/openai-compatible.d.ts +2 -2
  15. package/dist/{extensions → agent}/providers/openai.d.ts +2 -2
  16. package/dist/{extensions → agent}/providers/openai.js +3 -2
  17. package/dist/{extensions → agent}/providers/openrouter.d.ts +2 -2
  18. package/dist/{extensions → agent}/providers/openrouter.js +4 -3
  19. package/dist/agent/skills.js +51 -7
  20. package/dist/agent/subagent.d.ts +1 -1
  21. package/dist/agent/system-prompt.js +14 -17
  22. package/dist/agent/tool-protocol.d.ts +1 -1
  23. package/dist/agent/tool-protocol.js +5 -3
  24. package/dist/agent/tool-registry.d.ts +9 -4
  25. package/dist/agent/tool-registry.js +27 -4
  26. package/dist/agent/tools/bash.d.ts +1 -1
  27. package/dist/agent/tools/bash.js +3 -2
  28. package/dist/agent/tools/edit-file.js +0 -1
  29. package/dist/agent/tools/glob.js +1 -1
  30. package/dist/agent/tools/grep.js +1 -1
  31. package/dist/agent/tools/pwsh.d.ts +1 -1
  32. package/dist/agent/tools/pwsh.js +1 -2
  33. package/dist/agent/tools/read-file.js +7 -4
  34. package/dist/agent/tools/write-file.js +0 -1
  35. package/dist/agent/types.d.ts +17 -2
  36. package/dist/cli/auth/cli.d.ts +1 -0
  37. package/dist/cli/auth/cli.js +216 -0
  38. package/dist/cli/auth/keys.d.ts +31 -0
  39. package/dist/cli/auth/keys.js +102 -0
  40. package/dist/{index.js → cli/index.js} +29 -32
  41. package/dist/{init.js → cli/init.js} +1 -1
  42. package/dist/{install.js → cli/install.js} +114 -5
  43. package/dist/cli/subcommands.d.ts +1 -0
  44. package/dist/cli/subcommands.js +17 -0
  45. package/dist/{event-bus.d.ts → core/event-bus.d.ts} +7 -13
  46. package/dist/{extension-loader.d.ts → core/extension-loader.d.ts} +1 -1
  47. package/dist/{extension-loader.js → core/extension-loader.js} +62 -70
  48. package/dist/{core.d.ts → core/index.d.ts} +18 -15
  49. package/dist/{core.js → core/index.js} +18 -92
  50. package/dist/{settings.d.ts → core/settings.d.ts} +7 -0
  51. package/dist/{settings.js → core/settings.js} +1 -0
  52. package/dist/core/types.d.ts +49 -0
  53. package/dist/core/types.js +1 -0
  54. package/dist/extensions/file-autocomplete.d.ts +1 -1
  55. package/dist/extensions/index.d.ts +7 -14
  56. package/dist/extensions/index.js +2 -19
  57. package/dist/extensions/slash-commands.d.ts +1 -1
  58. package/dist/extensions/slash-commands.js +7 -2
  59. package/dist/shell/host-types.d.ts +114 -0
  60. package/dist/shell/host-types.js +1 -0
  61. package/dist/shell/index.d.ts +8 -7
  62. package/dist/shell/index.js +58 -9
  63. package/dist/shell/input-handler.d.ts +7 -1
  64. package/dist/shell/input-handler.js +5 -2
  65. package/dist/shell/output-parser.d.ts +1 -1
  66. package/dist/{extensions → shell}/shell-context.d.ts +1 -1
  67. package/dist/{extensions → shell}/shell-context.js +18 -12
  68. package/dist/shell/shell.d.ts +6 -4
  69. package/dist/shell/shell.js +33 -109
  70. package/dist/shell/strategies/bash.d.ts +2 -0
  71. package/dist/shell/strategies/bash.js +68 -0
  72. package/dist/shell/strategies/fish.d.ts +2 -0
  73. package/dist/shell/strategies/fish.js +65 -0
  74. package/dist/shell/strategies/index.d.ts +13 -0
  75. package/dist/shell/strategies/index.js +17 -0
  76. package/dist/shell/strategies/types.d.ts +50 -0
  77. package/dist/shell/strategies/types.js +9 -0
  78. package/dist/shell/strategies/zsh.d.ts +2 -0
  79. package/dist/shell/strategies/zsh.js +72 -0
  80. package/dist/shell/tui-input-view.js +14 -3
  81. package/dist/{extensions → shell}/tui-renderer.d.ts +1 -1
  82. package/dist/{extensions → shell}/tui-renderer.js +27 -55
  83. package/dist/utils/box-frame.d.ts +4 -0
  84. package/dist/utils/box-frame.js +17 -6
  85. package/dist/utils/compositor.d.ts +1 -1
  86. package/dist/utils/compositor.js +2 -1
  87. package/dist/{executor.js → utils/executor.js} +1 -1
  88. package/dist/utils/floating-panel.d.ts +17 -5
  89. package/dist/utils/floating-panel.js +218 -70
  90. package/dist/utils/llm-facade.d.ts +7 -3
  91. package/dist/utils/stream-transform.d.ts +1 -1
  92. package/dist/utils/terminal-buffer.d.ts +1 -1
  93. package/dist/utils/tool-display.js +4 -0
  94. package/dist/utils/tool-interactive.d.ts +1 -1
  95. package/dist/utils/tty.d.ts +7 -0
  96. package/dist/utils/tty.js +15 -0
  97. package/examples/extensions/ash-acp-bridge/README.md +4 -1
  98. package/examples/extensions/ash-acp-bridge/src/index.ts +654 -0
  99. package/examples/extensions/ash-mcp-bridge/index.ts +1 -1
  100. package/examples/extensions/ashi/README.md +250 -0
  101. package/examples/extensions/ashi/package.json +60 -0
  102. package/examples/extensions/ashi/src/autocomplete.ts +91 -0
  103. package/examples/extensions/ashi/src/capture.ts +34 -0
  104. package/examples/extensions/ashi/src/cli.ts +126 -0
  105. package/examples/extensions/ashi/src/commands.ts +82 -0
  106. package/examples/extensions/ashi/src/compaction.ts +157 -0
  107. package/examples/extensions/ashi/src/components.ts +332 -0
  108. package/examples/extensions/ashi/src/default-renderers.ts +153 -0
  109. package/examples/extensions/ashi/src/display-config.ts +62 -0
  110. package/examples/extensions/ashi/src/frontend.ts +735 -0
  111. package/examples/extensions/ashi/src/hooks.ts +136 -0
  112. package/examples/extensions/ashi/src/multi-session-store.ts +146 -0
  113. package/examples/extensions/ashi/src/session-commands.ts +76 -0
  114. package/examples/extensions/ashi/src/session-store.ts +264 -0
  115. package/examples/extensions/ashi/src/status-footer.ts +66 -0
  116. package/examples/extensions/ashi/src/theme.ts +151 -0
  117. package/examples/extensions/ashi/tsconfig.json +14 -0
  118. package/examples/extensions/emacs-buffer.ts +364 -0
  119. package/examples/extensions/interactive-prompts.ts +114 -69
  120. package/examples/extensions/latex-images.ts +3 -3
  121. package/examples/extensions/opencode-bridge/index.ts +1 -1
  122. package/examples/extensions/overlay-agent.ts +35 -10
  123. package/examples/extensions/peer-mesh.ts +1 -1
  124. package/examples/extensions/pi-bridge/index.ts +0 -1
  125. package/examples/extensions/questionnaire.ts +2 -1
  126. package/examples/extensions/rtk-proxy.ts +3 -3
  127. package/examples/extensions/solarized-theme.ts +3 -3
  128. package/examples/extensions/subagents.ts +6 -6
  129. package/examples/extensions/terminal-buffer.ts +174 -33
  130. package/examples/extensions/tmux-pane.ts +6 -4
  131. package/examples/extensions/tunnel-vision.ts +405 -0
  132. package/examples/extensions/user-shell.ts +1 -1
  133. package/examples/extensions/web-access.ts +8 -113
  134. package/package.json +26 -22
  135. package/dist/extensions/agent-backend.d.ts +0 -14
  136. package/dist/extensions/agent-backend.js +0 -307
  137. package/dist/types.d.ts +0 -227
  138. /package/dist/{types.js → agent/host-types.js} +0 -0
  139. /package/dist/{extensions → agent}/providers/openai-compatible.js +0 -0
  140. /package/dist/{index.d.ts → cli/index.d.ts} +0 -0
  141. /package/dist/{init.d.ts → cli/init.d.ts} +0 -0
  142. /package/dist/{install.d.ts → cli/install.d.ts} +0 -0
  143. /package/dist/{event-bus.js → core/event-bus.js} +0 -0
  144. /package/dist/{executor.d.ts → utils/executor.d.ts} +0 -0
@@ -1,10 +1,9 @@
1
1
  /**
2
2
  * Interactive permission prompts extension.
3
3
  *
4
- * Adds permission gates for tool calls and file writes.
5
- * Without this extension, agent-sh runs in yolo mode (auto-approve).
6
- *
7
- * Uses the interactive UI primitive for compositor-aware, themed rendering.
4
+ * Gates the four built-in side-effect tools (bash, pwsh, write_file,
5
+ * edit_file) via tool advisors. Without this extension, agent-sh runs in
6
+ * yolo mode — tools execute without confirmation.
8
7
  *
9
8
  * Usage:
10
9
  * agent-sh -e ./examples/extensions/interactive-prompts.ts
@@ -12,19 +11,22 @@
12
11
  * # Or copy to ~/.agent-sh/extensions/ for permanent use:
13
12
  * cp examples/extensions/interactive-prompts.ts ~/.agent-sh/extensions/
14
13
  */
14
+ import * as fs from "node:fs/promises";
15
+ import * as path from "node:path";
15
16
  import { renderDiff } from "agent-sh/utils/diff-renderer.js";
16
17
  import { renderBoxFrame } from "agent-sh/utils/box-frame.js";
17
18
  import { palette as p } from "agent-sh/utils/palette.js";
18
- import type { ExtensionContext } from "agent-sh/types";
19
+ import { computeDiff, computeEditDiff, computeInputDiff, type DiffResult } from "agent-sh/utils/diff.js";
20
+ import type { AgentContext } from "agent-sh/types";
19
21
  import type { ToolUI } from "agent-sh/agent/types.js";
20
22
 
21
- export default function activate(ctx: ExtensionContext) {
23
+ const GATED_TOOLS = ["bash", "pwsh", "write_file", "edit_file"] as const;
24
+
25
+ export default function activate(ctx: AgentContext) {
22
26
  let autoApproveWrites = false;
23
27
 
24
- // Advise the TUI diff renderer to add permission prompt framing.
25
- // This replaces the default plain diff box with one that has a warning
26
- // border and key hints, so only one diff box is shown (not two).
27
- ctx.advise("tui:render-diff", (next, filePath: string, diff: any, width: number) => {
28
+ // Frame pre-execute diff previews as a permission prompt.
29
+ ctx.advise("tui:render-diff", (_next, filePath: string, diff: DiffResult, width: number) => {
28
30
  const boxW = Math.min(84, width);
29
31
  const contentW = boxW - 4;
30
32
  const MAX_DISPLAY = 25;
@@ -54,94 +56,137 @@ export default function activate(ctx: ExtensionContext) {
54
56
  });
55
57
  });
56
58
 
57
- const { bus } = ctx;
59
+ for (const name of GATED_TOOLS) {
60
+ ctx.agent.adviseTool(name, async (next, args, onChunk, toolCtx) => {
61
+ const ui = toolCtx?.ui;
62
+ if (!ui) return next(args, onChunk, toolCtx);
58
63
 
59
- bus.onPipeAsync("permission:request", async (payload) => {
60
- const ui = payload.ui as ToolUI | undefined;
61
- if (!ui) return payload;
64
+ const isFileWrite = name === "write_file" || name === "edit_file";
65
+ let diffPreRendered = false;
62
66
 
63
- switch (payload.kind) {
64
- case "tool-call":
65
- return handleToolCall(payload, ui);
66
- case "file-write": {
67
- if (autoApproveWrites) {
68
- return { ...payload, decision: { outcome: "approved" } };
69
- }
70
- const result = await handleFileWrite(payload, ui);
71
- if ((result.decision as any).autoApprove) {
72
- autoApproveWrites = true;
67
+ ctx.bus.emit("shell:stdout-show", {});
68
+ try {
69
+ if (isFileWrite) {
70
+ if (autoApproveWrites) {
71
+ // Skip prompt; tool's own post-execute diff renders as usual.
72
+ return next(args, onChunk, toolCtx);
73
+ }
74
+ await renderPreviewDiff(ctx, name, args);
75
+ diffPreRendered = true;
76
+
77
+ const answer = await promptWrite(ui);
78
+ if (answer === "reject") {
79
+ return { content: "Permission denied by user.", exitCode: 1, isError: true };
80
+ }
81
+ if (answer === "approve_all") autoApproveWrites = true;
82
+ } else {
83
+ const answer = await promptCommand(ui, name, args);
84
+ if (answer === "deny") {
85
+ return { content: "Permission denied by user.", exitCode: 1, isError: true };
86
+ }
73
87
  }
74
- return result;
88
+ } finally {
89
+ ctx.bus.emit("shell:stdout-hide", {});
75
90
  }
76
- default:
77
- return payload;
78
- }
79
- });
91
+
92
+ const result = await next(args, onChunk, toolCtx);
93
+ if (diffPreRendered && result.display?.body?.kind === "diff") {
94
+ // Strip the redundant post-execute diff body since we already showed it.
95
+ return { ...result, display: { ...result.display, body: undefined } };
96
+ }
97
+ return result;
98
+ });
99
+ }
80
100
  }
81
101
 
82
- async function handleToolCall(payload: any, ui: ToolUI) {
83
- const options = payload.metadata.options;
102
+ async function renderPreviewDiff(
103
+ ctx: AgentContext,
104
+ toolName: string,
105
+ args: Record<string, unknown>,
106
+ ): Promise<void> {
107
+ const rawPath = args.path;
108
+ if (typeof rawPath !== "string") return;
109
+ const absPath = path.resolve(process.cwd(), rawPath);
84
110
 
85
- const answer = await ui.custom<"approve" | "approve_all" | "deny">({
111
+ let diff: DiffResult | undefined;
112
+ if (toolName === "edit_file" && typeof args.old_text === "string" && typeof args.new_text === "string") {
113
+ const normalizedOld = (args.old_text as string).replace(/\r\n/g, "\n");
114
+ const normalizedNew = (args.new_text as string).replace(/\r\n/g, "\n");
115
+ try {
116
+ const oldFileContent = await fs.readFile(absPath, "utf-8");
117
+ diff = computeEditDiff(
118
+ oldFileContent, normalizedOld, normalizedNew,
119
+ args.replace_all === true,
120
+ );
121
+ } catch {
122
+ diff = computeInputDiff(normalizedOld, normalizedNew);
123
+ }
124
+ } else if (toolName === "write_file" && typeof args.content === "string") {
125
+ let oldContent: string | null = null;
126
+ try { oldContent = await fs.readFile(absPath, "utf-8"); } catch { /* new file */ }
127
+ diff = computeDiff(oldContent, args.content as string);
128
+ }
129
+ if (!diff || diff.isIdentical) return;
130
+
131
+ const cwd = process.cwd();
132
+ const home = process.env.HOME ?? "";
133
+ let displayPath = absPath;
134
+ if (absPath.startsWith(cwd + "/")) displayPath = absPath.slice(cwd.length + 1);
135
+ else if (home && absPath.startsWith(home + "/")) displayPath = "~/" + absPath.slice(home.length + 1);
136
+
137
+ ctx.call("tui:show-diff", displayPath, diff);
138
+ }
139
+
140
+ async function promptWrite(ui: ToolUI): Promise<"approve" | "approve_all" | "reject"> {
141
+ return ui.custom<"approve" | "approve_all" | "reject">({
86
142
  render(width) {
87
143
  const boxW = Math.min(84, width);
88
- return renderBoxFrame(
89
- [`${p.bold}⚠ ${payload.title}${p.reset}`],
90
- {
91
- width: boxW,
92
- style: "rounded",
93
- borderColor: p.warning,
94
- title: "Permission required",
95
- footer: [` ${p.dim}[y]es / [n]o / [a]llow all${p.reset}`],
96
- },
97
- );
144
+ return renderBoxFrame([], {
145
+ width: boxW,
146
+ style: "rounded",
147
+ borderColor: p.warning,
148
+ footer: [` ${p.bold}[y] Apply [n] Skip [a] Don't ask again${p.reset}`],
149
+ });
98
150
  },
99
151
  handleInput(data, done) {
100
152
  const ch = data.toLowerCase();
101
153
  if (ch === "y") done("approve");
102
154
  else if (ch === "a") done("approve_all");
103
- else if (ch === "n" || ch === "\x1b") done("deny");
155
+ else if (ch === "n" || ch === "\x1b") done("reject");
104
156
  },
105
157
  });
106
-
107
- if (answer === "approve" || answer === "approve_all") {
108
- const kind = answer === "approve_all" ? "allow_always" : "allow_once";
109
- const option = options?.find((o: any) => o.kind === kind)
110
- ?? options?.find((o: any) => o.kind === "allow_once" || o.kind === "allow_always");
111
- if (option) {
112
- return { ...payload, decision: { outcome: "selected", optionId: option.optionId } };
113
- }
114
- return { ...payload, decision: { outcome: "approved" } };
115
- }
116
- return { ...payload, decision: { outcome: "cancelled" } };
117
158
  }
118
159
 
119
- async function handleFileWrite(payload: any, ui: ToolUI) {
120
- const answer = await ui.custom<"approve" | "approve_all" | "reject">({
160
+ async function promptCommand(
161
+ ui: ToolUI,
162
+ toolName: string,
163
+ args: Record<string, unknown>,
164
+ ): Promise<"approve" | "deny"> {
165
+ const command = typeof args.command === "string" ? args.command : "";
166
+ const description = typeof args.description === "string" ? args.description : "";
167
+ const title = description ? `${toolName}: ${description}` : toolName;
168
+ const body = command
169
+ ? `${p.bold}${title}${p.reset}\n${p.dim}${truncate(command, 200)}${p.reset}`
170
+ : `${p.bold}${title}${p.reset}`;
171
+ return ui.custom<"approve" | "deny">({
121
172
  render(width) {
122
173
  const boxW = Math.min(84, width);
123
- // Just show the prompt actions — the diff itself was already rendered
124
- // by our advise on "tui:render-diff".
125
- return renderBoxFrame([], {
174
+ return renderBoxFrame(body.split("\n"), {
126
175
  width: boxW,
127
176
  style: "rounded",
128
177
  borderColor: p.warning,
129
- footer: [` ${p.bold}[y] Apply [n] Skip [a] Don't ask again${p.reset}`],
178
+ title: "Permission required",
179
+ footer: [` ${p.dim}[y]es / [n]o${p.reset}`],
130
180
  });
131
181
  },
132
182
  handleInput(data, done) {
133
183
  const ch = data.toLowerCase();
134
184
  if (ch === "y") done("approve");
135
- else if (ch === "a") done("approve_all");
136
- else if (ch === "n" || ch === "\x1b") done("reject");
185
+ else if (ch === "n" || ch === "\x1b") done("deny");
137
186
  },
138
187
  });
188
+ }
139
189
 
140
- if (answer === "approve") {
141
- return { ...payload, decision: { outcome: "approved" } };
142
- }
143
- if (answer === "approve_all") {
144
- return { ...payload, decision: { outcome: "approved", autoApprove: true } };
145
- }
146
- return { ...payload, decision: { outcome: "cancelled" } };
190
+ function truncate(s: string, max: number): string {
191
+ return s.length <= max ? s : s.slice(0, max - 1) + "";
147
192
  }
@@ -19,7 +19,7 @@ import { execSync } from "node:child_process";
19
19
  import * as fs from "node:fs";
20
20
  import * as os from "node:os";
21
21
  import * as path from "node:path";
22
- import type { ExtensionContext } from "agent-sh/types";
22
+ import type { ShellContext } from "agent-sh/types";
23
23
 
24
24
  // Settings loaded in activate() via ctx.getExtensionSettings
25
25
  let config = { dpi: 300, fgColor: "d4d4d4" };
@@ -98,7 +98,7 @@ function renderEquation(equation: string): Buffer | null {
98
98
 
99
99
  // ── Extension entry point ────────────────────────────────────────
100
100
 
101
- export default function activate(ctx: ExtensionContext) {
101
+ export default function activate(ctx: ShellContext) {
102
102
  const { bus } = ctx;
103
103
 
104
104
  // Load settings: ~/.agent-sh/settings.json → "latex-images": { dpi, fgColor }
@@ -116,7 +116,7 @@ export default function activate(ctx: ExtensionContext) {
116
116
  }
117
117
 
118
118
  // Handle inline $$...$$ display math
119
- ctx.createBlockTransform({
119
+ ctx.shell.createBlockTransform({
120
120
  open: "$$",
121
121
  close: "$$",
122
122
  transform(latex) {
@@ -60,7 +60,7 @@ function parseUnifiedDiff(patch: string): DiffResult | null {
60
60
  }
61
61
 
62
62
  export default function activate(ctx: ExtensionContext): void {
63
- const { bus, call, compositor } = ctx;
63
+ const { bus, call } = ctx; const { compositor } = ctx.shell;
64
64
 
65
65
  const cwd = (): string => {
66
66
  const v = call("cwd");
@@ -20,7 +20,7 @@
20
20
  * - terminal-buffer.ts → terminal_read / terminal_keys tools
21
21
  * - user-shell.ts → user_shell tool (run new shell commands)
22
22
  */
23
- import type { ExtensionContext, RemoteSession } from "agent-sh/types";
23
+ import type { AgentContext, ShellContext, RemoteSession } from "agent-sh/types";
24
24
  import type { RenderSurface } from "agent-sh/utils/compositor";
25
25
  import { FloatingPanel } from "agent-sh/utils/floating-panel";
26
26
  import { formatScreenContext, type TerminalBuffer } from "agent-sh/utils/terminal-buffer";
@@ -66,8 +66,10 @@ function createPanelSurface(panel: FloatingPanel): RenderSurface {
66
66
  };
67
67
  }
68
68
 
69
- export default function activate(ctx: ExtensionContext): void {
70
- const { bus, registerInstruction, createRemoteSession } = ctx;
69
+ export default function activate(ctx: AgentContext & ShellContext): void {
70
+ const { bus } = ctx;
71
+ const { registerInstruction } = ctx.agent;
72
+ const { createRemoteSession } = ctx.shell;
71
73
  const terminalBuffer: TerminalBuffer | null = ctx.call("terminal-buffer");
72
74
 
73
75
  const panel = new FloatingPanel(bus, {
@@ -79,13 +81,13 @@ export default function activate(ctx: ExtensionContext): void {
79
81
  const panelSurface = createPanelSurface(panel);
80
82
  let session: RemoteSession | null = null;
81
83
 
82
- ctx.registerContextProducer("interactive-session", () =>
84
+ ctx.agent.registerContextProducer("interactive-session", () =>
83
85
  session?.active ? "interactive-session: true" : null,
84
86
  );
85
87
 
86
88
  // Inject the live screen for TUI / REPL programs. At a plain shell prompt
87
89
  // `<shell_events>` already covers the visible scrollback — skip to dedupe.
88
- ctx.registerContextProducer("terminal-screen", () => {
90
+ ctx.agent.registerContextProducer("terminal-screen", () => {
89
91
  if (!session?.active || !terminalBuffer?.altScreen) return null;
90
92
  return formatScreenContext(terminalBuffer.readScreen(), 80);
91
93
  });
@@ -104,8 +106,18 @@ export default function activate(ctx: ExtensionContext): void {
104
106
  surface: panelSurface,
105
107
  });
106
108
  }
107
- panel.setActive();
108
- session.submit(query);
109
+ if (query.startsWith("/")) {
110
+ // Sync commands (/model, /help) render via ui:info and leave us in
111
+ // input phase; ones that fan out to agent:submit flip the phase via
112
+ // the agent:processing-start listener below.
113
+ const spaceIdx = query.indexOf(" ");
114
+ const name = spaceIdx === -1 ? query : query.slice(0, spaceIdx);
115
+ const args = spaceIdx === -1 ? "" : query.slice(spaceIdx + 1).trim();
116
+ bus.emit("command:execute", { name, args });
117
+ } else {
118
+ panel.setActive();
119
+ session.submit(query);
120
+ }
109
121
  });
110
122
 
111
123
  panel.handlers.advise("panel:show", (_next) => {
@@ -114,9 +126,9 @@ export default function activate(ctx: ExtensionContext): void {
114
126
  }
115
127
  });
116
128
 
117
- // Keep the session alive while the agent is still working, even after
118
- // dismiss so output keeps buffering and tools keep executing.
119
- panel.handlers.advise("panel:dismiss", (next) => {
129
+ // While the agent is still working, keep the session open so output and
130
+ // tool calls survive a hide. Once it's idle, close to release redirects.
131
+ panel.handlers.advise("panel:hide", (next) => {
120
132
  next();
121
133
  if (session && !panel.processing) {
122
134
  session.close();
@@ -124,6 +136,19 @@ export default function activate(ctx: ExtensionContext): void {
124
136
  }
125
137
  });
126
138
 
139
+ panel.handlers.advise("panel:reset", (next) => {
140
+ next();
141
+ if (session) {
142
+ session.close();
143
+ session = null;
144
+ }
145
+ });
146
+
147
+ // Picks up turns triggered indirectly (e.g. /skill:foo → agent:submit).
148
+ bus.on("agent:processing-start", () => {
149
+ if (panel.active && !panel.processing) panel.setActive();
150
+ });
151
+
127
152
  bus.on("agent:processing-done", () => {
128
153
  if (panel.active) panel.setDone();
129
154
  });
@@ -190,7 +190,7 @@ class PeerServer {
190
190
  }
191
191
 
192
192
  export default function activate(ctx: ExtensionContext): void {
193
- const { bus, registerCommand, registerTool, registerInstruction, define } = ctx;
193
+ const { bus, registerCommand, define } = ctx; const { registerTool, registerInstruction } = ctx.agent;
194
194
  const getCwd = () => ctx.call("cwd") as string;
195
195
  const startTime = Date.now();
196
196
 
@@ -391,7 +391,6 @@ export default function activate(ctx: ExtensionContext): void {
391
391
  return;
392
392
  }
393
393
  session.setThinkingLevel(level);
394
- bus.emit("ui:info", { message: `Thinking: ${level}` });
395
394
  bus.emit("config:changed", {});
396
395
  };
397
396
 
@@ -55,7 +55,8 @@ interface QuestionnaireResult {
55
55
 
56
56
  // ── Extension ────────────────────────────────────────────────────
57
57
 
58
- export default function activate({ registerTool, registerInstruction }: ExtensionContext) {
58
+ export default function activate(ctx: ExtensionContext) {
59
+ const { registerTool, registerInstruction } = ctx.agent;
59
60
  registerInstruction("questionnaire", [
60
61
  "# When to use the questionnaire tool",
61
62
  "ALWAYS use the `questionnaire` tool instead of asking the user a question in plain text when:",
@@ -20,7 +20,7 @@
20
20
  * cp examples/extensions/rtk-proxy.ts ~/.agent-sh/extensions/
21
21
  */
22
22
  import { execSync } from "node:child_process";
23
- import type { ExtensionContext } from "agent-sh/types";
23
+ import type { AgentContext } from "agent-sh/types";
24
24
 
25
25
  const DEFAULT_PREFIXES = new Set([
26
26
  "git", "gh",
@@ -73,7 +73,7 @@ function rewriteForRtk(cmd: string, prefixes: Set<string>, flag: string): string
73
73
  return `RTK_TELEMETRY_DISABLED=1 rtk ${flag}${cmd}`;
74
74
  }
75
75
 
76
- export default function activate(ctx: ExtensionContext) {
76
+ export default function activate(ctx: AgentContext) {
77
77
  const config = ctx.getExtensionSettings("rtk-proxy", {
78
78
  enabled: true,
79
79
  ultraCompact: false,
@@ -95,7 +95,7 @@ export default function activate(ctx: ExtensionContext) {
95
95
  for (const p of config.excludePrefixes) prefixes.delete(p);
96
96
  const flag = config.ultraCompact ? "--ultra-compact " : "";
97
97
 
98
- ctx.registerInstruction("rtk-proxy",
98
+ ctx.agent.registerInstruction("rtk-proxy",
99
99
  "The rtk-proxy extension transparently rewrites bash commands like " +
100
100
  "`git status`, `cargo test`, `pytest` to their rtk-compressed equivalents " +
101
101
  "before execution. Output will be condensed (errors/failures first, " +
@@ -9,10 +9,10 @@
9
9
  * # Or copy to ~/.agent-sh/extensions/ for permanent use:
10
10
  * cp examples/extensions/solarized-theme.ts ~/.agent-sh/extensions/
11
11
  */
12
- import type { ExtensionContext } from "agent-sh/types";
12
+ import type { ShellContext } from "agent-sh/types";
13
13
 
14
- export default function activate({ setPalette }: ExtensionContext) {
15
- setPalette({
14
+ export default function activate(ctx: ShellContext) {
15
+ ctx.shell.setPalette({
16
16
  accent: "\x1b[38;2;38;139;210m", // blue (#268bd2)
17
17
  success: "\x1b[38;2;133;153;0m", // green (#859900)
18
18
  warning: "\x1b[38;2;181;137;0m", // yellow (#b58900)
@@ -8,22 +8,22 @@
8
8
  * Usage:
9
9
  * agent-sh -e ./examples/extensions/subagents.ts
10
10
  */
11
- import type { ExtensionContext } from "agent-sh/types";
11
+ import type { AgentContext } from "agent-sh/types";
12
12
  import { runSubagent } from "agent-sh/agent/subagent";
13
13
 
14
- export default function activate(ctx: ExtensionContext): void {
14
+ export default function activate(ctx: AgentContext): void {
15
15
  const { bus, llmClient } = ctx;
16
16
  if (!llmClient) return;
17
17
 
18
- const allToolNames = () => ctx.getTools().map(t => t.name);
18
+ const allToolNames = () => ctx.agent.getTools().map(t => t.name);
19
19
 
20
- ctx.registerInstruction("subagent-guide", [
20
+ ctx.agent.registerInstruction("subagent-guide", [
21
21
  "You have a spawn_agent tool for delegating work to a subagent with its own context.",
22
22
  "The subagent inherits your session history, so write a short directive (what to do), not a briefing (what happened).",
23
23
  "Use it for tasks that need multiple tool calls you don't need to see — research, exploration, independent implementation.",
24
24
  ].join("\n"));
25
25
 
26
- ctx.registerTool({
26
+ ctx.agent.registerTool({
27
27
  name: "spawn_agent",
28
28
  description:
29
29
  "Spawn a subagent with its own fresh context to handle a focused task. " +
@@ -63,7 +63,7 @@ export default function activate(ctx: ExtensionContext): void {
63
63
  const task = args.task as string;
64
64
  const toolNames = args.tools as string[] | undefined;
65
65
 
66
- const allTools = ctx.getTools();
66
+ const allTools = ctx.agent.getTools();
67
67
  // Filter to requested tools, or give all tools (minus spawn_agent to prevent recursion)
68
68
  const tools = toolNames
69
69
  ? allTools.filter(t => toolNames.includes(t.name))