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.
- package/README.md +27 -43
- package/dist/agent/agent-loop.d.ts +69 -6
- package/dist/agent/agent-loop.js +954 -153
- package/dist/agent/conversation-state.d.ts +74 -21
- package/dist/agent/conversation-state.js +361 -150
- package/dist/agent/history-file.d.ts +13 -4
- package/dist/agent/history-file.js +110 -36
- package/dist/agent/nuclear-form.d.ts +28 -3
- package/dist/agent/nuclear-form.js +88 -6
- package/dist/agent/skills.d.ts +2 -4
- package/dist/agent/skills.js +10 -4
- package/dist/agent/subagent.d.ts +23 -0
- package/dist/agent/subagent.js +53 -11
- package/dist/agent/system-prompt.d.ts +37 -5
- package/dist/agent/system-prompt.js +100 -67
- package/dist/{token-budget.d.ts → agent/token-budget.d.ts} +5 -4
- package/dist/{token-budget.js → agent/token-budget.js} +15 -20
- package/dist/agent/tool-protocol.d.ts +105 -0
- package/dist/agent/tool-protocol.js +551 -0
- package/dist/agent/tools/bash.js +3 -3
- package/dist/agent/tools/edit-file.js +9 -6
- package/dist/agent/tools/glob.js +4 -2
- package/dist/agent/tools/grep.js +27 -3
- package/dist/agent/tools/ls.js +5 -6
- package/dist/agent/types.d.ts +22 -2
- package/dist/context-manager.d.ts +17 -0
- package/dist/context-manager.js +37 -4
- package/dist/core.d.ts +7 -7
- package/dist/core.js +99 -196
- package/dist/event-bus.d.ts +85 -2
- package/dist/event-bus.js +20 -1
- package/dist/executor.d.ts +4 -3
- package/dist/executor.js +18 -15
- package/dist/extension-loader.d.ts +5 -0
- package/dist/extension-loader.js +143 -19
- package/dist/extensions/agent-backend.d.ts +14 -0
- package/dist/extensions/agent-backend.js +188 -0
- package/dist/extensions/command-suggest.d.ts +3 -3
- package/dist/extensions/command-suggest.js +4 -3
- package/dist/extensions/index.d.ts +19 -0
- package/dist/extensions/index.js +24 -0
- package/dist/extensions/slash-commands.d.ts +1 -1
- package/dist/extensions/slash-commands.js +30 -10
- package/dist/extensions/tui-renderer.js +117 -113
- package/dist/index.js +39 -26
- package/dist/settings.d.ts +40 -3
- package/dist/settings.js +57 -10
- package/dist/{input-handler.d.ts → shell/input-handler.d.ts} +3 -2
- package/dist/{input-handler.js → shell/input-handler.js} +111 -85
- package/dist/{output-parser.d.ts → shell/output-parser.d.ts} +1 -1
- package/dist/{output-parser.js → shell/output-parser.js} +1 -1
- package/dist/{shell.d.ts → shell/shell.d.ts} +8 -2
- package/dist/{shell.js → shell/shell.js} +39 -8
- package/dist/types.d.ts +61 -10
- package/dist/utils/ansi.d.ts +5 -0
- package/dist/utils/ansi.js +1 -1
- package/dist/utils/compositor.d.ts +67 -0
- package/dist/utils/compositor.js +116 -0
- package/dist/utils/diff-renderer.d.ts +9 -0
- package/dist/utils/diff-renderer.js +312 -146
- package/dist/utils/diff.d.ts +21 -2
- package/dist/utils/diff.js +165 -89
- package/dist/utils/floating-panel.d.ts +2 -0
- package/dist/utils/floating-panel.js +30 -14
- package/dist/utils/handler-registry.d.ts +31 -10
- package/dist/utils/handler-registry.js +58 -16
- package/dist/utils/line-editor.d.ts +33 -3
- package/dist/utils/line-editor.js +221 -44
- package/dist/utils/markdown.d.ts +1 -0
- package/dist/utils/markdown.js +1 -1
- package/dist/utils/message-utils.d.ts +35 -0
- package/dist/utils/message-utils.js +75 -0
- package/dist/utils/terminal-buffer.d.ts +5 -1
- package/dist/utils/terminal-buffer.js +18 -2
- package/dist/utils/tool-display.d.ts +1 -1
- package/dist/utils/tool-display.js +4 -4
- package/dist/utils/tool-interactive.d.ts +12 -0
- package/dist/utils/tool-interactive.js +53 -0
- package/examples/extensions/ash-acp-bridge/README.md +39 -0
- package/examples/extensions/ash-acp-bridge/package.json +23 -0
- package/examples/extensions/ash-acp-bridge/src/index.ts +574 -0
- package/examples/extensions/ash-acp-bridge/tsconfig.json +14 -0
- package/examples/extensions/ash-mcp-bridge/README.md +72 -0
- package/examples/extensions/ash-mcp-bridge/index.ts +164 -0
- package/examples/extensions/ash-mcp-bridge/package.json +9 -0
- package/examples/extensions/claude-code-bridge/index.ts +198 -51
- package/examples/extensions/claude-code-bridge/package.json +1 -0
- package/examples/extensions/interactive-prompts.ts +98 -112
- package/examples/extensions/overlay-agent.ts +84 -38
- package/examples/extensions/peer-mesh.ts +565 -0
- package/examples/extensions/pi-bridge/index.ts +2 -2
- package/examples/extensions/questionnaire.ts +260 -0
- package/examples/extensions/subagents.ts +19 -4
- package/examples/extensions/terminal-buffer.ts +32 -53
- package/examples/extensions/tmux-pane.ts +307 -0
- package/examples/extensions/user-shell.ts +136 -0
- package/examples/extensions/web-access.ts +335 -0
- package/package.json +44 -2
- package/dist/agent/tools/display.d.ts +0 -13
- package/dist/agent/tools/display.js +0 -70
- package/dist/agent/tools/user-shell.d.ts +0 -13
- package/dist/agent/tools/user-shell.js +0 -87
- package/dist/extensions/overlay-agent.d.ts +0 -14
- package/dist/extensions/overlay-agent.js +0 -147
- package/dist/extensions/terminal-buffer.d.ts +0 -14
- 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 "
|
|
12
|
-
import { runSubagent } from "
|
|
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
|
|
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
|
-
*
|
|
22
|
-
*
|
|
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
|
-
/**
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
return;
|
|
48
|
-
}
|
|
32
|
+
function settle(ms = 100): Promise<void> {
|
|
33
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
34
|
+
}
|
|
49
35
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
93
|
-
"
|
|
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: (
|
|
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
|
}
|