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
@@ -0,0 +1,260 @@
1
+ /**
2
+ * Questionnaire tool — the agent can ask the user one or more questions.
3
+ *
4
+ * Single question: simple option list with arrow key navigation.
5
+ * Multiple questions: tab bar navigation between questions.
6
+ *
7
+ * Usage:
8
+ * agent-sh -e ./examples/extensions/questionnaire.ts
9
+ */
10
+ import type { ExtensionContext } from "agent-sh/types";
11
+ import type { InteractiveSession, ToolExecutionContext } from "agent-sh/agent/types.js";
12
+ import { palette as p } from "agent-sh/utils/palette.js";
13
+
14
+ // ── Key matching ─────────────────────────────────────────────────
15
+
16
+ function isKey(data: string, key: string): boolean {
17
+ switch (key) {
18
+ case "up": return data === "\x1b[A" || data === "\x1bOA";
19
+ case "down": return data === "\x1b[B" || data === "\x1bOB";
20
+ case "left": return data === "\x1b[D" || data === "\x1bOD";
21
+ case "right": return data === "\x1b[C" || data === "\x1bOC";
22
+ case "enter": return data === "\r" || data === "\n";
23
+ case "escape": return data === "\x1b";
24
+ case "tab": return data === "\t";
25
+ default: return data === key;
26
+ }
27
+ }
28
+
29
+ // ── Types ────────────────────────────────────────────────────────
30
+
31
+ interface QuestionOption {
32
+ value: string;
33
+ label: string;
34
+ description?: string;
35
+ }
36
+
37
+ interface Question {
38
+ id: string;
39
+ label: string;
40
+ prompt: string;
41
+ options: QuestionOption[];
42
+ }
43
+
44
+ interface Answer {
45
+ id: string;
46
+ value: string;
47
+ label: string;
48
+ index: number;
49
+ }
50
+
51
+ interface QuestionnaireResult {
52
+ answers: Answer[];
53
+ cancelled: boolean;
54
+ }
55
+
56
+ // ── Extension ────────────────────────────────────────────────────
57
+
58
+ export default function activate({ registerTool, registerInstruction }: ExtensionContext) {
59
+ registerInstruction("questionnaire", [
60
+ "# When to use the questionnaire tool",
61
+ "ALWAYS use the `questionnaire` tool instead of asking the user a question in plain text when:",
62
+ "- You need the user to choose from specific options (e.g. frameworks, approaches, yes/no decisions)",
63
+ "- You are unsure about a preference and can enumerate reasonable choices",
64
+ "- The user's answer determines a significant branch in your behavior",
65
+ "",
66
+ "Do NOT just list options in your reply prose — call the questionnaire tool so the user can select interactively.",
67
+ "This applies to single questions too (yes/no, pick-one).",
68
+ ].join("\n"));
69
+
70
+ registerTool({
71
+ name: "questionnaire",
72
+ displayName: "questionnaire",
73
+ description:
74
+ "Present the user with one or more questions with selectable options and wait for their interactive response. " +
75
+ "PREFER THIS over asking questions in plain text — it gives the user an interactive picker (arrow keys + enter). " +
76
+ "Use for: clarifying requirements, getting preferences, confirming decisions, choosing between options. " +
77
+ "Single question → simple option list. Multiple questions → tab-based multi-page interface.",
78
+ input_schema: {
79
+ type: "object",
80
+ properties: {
81
+ questions: {
82
+ type: "array",
83
+ description: "Questions to ask the user",
84
+ items: {
85
+ type: "object",
86
+ properties: {
87
+ id: { type: "string", description: "Unique identifier" },
88
+ label: { type: "string", description: "Short label for tab bar (defaults to Q1, Q2)" },
89
+ prompt: { type: "string", description: "The full question text" },
90
+ options: {
91
+ type: "array",
92
+ items: {
93
+ type: "object",
94
+ properties: {
95
+ value: { type: "string", description: "Value returned when selected" },
96
+ label: { type: "string", description: "Display label" },
97
+ description: { type: "string", description: "Optional description" },
98
+ },
99
+ required: ["value", "label"],
100
+ },
101
+ },
102
+ },
103
+ required: ["id", "prompt", "options"],
104
+ },
105
+ },
106
+ },
107
+ required: ["questions"],
108
+ },
109
+
110
+ async execute(args, _onChunk, ctx?: ToolExecutionContext) {
111
+ if (!ctx?.ui) {
112
+ return { content: "Error: interactive UI not available", exitCode: 1, isError: true };
113
+ }
114
+
115
+ const rawQuestions = args.questions as any[];
116
+ if (!rawQuestions?.length) {
117
+ return { content: "Error: no questions provided", exitCode: 1, isError: true };
118
+ }
119
+
120
+ const questions: Question[] = rawQuestions.map((q, i) => ({
121
+ ...q,
122
+ label: q.label || `Q${i + 1}`,
123
+ }));
124
+
125
+ const result = await ctx.ui.custom<QuestionnaireResult>(
126
+ createSession(questions),
127
+ );
128
+
129
+ if (result.cancelled) {
130
+ return { content: "User cancelled the questionnaire.", exitCode: 1, isError: false };
131
+ }
132
+
133
+ const lines = result.answers.map((a) => {
134
+ const q = questions.find((q) => q.id === a.id);
135
+ return `${q?.label ?? a.id}: ${a.index + 1}. ${a.label}`;
136
+ });
137
+
138
+ return { content: lines.join("\n"), exitCode: 0, isError: false };
139
+ },
140
+
141
+ getDisplayInfo: () => ({ kind: "execute" as const, icon: "?" }),
142
+
143
+ formatCall(args) {
144
+ const qs = (args.questions as any[]) ?? [];
145
+ const labels = qs.map((q: any) => q.label || q.id).join(", ");
146
+ return `${qs.length} question${qs.length !== 1 ? "s" : ""}${labels ? ` (${labels})` : ""}`;
147
+ },
148
+ });
149
+ }
150
+
151
+ // ── Interactive session ──────────────────────────────────────────
152
+
153
+ function createSession(questions: Question[]): InteractiveSession<QuestionnaireResult> {
154
+ const isMulti = questions.length > 1;
155
+ let tab = 0;
156
+ let optionIdx = 0;
157
+ const answers = new Map<string, Answer>();
158
+
159
+ return {
160
+ render(width) {
161
+ const w = Math.min(80, width);
162
+ const lines: string[] = [];
163
+ const q = questions[tab];
164
+
165
+ lines.push(`${p.muted}${"─".repeat(w)}${p.reset}`);
166
+
167
+ // Tab bar for multi-question
168
+ if (isMulti) {
169
+ const tabs: string[] = [];
170
+ for (let i = 0; i < questions.length; i++) {
171
+ const answered = answers.has(questions[i].id);
172
+ const active = i === tab;
173
+ const box = answered ? "■" : "□";
174
+ const label = ` ${box} ${questions[i].label} `;
175
+ tabs.push(active
176
+ ? `${p.accent}${p.bold}${label}${p.reset}`
177
+ : `${p.muted}${label}${p.reset}`);
178
+ }
179
+ lines.push(` ${tabs.join(" ")}`);
180
+ lines.push("");
181
+ }
182
+
183
+ // Question + options
184
+ if (q) {
185
+ lines.push(` ${q.prompt}`);
186
+ lines.push("");
187
+ for (let i = 0; i < q.options.length; i++) {
188
+ const opt = q.options[i];
189
+ const sel = i === optionIdx;
190
+ const prefix = sel ? `${p.accent}> ${p.reset}` : " ";
191
+ lines.push(`${prefix}${sel ? p.accent : ""}${i + 1}. ${opt.label}${sel ? p.reset : ""}`);
192
+ if (opt.description) {
193
+ lines.push(` ${p.muted}${opt.description}${p.reset}`);
194
+ }
195
+ }
196
+ }
197
+
198
+ lines.push("");
199
+ lines.push(isMulti
200
+ ? ` ${p.dim}Tab/←→ navigate • ↑↓ select • Enter confirm • Esc cancel${p.reset}`
201
+ : ` ${p.dim}↑↓ navigate • Enter select • Esc cancel${p.reset}`);
202
+ lines.push(`${p.muted}${"─".repeat(w)}${p.reset}`);
203
+
204
+ return lines;
205
+ },
206
+
207
+ handleInput(data, done) {
208
+ const q = questions[tab];
209
+
210
+ if (isKey(data, "escape")) {
211
+ done({ answers: [], cancelled: true });
212
+ return;
213
+ }
214
+
215
+ // Tab navigation
216
+ if (isMulti) {
217
+ if (isKey(data, "tab") || isKey(data, "right")) {
218
+ tab = (tab + 1) % questions.length;
219
+ optionIdx = 0;
220
+ return;
221
+ }
222
+ if (isKey(data, "left")) {
223
+ tab = (tab - 1 + questions.length) % questions.length;
224
+ optionIdx = 0;
225
+ return;
226
+ }
227
+ }
228
+
229
+ if (!q) return;
230
+
231
+ if (isKey(data, "up")) {
232
+ optionIdx = Math.max(0, optionIdx - 1);
233
+ return;
234
+ }
235
+ if (isKey(data, "down")) {
236
+ optionIdx = Math.min(q.options.length - 1, optionIdx + 1);
237
+ return;
238
+ }
239
+
240
+ if (isKey(data, "enter")) {
241
+ const opt = q.options[optionIdx];
242
+ answers.set(q.id, { id: q.id, value: opt.value, label: opt.label, index: optionIdx });
243
+
244
+ if (!isMulti) {
245
+ done({ answers: Array.from(answers.values()), cancelled: false });
246
+ return;
247
+ }
248
+
249
+ // Advance to next unanswered or finish
250
+ const unanswered = questions.findIndex((q) => !answers.has(q.id));
251
+ if (unanswered === -1) {
252
+ done({ answers: Array.from(answers.values()), cancelled: false });
253
+ } else {
254
+ tab = unanswered;
255
+ optionIdx = 0;
256
+ }
257
+ }
258
+ },
259
+ };
260
+ }
@@ -8,8 +8,8 @@
8
8
  * Usage:
9
9
  * agent-sh -e ./examples/extensions/subagents.ts
10
10
  */
11
- import type { ExtensionContext } from "../../src/types.js";
12
- import { runSubagent } from "../../src/agent/subagent.js";
11
+ import type { ExtensionContext } from "agent-sh/types";
12
+ import { runSubagent } from "agent-sh/agent/subagent";
13
13
 
14
14
  export default function activate(ctx: ExtensionContext): void {
15
15
  const { bus, llmClient, contextManager } = ctx;
@@ -17,6 +17,12 @@ export default function activate(ctx: ExtensionContext): void {
17
17
 
18
18
  const allToolNames = () => ctx.getTools().map(t => t.name);
19
19
 
20
+ ctx.registerInstruction("subagent-guide", [
21
+ "You have a spawn_agent tool for delegating work to a subagent with its own context.",
22
+ "The subagent inherits your session history, so write a short directive (what to do), not a briefing (what happened).",
23
+ "Use it for tasks that need multiple tool calls you don't need to see — research, exploration, independent implementation.",
24
+ ].join("\n"));
25
+
20
26
  ctx.registerTool({
21
27
  name: "spawn_agent",
22
28
  description:
@@ -44,8 +50,15 @@ export default function activate(ctx: ExtensionContext): void {
44
50
 
45
51
  getDisplayInfo: () => ({
46
52
  kind: "execute",
53
+ icon: "⤵",
47
54
  }),
48
55
 
56
+ formatCall: (args) => {
57
+ const task = String(args.task ?? "");
58
+ const max = 80;
59
+ return task.length > max ? task.slice(0, max - 1) + "…" : task;
60
+ },
61
+
49
62
  async execute(args) {
50
63
  const task = args.task as string;
51
64
  const toolNames = args.tools as string[] | undefined;
@@ -56,9 +69,12 @@ export default function activate(ctx: ExtensionContext): void {
56
69
  ? allTools.filter(t => toolNames.includes(t.name))
57
70
  : allTools.filter(t => t.name !== "spawn_agent");
58
71
 
72
+ const nuclearSummary = bus.emitPipe("agent:get-nuclear-summary", { summary: null }).summary;
73
+
59
74
  const systemPrompt =
60
75
  `You are a focused subagent. Complete the task and return a clear, concise result.\n` +
61
- `Working directory: ${contextManager.getCwd()}`;
76
+ `Working directory: ${contextManager.getCwd()}` +
77
+ (nuclearSummary ? `\n\n[Parent session history]\n${nuclearSummary}` : "");
62
78
 
63
79
  try {
64
80
  const result = await runSubagent({
@@ -66,7 +82,6 @@ export default function activate(ctx: ExtensionContext): void {
66
82
  tools,
67
83
  systemPrompt,
68
84
  task,
69
- bus,
70
85
  maxIterations: 25,
71
86
  });
72
87
 
@@ -1,11 +1,6 @@
1
1
  /**
2
2
  * Terminal buffer extension.
3
3
  *
4
- * Maintains a headless xterm.js terminal fed from raw PTY data.
5
- * Provides an accurate, clean-text snapshot of the terminal screen
6
- * that the agent can use for context — handling ANSI codes, cursor
7
- * movement, alternate screen (vim/htop), and line wrapping correctly.
8
- *
9
4
  * Registers two agent tools:
10
5
  * - terminal_read: get the current screen contents + cursor position
11
6
  * - terminal_keys: send raw keystrokes into the user's live PTY
@@ -13,22 +8,15 @@
13
8
  * Together these let the agent operate inside interactive programs
14
9
  * (vim, htop, less, etc.) by reading the screen and typing keys.
15
10
  *
16
- * Requires: npm install @xterm/headless@5.5.0 @xterm/addon-serialize@0.13.0
17
- *
18
- * Usage:
19
- * agent-sh -e ./examples/extensions/terminal-buffer.ts
11
+ * Requires xterm in the extension directory:
12
+ * npm install @xterm/headless@5.5.0 @xterm/addon-serialize@0.13.0
20
13
  *
21
- * # Or copy to ~/.agent-sh/extensions/ for permanent use:
22
- * cp examples/extensions/terminal-buffer.ts ~/.agent-sh/extensions/
14
+ * Core already loads xterm lazily (for floating-panel compositing), so
15
+ * installing these deps anywhere on the NODE_PATH is enough.
23
16
  */
24
17
  import type { ExtensionContext } from "agent-sh/types";
25
18
 
26
- /** Wait for PTY output to settle after sending keystrokes. */
27
- function settle(ms = 100): Promise<void> {
28
- return new Promise((resolve) => setTimeout(resolve, ms));
29
- }
30
-
31
- /** Interpret C-style escape sequences in a string (e.g. \r → CR, \x1b → ESC). */
19
+ /** Interpret C-style escape sequences (e.g. \r CR, \x1b → ESC). */
32
20
  function interpretEscapes(str: string): string {
33
21
  return str.replace(/\\(x[0-9a-fA-F]{2}|r|n|t|\\|0)/g, (_, seq: string) => {
34
22
  if (seq === "r") return "\r";
@@ -41,27 +29,32 @@ function interpretEscapes(str: string): string {
41
29
  });
42
30
  }
43
31
 
44
- export default function activate({ bus, terminalBuffer: tb, registerTool }: ExtensionContext): void {
45
- if (!tb) {
46
- console.warn("terminal-buffer: @xterm/headless not installed — extension disabled");
47
- return;
48
- }
32
+ function settle(ms = 100): Promise<void> {
33
+ return new Promise((resolve) => setTimeout(resolve, ms));
34
+ }
49
35
 
50
- // ── Agent tools ─────────────────────────────────────────────
51
- // Context injection is intentionally NOT done here the terminal
52
- // buffer content would bloat every agent message. The agent can
53
- // call terminal_read on demand, and the overlay extension injects
54
- // context only when the overlay is active.
36
+ export default function activate(ctx: ExtensionContext): void {
37
+ const { bus, terminalBuffer: tb, registerTool, registerInstruction } = ctx;
38
+ if (!tb) return; // @xterm/headless not installed
55
39
 
56
40
  registerTool({
57
41
  name: "terminal_read",
58
42
  description:
59
- "Read the current terminal screen contents. Returns clean text (ANSI stripped) " +
43
+ "Read what is currently visible on the user's terminal screen. Returns clean text (ANSI stripped) " +
60
44
  "with cursor position and whether an alternate-screen program (vim, htop, less) is active. " +
61
- "Use this to see what the user sees before sending keystrokes with terminal_keys.",
45
+ "Use this to observe what the user sees helpful for answering questions about terminal output, " +
46
+ "diagnosing errors on screen, or checking state before/after sending keystrokes with terminal_keys.",
62
47
  input_schema: {
63
48
  type: "object",
64
- properties: {},
49
+ properties: {
50
+ include_scrollback: {
51
+ type: "boolean",
52
+ description:
53
+ "If true, include scrollback buffer (content that scrolled off screen) " +
54
+ "in addition to the visible viewport. Useful for capturing output from " +
55
+ "long-running or streaming commands. Default: false.",
56
+ },
57
+ },
65
58
  },
66
59
  showOutput: true,
67
60
 
@@ -71,8 +64,9 @@ export default function activate({ bus, terminalBuffer: tb, registerTool }: Exte
71
64
  locations: [],
72
65
  }),
73
66
 
74
- async execute() {
75
- const { text, altScreen, cursorX, cursorY } = tb.readScreen();
67
+ async execute(args) {
68
+ const includeScrollback = (args.include_scrollback as boolean) ?? false;
69
+ const { text, altScreen, cursorX, cursorY } = tb.readScreen({ includeScrollback });
76
70
  const info = [
77
71
  altScreen ? "mode: alternate screen" : "mode: normal",
78
72
  `cursor: row=${cursorY} col=${cursorX}`,
@@ -89,8 +83,10 @@ export default function activate({ bus, terminalBuffer: tb, registerTool }: Exte
89
83
  registerTool({
90
84
  name: "terminal_keys",
91
85
  description:
92
- "Send keystrokes to the user's live terminal. The keys are written directly to the PTY " +
93
- "as if the user typed them. Use escape sequences for special keys:\n" +
86
+ "Send keystrokes directly into the user's live terminal PTY, as if the user typed them. " +
87
+ "Use this to interact with programs already running in the terminal (vim, htop, less, ssh, REPLs, etc.) " +
88
+ "or to type commands at the shell prompt. This types directly into whatever is currently on screen.\n\n" +
89
+ "Escape sequences for special keys:\n" +
94
90
  " - Escape: \\x1b\n" +
95
91
  " - Enter/Return: \\r\n" +
96
92
  " - Tab: \\t\n" +
@@ -121,7 +117,7 @@ export default function activate({ bus, terminalBuffer: tb, registerTool }: Exte
121
117
  },
122
118
  showOutput: false,
123
119
 
124
- getDisplayInfo: (args) => ({
120
+ getDisplayInfo: () => ({
125
121
  kind: "execute" as const,
126
122
  icon: "⌨",
127
123
  locations: [],
@@ -129,8 +125,6 @@ export default function activate({ bus, terminalBuffer: tb, registerTool }: Exte
129
125
 
130
126
  formatCall: (args) => {
131
127
  const keys = args.keys as string;
132
- // Show a readable version of the keys — handle both literal
133
- // escape strings (\\x1b) and actual bytes (\x1b)
134
128
  return keys
135
129
  .replace(/\\x1b|\x1b/g, "ESC")
136
130
  .replace(/\\r|\r/g, "⏎")
@@ -146,17 +140,13 @@ export default function activate({ bus, terminalBuffer: tb, registerTool }: Exte
146
140
  const keys = interpretEscapes(raw);
147
141
  const settleMs = (args.settle_ms as number) ?? 150;
148
142
 
149
- // Force PTY output visible so the user sees the program's response.
150
- // Stays visible for the rest of agent processing — Shell resets
151
- // paused=false on processing-done anyway.
152
143
  bus.emit("shell:stdout-show", {});
153
144
  process.stdout.write("\n");
154
145
  bus.emit("shell:pty-write", { data: keys });
155
146
 
156
- // Wait for the terminal to process the keystrokes and render
157
147
  await settle(settleMs);
148
+ bus.emit("shell:stdout-hide", {});
158
149
 
159
- // Return the screen state after the keystrokes
160
150
  const { text, altScreen, cursorX, cursorY } = tb.readScreen();
161
151
  const info = [
162
152
  altScreen ? "mode: alternate screen" : "mode: normal",
@@ -170,15 +160,4 @@ export default function activate({ bus, terminalBuffer: tb, registerTool }: Exte
170
160
  };
171
161
  },
172
162
  });
173
-
174
- // ── Bus snapshot for other extensions ───────────────────────
175
-
176
- bus.on("shell:buffer-request", () => {
177
- const { text, altScreen, cursorX, cursorY } = tb.readScreen();
178
- bus.emit("shell:buffer-snapshot", {
179
- text,
180
- altScreen,
181
- cursor: { x: cursorX, y: cursorY },
182
- });
183
- });
184
163
  }