agent-sh 0.12.27 → 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 +42 -98
  4. package/dist/agent/conversation-state.d.ts +9 -0
  5. package/dist/agent/conversation-state.js +16 -0
  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} +31 -2
  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 +1 -1
  89. package/dist/utils/floating-panel.js +9 -4
  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 +1 -1
  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 +7 -5
  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 +1 -1
  130. package/examples/extensions/tmux-pane.ts +6 -4
  131. package/examples/extensions/tunnel-vision.ts +5 -5
  132. package/examples/extensions/user-shell.ts +1 -1
  133. package/examples/extensions/web-access.ts +5 -5
  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
@@ -0,0 +1,151 @@
1
+ import chalk from "chalk";
2
+ import { highlight, supportsLanguage } from "cli-highlight";
3
+ import type { EditorTheme, MarkdownTheme, SelectListTheme } from "@earendil-works/pi-tui";
4
+
5
+ /** Bundled dark palette, lifted from pi-coding-agent's dark.json. */
6
+ const VARS: Record<string, string> = {
7
+ cyan: "#00d7ff",
8
+ blue: "#5f87ff",
9
+ green: "#b5bd68",
10
+ red: "#cc6666",
11
+ yellow: "#ffff00",
12
+ gray: "#808080",
13
+ dimGray: "#666666",
14
+ darkGray: "#505050",
15
+ accent: "#8abeb7",
16
+ selectedBg: "#3a3a4a",
17
+ userMsgBg: "#343541",
18
+ toolPendingBg: "#282832",
19
+ toolSuccessBg: "#283228",
20
+ toolErrorBg: "#3c2828",
21
+ };
22
+
23
+ const RAW = {
24
+ accent: "accent",
25
+ border: "blue",
26
+ borderAccent: "cyan",
27
+ borderMuted: "darkGray",
28
+ success: "green",
29
+ error: "red",
30
+ warning: "yellow",
31
+ muted: "gray",
32
+ dim: "dimGray",
33
+ text: "",
34
+ thinkingText: "gray",
35
+ selectedBg: "selectedBg",
36
+ userMessageBg: "userMsgBg",
37
+ userMessageText: "",
38
+ toolPendingBg: "toolPendingBg",
39
+ toolSuccessBg: "toolSuccessBg",
40
+ toolErrorBg: "toolErrorBg",
41
+ toolTitle: "",
42
+ toolOutput: "gray",
43
+ mdHeading: "#f0c674",
44
+ mdLink: "#81a2be",
45
+ mdLinkUrl: "dimGray",
46
+ mdCode: "accent",
47
+ mdCodeBlock: "green",
48
+ mdCodeBlockBorder: "gray",
49
+ mdQuote: "gray",
50
+ mdQuoteBorder: "gray",
51
+ mdHr: "gray",
52
+ mdListBullet: "accent",
53
+ toolDiffAdded: "green",
54
+ toolDiffRemoved: "red",
55
+ toolDiffContext: "gray",
56
+ bashMode: "green",
57
+ } as const;
58
+
59
+ export type ThemeColor = keyof typeof RAW;
60
+
61
+ function resolve(v: string): string {
62
+ if (v === "") return "";
63
+ if (v.startsWith("#")) return v;
64
+ const hex = VARS[v];
65
+ if (!hex) throw new Error(`Unknown theme var: ${v}`);
66
+ return hex;
67
+ }
68
+
69
+ function hexToRgb(hex: string): [number, number, number] {
70
+ const c = hex.replace("#", "");
71
+ return [parseInt(c.slice(0, 2), 16), parseInt(c.slice(2, 4), 16), parseInt(c.slice(4, 6), 16)];
72
+ }
73
+
74
+ function fgAnsi(hex: string): string {
75
+ if (hex === "") return "\x1b[39m";
76
+ const [r, g, b] = hexToRgb(hex);
77
+ return `\x1b[38;2;${r};${g};${b}m`;
78
+ }
79
+
80
+ function bgAnsi(hex: string): string {
81
+ if (hex === "") return "\x1b[49m";
82
+ const [r, g, b] = hexToRgb(hex);
83
+ return `\x1b[48;2;${r};${g};${b}m`;
84
+ }
85
+
86
+ class Theme {
87
+ private fgCodes = new Map<ThemeColor, string>();
88
+ private bgCodes = new Map<ThemeColor, string>();
89
+ constructor() {
90
+ for (const k of Object.keys(RAW) as ThemeColor[]) {
91
+ const hex = resolve(RAW[k]);
92
+ this.fgCodes.set(k, fgAnsi(hex));
93
+ this.bgCodes.set(k, bgAnsi(hex));
94
+ }
95
+ }
96
+ fg(color: ThemeColor, text: string): string { return `${this.fgCodes.get(color)}${text}\x1b[39m`; }
97
+ bg(color: ThemeColor, text: string): string { return `${this.bgCodes.get(color)}${text}\x1b[49m`; }
98
+ bgCode(color: ThemeColor): string { return this.bgCodes.get(color) ?? ""; }
99
+ bold(text: string): string { return chalk.bold(text); }
100
+ italic(text: string): string { return chalk.italic(text); }
101
+ underline(text: string): string { return chalk.underline(text); }
102
+ strikethrough(text: string): string { return chalk.strikethrough(text); }
103
+ }
104
+
105
+ export const theme = new Theme();
106
+
107
+ export function markdownTheme(): MarkdownTheme {
108
+ return {
109
+ heading: (t) => theme.fg("mdHeading", t),
110
+ link: (t) => theme.fg("mdLink", t),
111
+ linkUrl: (t) => theme.fg("mdLinkUrl", t),
112
+ code: (t) => theme.fg("mdCode", t),
113
+ codeBlock: (t) => theme.fg("mdCodeBlock", t),
114
+ codeBlockBorder: (t) => theme.fg("mdCodeBlockBorder", t),
115
+ quote: (t) => theme.fg("mdQuote", t),
116
+ quoteBorder: (t) => theme.fg("mdQuoteBorder", t),
117
+ hr: (t) => theme.fg("mdHr", t),
118
+ listBullet: (t) => theme.fg("mdListBullet", t),
119
+ bold: (t) => theme.bold(t),
120
+ italic: (t) => theme.italic(t),
121
+ underline: (t) => theme.underline(t),
122
+ strikethrough: (t) => theme.strikethrough(t),
123
+ highlightCode: (src: string, lang?: string): string[] => {
124
+ const validLang = lang && supportsLanguage(lang) ? lang : undefined;
125
+ if (!validLang) return src.split("\n").map((l) => theme.fg("mdCodeBlock", l));
126
+ try {
127
+ return highlight(src, { language: validLang, ignoreIllegals: true }).split("\n");
128
+ } catch {
129
+ return src.split("\n").map((l) => theme.fg("mdCodeBlock", l));
130
+ }
131
+ },
132
+ };
133
+ }
134
+
135
+ export function selectListTheme(): SelectListTheme {
136
+ return {
137
+ selectedPrefix: (t) => theme.fg("accent", t),
138
+ selectedText: (t) => theme.fg("accent", t),
139
+ description: (t) => theme.fg("muted", t),
140
+ scrollInfo: (t) => theme.fg("muted", t),
141
+ noMatch: (t) => theme.fg("muted", t),
142
+ };
143
+ }
144
+
145
+ export function editorTheme(): EditorTheme {
146
+ return {
147
+ borderColor: (t) => theme.fg("borderMuted", t),
148
+ selectList: selectListTheme(),
149
+ };
150
+ }
151
+
@@ -0,0 +1,14 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "outDir": "dist",
7
+ "rootDir": "src",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "declaration": true,
11
+ "skipLibCheck": true
12
+ },
13
+ "include": ["src"]
14
+ }
@@ -187,7 +187,7 @@ function renderSnapshot(snap: EmacsSnapshot): string {
187
187
  }
188
188
 
189
189
  export default function activate(ctx: ExtensionContext): void {
190
- const { registerTool } = ctx;
190
+ const { registerTool } = ctx.agent;
191
191
  if (!emacsclientAvailable()) return;
192
192
 
193
193
  registerTool({
@@ -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
  });
@@ -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))
@@ -138,7 +138,7 @@ function diffScreens(before: string, after: string): string {
138
138
  }
139
139
 
140
140
  export default function activate(ctx: ExtensionContext): void {
141
- const { bus, registerTool } = ctx;
141
+ const { bus } = ctx; const { registerTool } = ctx.agent;
142
142
  const tb = ctx.call("terminal-buffer");
143
143
  if (!tb) return; // @xterm/headless not installed, or shell frontend not loaded
144
144
 
@@ -22,7 +22,7 @@ import * as net from "node:net";
22
22
  import * as os from "node:os";
23
23
  import * as path from "node:path";
24
24
  import { execSync, spawn } from "node:child_process";
25
- import type { ExtensionContext, RenderSurface, RemoteSession } from "agent-sh/types";
25
+ import type { AgentContext, ShellContext, RenderSurface, RemoteSession } from "agent-sh/types";
26
26
 
27
27
  // ── Helpers ─────────────────────────────────────────────────────
28
28
 
@@ -142,15 +142,17 @@ interface PaneState {
142
142
 
143
143
  // ── Extension ───────────────────────────────────────────────────
144
144
 
145
- export default function activate(ctx: ExtensionContext): void {
146
- const { bus, registerCommand, registerInstruction, createRemoteSession } = ctx;
145
+ export default function activate(ctx: AgentContext & ShellContext): void {
146
+ const { bus, registerCommand } = ctx;
147
+ const { registerInstruction } = ctx.agent;
148
+ const { createRemoteSession } = ctx.shell;
147
149
 
148
150
  if (!inTmux()) return;
149
151
 
150
152
  let state: PaneState | null = null;
151
153
 
152
154
  // Tell the LLM it's running inside an interactive pane session.
153
- ctx.registerContextProducer("interactive-session", () =>
155
+ ctx.agent.registerContextProducer("interactive-session", () =>
154
156
  state?.mode === "rsplit" ? "interactive-session: true" : null,
155
157
  );
156
158