agent-sh 0.9.0 → 0.10.1
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 +25 -30
- package/dist/agent/agent-loop.d.ts +43 -6
- package/dist/agent/agent-loop.js +817 -157
- package/dist/agent/conversation-state.d.ts +72 -21
- package/dist/agent/conversation-state.js +364 -151
- 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 +84 -3
- 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 +34 -1
- package/dist/agent/system-prompt.js +96 -47
- package/dist/agent/token-budget.d.ts +10 -13
- package/dist/agent/token-budget.js +6 -46
- package/dist/agent/tool-protocol.d.ts +23 -1
- package/dist/agent/tool-protocol.js +169 -4
- 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 +1 -2
- package/dist/context-manager.d.ts +16 -19
- package/dist/context-manager.js +48 -152
- package/dist/core.js +27 -6
- package/dist/event-bus.d.ts +59 -3
- package/dist/executor.d.ts +4 -3
- package/dist/executor.js +18 -15
- package/dist/extension-loader.js +75 -17
- package/dist/extensions/agent-backend.d.ts +8 -7
- package/dist/extensions/agent-backend.js +72 -50
- package/dist/extensions/index.js +0 -2
- package/dist/extensions/slash-commands.js +14 -9
- package/dist/extensions/tui-renderer.js +67 -80
- package/dist/index.js +25 -6
- package/dist/settings.d.ts +39 -16
- package/dist/settings.js +51 -11
- package/dist/shell/input-handler.d.ts +2 -1
- package/dist/shell/input-handler.js +84 -76
- package/dist/shell/shell.js +19 -2
- package/dist/types.d.ts +15 -0
- package/dist/utils/ansi.d.ts +7 -0
- package/dist/utils/ansi.js +69 -8
- package/dist/utils/box-frame.js +8 -2
- package/dist/utils/compositor.d.ts +5 -0
- package/dist/utils/compositor.js +31 -3
- package/dist/utils/diff-renderer.d.ts +9 -0
- package/dist/utils/diff-renderer.js +221 -143
- package/dist/utils/diff.d.ts +21 -2
- package/dist/utils/diff.js +165 -89
- package/dist/utils/handler-registry.d.ts +5 -0
- package/dist/utils/handler-registry.js +6 -0
- package/dist/utils/line-editor.d.ts +11 -1
- package/dist/utils/line-editor.js +44 -5
- package/dist/utils/markdown.js +23 -8
- package/dist/utils/package-version.d.ts +1 -0
- package/dist/utils/package-version.js +10 -0
- package/dist/utils/shell-output-spill.d.ts +2 -0
- package/dist/utils/shell-output-spill.js +81 -0
- package/dist/utils/tool-display.d.ts +1 -1
- package/dist/utils/tool-display.js +4 -4
- package/examples/extensions/ash-acp-bridge/src/index.ts +4 -1
- package/examples/extensions/ash-mcp-bridge/index.ts +13 -3
- package/examples/extensions/claude-code-bridge/README.md +14 -0
- package/examples/extensions/claude-code-bridge/index.ts +204 -145
- package/examples/extensions/claude-code-bridge/package.json +1 -0
- package/examples/extensions/interactive-prompts.ts +39 -25
- package/examples/extensions/overlay-agent.ts +3 -3
- package/examples/extensions/peer-mesh.ts +115 -0
- package/examples/extensions/pi-bridge/README.md +16 -0
- package/examples/extensions/pi-bridge/index.ts +9 -155
- package/examples/extensions/questionnaire.ts +16 -5
- package/examples/extensions/subagents.ts +19 -4
- package/examples/extensions/terminal-buffer.ts +163 -0
- package/examples/extensions/user-shell.ts +136 -0
- package/examples/extensions/web-access.ts +8 -0
- package/package.json +36 -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/shell-recall.d.ts +0 -9
- package/dist/extensions/shell-recall.js +0 -8
- package/dist/extensions/terminal-buffer.d.ts +0 -14
- package/dist/extensions/terminal-buffer.js +0 -134
|
@@ -65,6 +65,17 @@ export default async function activate(ctx: any): Promise<void> {
|
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
// Contribute connected servers to the startup banner
|
|
69
|
+
bus.onPipe("banner:collect", (e) => {
|
|
70
|
+
if (connected.length > 0) {
|
|
71
|
+
e.sections.push({
|
|
72
|
+
label: "MCP Servers",
|
|
73
|
+
items: connected.map((s) => s.name),
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
return e;
|
|
77
|
+
});
|
|
78
|
+
|
|
68
79
|
// Clean up on exit
|
|
69
80
|
bus.on("app:quit", () => {
|
|
70
81
|
for (const server of connected) {
|
|
@@ -84,6 +95,7 @@ async function connectServer(
|
|
|
84
95
|
command: config.command,
|
|
85
96
|
args: config.args,
|
|
86
97
|
env: { ...process.env, ...config.env } as Record<string, string>,
|
|
98
|
+
stderr: "pipe",
|
|
87
99
|
});
|
|
88
100
|
|
|
89
101
|
const client = new Client({ name: `ash-${name}`, version: "0.1.0" });
|
|
@@ -146,9 +158,7 @@ async function connectServer(
|
|
|
146
158
|
});
|
|
147
159
|
}
|
|
148
160
|
|
|
149
|
-
|
|
150
|
-
message: `mcp-bridge: "${name}" connected (${tools.length} tools)`,
|
|
151
|
-
});
|
|
161
|
+
// ui:info suppressed — connection is silent by default
|
|
152
162
|
|
|
153
163
|
return { name, client, transport };
|
|
154
164
|
}
|
|
@@ -33,3 +33,17 @@ Or switch at runtime:
|
|
|
33
33
|
|
|
34
34
|
- `ANTHROPIC_API_KEY` must be set in your environment
|
|
35
35
|
- Claude Code manages its own model selection — no model configuration needed in agent-sh
|
|
36
|
+
|
|
37
|
+
## What this bridge is
|
|
38
|
+
|
|
39
|
+
A pure protocol translator between the Claude Agent SDK's event stream and agent-sh's bus events. Claude Code uses its own built-in tools exactly as the SDK ships them (`Read`, `Edit`, `Write`, `Bash`, `Glob`, `Grep`). The bridge adds no tools of its own.
|
|
40
|
+
|
|
41
|
+
## What this bridge intentionally does NOT bundle
|
|
42
|
+
|
|
43
|
+
Three PTY-access tools are left out on purpose:
|
|
44
|
+
|
|
45
|
+
- `terminal_read` — observe the user's live terminal screen
|
|
46
|
+
- `terminal_keys` — send keystrokes to the user's PTY
|
|
47
|
+
- `user_shell` — run commands in the user's live shell with lasting `cd`/`export`/`source` effects
|
|
48
|
+
|
|
49
|
+
These are opt-in capabilities that belong in their own extensions. If you want any of them with Claude Code, write a companion extension that uses the SDK's `tool()` + `createSdkMcpServer()` to expose them as MCP tools, and extend the bridge (or fork it) to attach that MCP server to the SDK's `query()` options.
|
|
@@ -2,148 +2,76 @@
|
|
|
2
2
|
* Claude Code bridge — runs Claude Code Agent SDK in-process as agent-sh's backend.
|
|
3
3
|
*
|
|
4
4
|
* Uses the official @anthropic-ai/claude-agent-sdk to spawn a Claude Code
|
|
5
|
-
* session
|
|
6
|
-
*
|
|
5
|
+
* session. Claude Code handles its own model selection, tool execution, and
|
|
6
|
+
* permissions — the bridge is a pure protocol translator between the SDK's
|
|
7
|
+
* event stream and agent-sh's bus events.
|
|
7
8
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
9
|
+
* PTY-access tools (`terminal_read`, `terminal_keys`, `user_shell`) are
|
|
10
|
+
* intentionally NOT bundled here. If you want Claude Code to observe or
|
|
11
|
+
* drive the user's live terminal, load a companion extension that
|
|
12
|
+
* registers those tools as MCP tools the SDK can consume.
|
|
13
|
+
*
|
|
14
|
+
* Setup (from repo root):
|
|
15
|
+
* npm run build && npm link # register local agent-sh globally
|
|
16
|
+
* cd examples/extensions/claude-code-bridge
|
|
17
|
+
* npm install && npm link agent-sh # link local dev copy
|
|
10
18
|
*
|
|
11
19
|
* Usage:
|
|
12
20
|
* agent-sh -e examples/extensions/claude-code-bridge
|
|
13
21
|
*
|
|
14
22
|
* Requires: Claude Code CLI installed and authenticated (claude login).
|
|
15
23
|
*/
|
|
16
|
-
import {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
} from "@anthropic-ai/claude-agent-sdk";
|
|
22
|
-
import { z } from "zod";
|
|
23
|
-
import type { ExtensionContext } from "../../src/types.js";
|
|
24
|
-
import type { EventBus } from "../../src/event-bus.js";
|
|
25
|
-
|
|
26
|
-
// ── Helpers ──────────────────────────────────────────────────────
|
|
27
|
-
function interpretEscapes(str: string): string {
|
|
28
|
-
return str.replace(/\\(x[0-9a-fA-F]{2}|r|n|t|\\|0)/g, (_, seq: string) => {
|
|
29
|
-
if (seq === "r") return "\r";
|
|
30
|
-
if (seq === "n") return "\n";
|
|
31
|
-
if (seq === "t") return "\t";
|
|
32
|
-
if (seq === "\\") return "\\";
|
|
33
|
-
if (seq === "0") return "\0";
|
|
34
|
-
if (seq.startsWith("x")) return String.fromCharCode(parseInt(seq.slice(1), 16));
|
|
35
|
-
return seq;
|
|
36
|
-
});
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function settle(ms = 100): Promise<void> {
|
|
40
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// ── user_shell MCP tool ───────────────────────────────────────────
|
|
44
|
-
function createUserShellTool(bus: EventBus) {
|
|
45
|
-
let liveCwd = process.cwd();
|
|
46
|
-
bus.on("shell:cwd-change", ({ cwd }) => { liveCwd = cwd; });
|
|
47
|
-
|
|
48
|
-
return tool(
|
|
49
|
-
"user_shell",
|
|
50
|
-
"Run a command with lasting effects in the user's live shell (cd, export, " +
|
|
51
|
-
"install packages, start servers) or show output the user wants to see. " +
|
|
52
|
-
"Set return_output=true only if you need to inspect the result.",
|
|
53
|
-
{
|
|
54
|
-
command: z.string().describe("Command to execute in user's shell"),
|
|
55
|
-
return_output: z.boolean().optional().describe(
|
|
56
|
-
"Whether to return the command output. Default false.",
|
|
57
|
-
),
|
|
58
|
-
},
|
|
59
|
-
async (args) => {
|
|
60
|
-
const result = await bus.emitPipeAsync("shell:exec-request", {
|
|
61
|
-
command: args.command,
|
|
62
|
-
output: "",
|
|
63
|
-
cwd: liveCwd,
|
|
64
|
-
done: false,
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
const text = args.return_output
|
|
68
|
-
? result.output || "(no output)"
|
|
69
|
-
: "Command executed.";
|
|
70
|
-
|
|
71
|
-
return { content: [{ type: "text" as const, text }] };
|
|
72
|
-
},
|
|
73
|
-
);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// ── terminal_read MCP tool ────────────────────────────────────────
|
|
77
|
-
function createTerminalReadTool(ctx: ExtensionContext) {
|
|
78
|
-
return tool(
|
|
79
|
-
"terminal_read",
|
|
80
|
-
"Read the current terminal screen contents. Returns clean text (ANSI stripped) " +
|
|
81
|
-
"with cursor position and whether an alternate-screen program (vim, htop, less) is active. " +
|
|
82
|
-
"Use this to see what the user sees before sending keystrokes with terminal_keys.",
|
|
83
|
-
{},
|
|
84
|
-
async () => {
|
|
85
|
-
const tb = ctx.terminalBuffer;
|
|
86
|
-
if (!tb) return { content: [{ type: "text" as const, text: "terminal buffer not available" }] };
|
|
87
|
-
const { text, altScreen, cursorX, cursorY } = tb.readScreen();
|
|
88
|
-
const info = [
|
|
89
|
-
altScreen ? "mode: alternate screen" : "mode: normal",
|
|
90
|
-
`cursor: row=${cursorY} col=${cursorX}`,
|
|
91
|
-
].join(", ");
|
|
92
|
-
return { content: [{ type: "text" as const, text: `[${info}]\n\n${text}` }] };
|
|
93
|
-
},
|
|
94
|
-
);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// ── terminal_keys MCP tool ───────────────────────────────────────
|
|
98
|
-
function createTerminalKeysTool(bus: EventBus, ctx: ExtensionContext) {
|
|
99
|
-
return tool(
|
|
100
|
-
"terminal_keys",
|
|
101
|
-
"Send keystrokes to the user's live terminal. The keys are written directly to the PTY " +
|
|
102
|
-
"as if the user typed them. Use escape sequences for special keys:\n" +
|
|
103
|
-
" - Escape: \\x1b - Enter: \\r - Tab: \\t\n" +
|
|
104
|
-
" - Ctrl+C: \\x03 - Arrow keys: \\x1b[A/B/C/D - Backspace: \\x7f\n" +
|
|
105
|
-
"Example: to quit vim without saving, send keys=\"\\x1b:q!\\r\".\n" +
|
|
106
|
-
"Always call terminal_read after sending keys to verify the result.",
|
|
107
|
-
{
|
|
108
|
-
keys: z.string().describe("Keystrokes to send (use \\x1b for Escape, \\r for Enter, etc.)"),
|
|
109
|
-
settle_ms: z.number().optional().describe("Wait time in ms after sending keys (default: 150)"),
|
|
110
|
-
},
|
|
111
|
-
async (args) => {
|
|
112
|
-
const keys = interpretEscapes(args.keys);
|
|
113
|
-
const settleMs = args.settle_ms ?? 150;
|
|
114
|
-
bus.emit("shell:stdout-show", {});
|
|
115
|
-
process.stdout.write("\n");
|
|
116
|
-
bus.emit("shell:pty-write", { data: keys });
|
|
117
|
-
await settle(settleMs);
|
|
118
|
-
|
|
119
|
-
const tb = ctx.terminalBuffer;
|
|
120
|
-
if (!tb) return { content: [{ type: "text" as const, text: "Keys sent." }] };
|
|
121
|
-
const { text, altScreen, cursorX, cursorY } = tb.readScreen();
|
|
122
|
-
const info = [
|
|
123
|
-
altScreen ? "mode: alternate screen" : "mode: normal",
|
|
124
|
-
`cursor: row=${cursorY} col=${cursorX}`,
|
|
125
|
-
].join(", ");
|
|
126
|
-
return { content: [{ type: "text" as const, text: `Keys sent. Screen after:\n[${info}]\n\n${text}` }] };
|
|
127
|
-
},
|
|
128
|
-
);
|
|
129
|
-
}
|
|
24
|
+
import { query, type Query } from "@anthropic-ai/claude-agent-sdk";
|
|
25
|
+
import { readFile } from "node:fs/promises";
|
|
26
|
+
import { resolve } from "node:path";
|
|
27
|
+
import type { ExtensionContext } from "agent-sh/types";
|
|
28
|
+
import { computeDiff, type DiffResult } from "agent-sh/utils/diff";
|
|
130
29
|
|
|
131
30
|
// ── Extension entry point ─────────────────────────────────────────
|
|
132
31
|
export default function activate(ctx: ExtensionContext): void {
|
|
133
32
|
const { bus } = ctx;
|
|
134
33
|
|
|
135
|
-
const shellTool = createUserShellTool(bus);
|
|
136
|
-
const termReadTool = createTerminalReadTool(ctx);
|
|
137
|
-
const termKeysTool = createTerminalKeysTool(bus, ctx);
|
|
138
|
-
const shellServer = createSdkMcpServer({
|
|
139
|
-
name: "agent-sh",
|
|
140
|
-
version: "1.0.0",
|
|
141
|
-
tools: [shellTool, termReadTool, termKeysTool],
|
|
142
|
-
});
|
|
143
|
-
|
|
144
34
|
let activeQuery: Query | null = null;
|
|
145
35
|
const listeners: Array<{ event: string; fn: Function }> = [];
|
|
146
36
|
|
|
37
|
+
// ── Tool display helpers ────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
/** Map Claude Code tool names to agent-sh display kinds. */
|
|
40
|
+
function toolKind(name: string): string {
|
|
41
|
+
if (name === "Read") return "read";
|
|
42
|
+
if (name === "Edit") return "edit";
|
|
43
|
+
if (name === "Write") return "write";
|
|
44
|
+
if (name === "Glob" || name === "Grep") return "search";
|
|
45
|
+
if (name === "Bash") return "execute";
|
|
46
|
+
return "execute";
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Map Claude Code tool names to agent-sh display icons. */
|
|
50
|
+
function toolIcon(name: string): string | undefined {
|
|
51
|
+
if (name === "Read") return "◆";
|
|
52
|
+
if (name === "Edit") return "✎";
|
|
53
|
+
if (name === "Write") return "✎";
|
|
54
|
+
if (name === "Glob" || name === "Grep") return "⌕";
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Extract file locations from tool input args. */
|
|
59
|
+
function toolLocations(input: Record<string, unknown>): { path: string; line?: number | null }[] | undefined {
|
|
60
|
+
const raw = input.file_path ?? input.path;
|
|
61
|
+
if (typeof raw !== "string") return undefined;
|
|
62
|
+
const line = (input.line_number ?? input.line ?? input.offset) as number | undefined;
|
|
63
|
+
return [{ path: raw, line: line ?? null }];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Format a compact display string for a tool call. */
|
|
67
|
+
function formatToolCall(name: string, input: Record<string, unknown>): string {
|
|
68
|
+
const str = (v: unknown) => typeof v === "string" ? v : "";
|
|
69
|
+
if (name === "Bash") return `$ ${str(input.command)}`;
|
|
70
|
+
if (name === "Read" || name === "Edit" || name === "Write") return str(input.file_path ?? input.path);
|
|
71
|
+
if (name === "Grep" || name === "Glob") return `${str(input.pattern)} ${str(input.path)}`.trim();
|
|
72
|
+
return name;
|
|
73
|
+
}
|
|
74
|
+
|
|
147
75
|
const wireListeners = () => {
|
|
148
76
|
const onSubmit = async ({ query: userQuery }: any) => {
|
|
149
77
|
bus.emit("agent:query", { query: userQuery });
|
|
@@ -151,6 +79,14 @@ export default function activate(ctx: ExtensionContext): void {
|
|
|
151
79
|
|
|
152
80
|
let fullResponseText = "";
|
|
153
81
|
let streamed = false;
|
|
82
|
+
/** Track in-flight tool calls so we can emit tool-completed when results arrive. */
|
|
83
|
+
const pendingTools = new Map<string, { name: string; kind: string; input?: Record<string, unknown> }>();
|
|
84
|
+
/** Tool input JSON being streamed via input_json_delta events. */
|
|
85
|
+
const inputBuffers = new Map<number, string>();
|
|
86
|
+
/** Tool metadata per content block index (for correlating deltas). */
|
|
87
|
+
const blockMeta = new Map<number, { name: string; id: string }>();
|
|
88
|
+
/** Pre-edit file snapshots for diff display (Edit/Write tools). */
|
|
89
|
+
const fileSnapshots = new Map<string, string | null>();
|
|
154
90
|
|
|
155
91
|
try {
|
|
156
92
|
activeQuery = query({
|
|
@@ -162,17 +98,9 @@ export default function activate(ctx: ExtensionContext): void {
|
|
|
162
98
|
preset: "claude_code",
|
|
163
99
|
append:
|
|
164
100
|
"You are running inside agent-sh, a terminal wrapper.\n" +
|
|
165
|
-
"Use your standard tools (Read, Edit, Write, Bash, Glob, Grep) for investigation
|
|
166
|
-
"Use mcp__agent-sh__user_shell to run commands in the user's live shell when they ask to see output or need lasting effects (cd, install, start servers).\n" +
|
|
167
|
-
"Default to standard tools. Use user_shell when the user is the intended audience for the output or the command has real effects.",
|
|
101
|
+
"Use your standard tools (Read, Edit, Write, Bash, Glob, Grep) for investigation.",
|
|
168
102
|
},
|
|
169
|
-
|
|
170
|
-
allowedTools: [
|
|
171
|
-
"mcp__agent-sh__user_shell",
|
|
172
|
-
"mcp__agent-sh__terminal_read",
|
|
173
|
-
"mcp__agent-sh__terminal_keys",
|
|
174
|
-
"Read", "Edit", "Write", "Bash", "Glob", "Grep",
|
|
175
|
-
],
|
|
103
|
+
allowedTools: ["Read", "Edit", "Write", "Bash", "Glob", "Grep"],
|
|
176
104
|
permissionMode: "acceptEdits",
|
|
177
105
|
includePartialMessages: true,
|
|
178
106
|
},
|
|
@@ -183,16 +111,56 @@ export default function activate(ctx: ExtensionContext): void {
|
|
|
183
111
|
case "stream_event": {
|
|
184
112
|
streamed = true;
|
|
185
113
|
const event = message.event;
|
|
186
|
-
if (event.type === "
|
|
187
|
-
const
|
|
188
|
-
if (
|
|
114
|
+
if (event.type === "content_block_start") {
|
|
115
|
+
const cb = (event as any).content_block;
|
|
116
|
+
if (cb?.type === "tool_use") {
|
|
117
|
+
blockMeta.set(event.index, { name: cb.name, id: cb.id });
|
|
118
|
+
inputBuffers.set(event.index, "");
|
|
119
|
+
}
|
|
120
|
+
} else if (event.type === "content_block_delta") {
|
|
121
|
+
const delta = (event as any).delta;
|
|
122
|
+
if (delta?.type === "text_delta" && delta.text) {
|
|
189
123
|
bus.emitTransform("agent:response-chunk", {
|
|
190
124
|
blocks: [{ type: "text" as const, text: delta.text }],
|
|
191
125
|
});
|
|
192
126
|
fullResponseText += delta.text;
|
|
193
|
-
} else if (delta
|
|
127
|
+
} else if (delta?.type === "thinking_delta" && delta.thinking) {
|
|
194
128
|
bus.emit("agent:thinking-chunk", { text: delta.thinking });
|
|
129
|
+
} else if (delta?.type === "input_json_delta" && delta.partial_json != null) {
|
|
130
|
+
// Accumulate tool input JSON as it streams in
|
|
131
|
+
const buf = inputBuffers.get(event.index) ?? "";
|
|
132
|
+
inputBuffers.set(event.index, buf + delta.partial_json);
|
|
195
133
|
}
|
|
134
|
+
} else if (event.type === "content_block_stop") {
|
|
135
|
+
const meta = blockMeta.get(event.index);
|
|
136
|
+
const inputJson = inputBuffers.get(event.index);
|
|
137
|
+
if (meta && inputJson != null) {
|
|
138
|
+
blockMeta.delete(event.index);
|
|
139
|
+
inputBuffers.delete(event.index);
|
|
140
|
+
|
|
141
|
+
let input: Record<string, unknown> = {};
|
|
142
|
+
try { input = JSON.parse(inputJson || "{}"); } catch {}
|
|
143
|
+
|
|
144
|
+
const kind = toolKind(meta.name);
|
|
145
|
+
bus.emit("agent:tool-started", {
|
|
146
|
+
title: meta.name,
|
|
147
|
+
toolCallId: meta.id,
|
|
148
|
+
kind,
|
|
149
|
+
icon: toolIcon(meta.name),
|
|
150
|
+
locations: toolLocations(input),
|
|
151
|
+
rawInput: input,
|
|
152
|
+
displayDetail: formatToolCall(meta.name, input),
|
|
153
|
+
});
|
|
154
|
+
pendingTools.set(meta.id, { name: meta.name, kind, input });
|
|
155
|
+
|
|
156
|
+
// Snapshot file content before Edit/Write modifies it
|
|
157
|
+
if ((meta.name === "Edit" || meta.name === "Write") && typeof (input as any).file_path === "string") {
|
|
158
|
+
const absPath = resolve(process.cwd(), (input as any).file_path);
|
|
159
|
+
readFile(absPath, "utf-8")
|
|
160
|
+
.then(content => fileSnapshots.set(meta.id, content))
|
|
161
|
+
.catch(() => fileSnapshots.set(meta.id, null)); // file doesn't exist yet
|
|
162
|
+
}
|
|
163
|
+
}
|
|
196
164
|
}
|
|
197
165
|
break;
|
|
198
166
|
}
|
|
@@ -206,24 +174,115 @@ export default function activate(ctx: ExtensionContext): void {
|
|
|
206
174
|
blocks: [{ type: "text" as const, text: b.text }],
|
|
207
175
|
});
|
|
208
176
|
fullResponseText += b.text;
|
|
209
|
-
} else if (b.type === "tool_use") {
|
|
177
|
+
} else if (b.type === "tool_use" && !streamed) {
|
|
178
|
+
// Non-streamed fallback: emit tool-started from full message
|
|
179
|
+
const input = (b.input ?? {}) as Record<string, unknown>;
|
|
180
|
+
const kind = toolKind(b.name);
|
|
210
181
|
bus.emit("agent:tool-started", {
|
|
211
182
|
title: b.name,
|
|
212
183
|
toolCallId: b.id,
|
|
213
|
-
kind
|
|
214
|
-
|
|
215
|
-
|
|
184
|
+
kind,
|
|
185
|
+
icon: toolIcon(b.name),
|
|
186
|
+
locations: toolLocations(input),
|
|
187
|
+
rawInput: input,
|
|
188
|
+
displayDetail: formatToolCall(b.name, input),
|
|
216
189
|
});
|
|
190
|
+
pendingTools.set(b.id, { name: b.name, kind, input });
|
|
191
|
+
|
|
192
|
+
// Snapshot file content before Edit/Write modifies it
|
|
193
|
+
if ((b.name === "Edit" || b.name === "Write") && typeof (input as any).file_path === "string") {
|
|
194
|
+
const absPath = resolve(process.cwd(), (input as any).file_path);
|
|
195
|
+
readFile(absPath, "utf-8")
|
|
196
|
+
.then(content => fileSnapshots.set(b.id, content))
|
|
197
|
+
.catch(() => fileSnapshots.set(b.id, null));
|
|
198
|
+
}
|
|
217
199
|
}
|
|
218
200
|
}
|
|
219
201
|
break;
|
|
220
202
|
}
|
|
221
203
|
|
|
204
|
+
case "user": {
|
|
205
|
+
// Tool results come back as user messages with tool_result content blocks
|
|
206
|
+
const msg = message.message as any;
|
|
207
|
+
if (msg?.content && Array.isArray(msg.content)) {
|
|
208
|
+
for (const block of msg.content) {
|
|
209
|
+
if (block.type === "tool_result") {
|
|
210
|
+
const toolUseId = block.tool_use_id as string;
|
|
211
|
+
const pending = pendingTools.get(toolUseId);
|
|
212
|
+
if (!pending) continue;
|
|
213
|
+
pendingTools.delete(toolUseId);
|
|
214
|
+
|
|
215
|
+
const isError = !!block.is_error;
|
|
216
|
+
const content = typeof block.content === "string"
|
|
217
|
+
? block.content
|
|
218
|
+
: Array.isArray(block.content)
|
|
219
|
+
? block.content.map((c: any) => c.text ?? JSON.stringify(c)).join("\n")
|
|
220
|
+
: "";
|
|
221
|
+
|
|
222
|
+
// Compute diff for Edit/Write tools
|
|
223
|
+
let resultDisplay: { summary?: string; body?: { kind: "diff"; diff: DiffResult; filePath: string } } | undefined;
|
|
224
|
+
if (!isError && (pending.name === "Edit" || pending.name === "Write")) {
|
|
225
|
+
const oldContent = fileSnapshots.get(toolUseId);
|
|
226
|
+
fileSnapshots.delete(toolUseId);
|
|
227
|
+
const filePath = (pending.input as any)?.file_path as string | undefined;
|
|
228
|
+
if (filePath) {
|
|
229
|
+
const absPath = resolve(process.cwd(), filePath);
|
|
230
|
+
try {
|
|
231
|
+
const newContent = await readFile(absPath, "utf-8");
|
|
232
|
+
const diff = computeDiff(oldContent, newContent);
|
|
233
|
+
if (!diff.isIdentical) {
|
|
234
|
+
const summary = diff.isNewFile
|
|
235
|
+
? `+${diff.added}`
|
|
236
|
+
: `+${diff.added} -${diff.removed}`;
|
|
237
|
+
resultDisplay = {
|
|
238
|
+
summary,
|
|
239
|
+
body: { kind: "diff", diff, filePath: absPath },
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
} catch { /* file may not exist after failed edit */ }
|
|
243
|
+
}
|
|
244
|
+
} else {
|
|
245
|
+
fileSnapshots.delete(toolUseId);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const exitCode = isError ? 1 : 0;
|
|
249
|
+
bus.emitTransform("agent:tool-completed", {
|
|
250
|
+
toolCallId: toolUseId,
|
|
251
|
+
exitCode,
|
|
252
|
+
rawOutput: content,
|
|
253
|
+
kind: pending.kind,
|
|
254
|
+
resultDisplay,
|
|
255
|
+
});
|
|
256
|
+
bus.emit("agent:tool-output", {
|
|
257
|
+
tool: pending.name,
|
|
258
|
+
output: content,
|
|
259
|
+
exitCode,
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
break;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
case "tool_progress":
|
|
268
|
+
// Tool still running — nothing to do, TUI spinner already active
|
|
269
|
+
break;
|
|
270
|
+
|
|
222
271
|
case "result":
|
|
223
272
|
break;
|
|
224
273
|
}
|
|
225
274
|
}
|
|
226
275
|
|
|
276
|
+
// Emit completion for any tools still pending (edge case: interrupted query)
|
|
277
|
+
for (const [id, pending] of pendingTools) {
|
|
278
|
+
bus.emitTransform("agent:tool-completed", {
|
|
279
|
+
toolCallId: id,
|
|
280
|
+
exitCode: 0,
|
|
281
|
+
rawOutput: "",
|
|
282
|
+
kind: pending.kind,
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
|
|
227
286
|
bus.emitTransform("agent:response-done", {
|
|
228
287
|
response: fullResponseText,
|
|
229
288
|
});
|
|
@@ -18,9 +18,44 @@ import { palette as p } from "agent-sh/utils/palette.js";
|
|
|
18
18
|
import type { ExtensionContext } from "agent-sh/types";
|
|
19
19
|
import type { ToolUI } from "agent-sh/agent/types.js";
|
|
20
20
|
|
|
21
|
-
export default function activate(
|
|
21
|
+
export default function activate(ctx: ExtensionContext) {
|
|
22
22
|
let autoApproveWrites = false;
|
|
23
23
|
|
|
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
|
+
const boxW = Math.min(84, width);
|
|
29
|
+
const contentW = boxW - 4;
|
|
30
|
+
const MAX_DISPLAY = 25;
|
|
31
|
+
|
|
32
|
+
const stats = diff.isNewFile
|
|
33
|
+
? `(+${diff.added} lines)`
|
|
34
|
+
: `(+${diff.added} / -${diff.removed})`;
|
|
35
|
+
const title = diff.isNewFile
|
|
36
|
+
? `new: ${filePath} ${stats}`
|
|
37
|
+
: `${filePath} ${stats}`;
|
|
38
|
+
|
|
39
|
+
const diffLines = renderDiff(diff, {
|
|
40
|
+
width: contentW,
|
|
41
|
+
filePath,
|
|
42
|
+
maxLines: MAX_DISPLAY,
|
|
43
|
+
trueColor: true,
|
|
44
|
+
mode: "unified",
|
|
45
|
+
});
|
|
46
|
+
const content = ["", ...diffLines.slice(1), ""];
|
|
47
|
+
|
|
48
|
+
return renderBoxFrame(content, {
|
|
49
|
+
width: boxW,
|
|
50
|
+
style: "rounded",
|
|
51
|
+
borderColor: p.warning,
|
|
52
|
+
title,
|
|
53
|
+
footer: [` ${p.bold}[y] Apply [n] Skip [a] Don't ask again${p.reset}`],
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const { bus } = ctx;
|
|
58
|
+
|
|
24
59
|
bus.onPipeAsync("permission:request", async (payload) => {
|
|
25
60
|
const ui = payload.ui as ToolUI | undefined;
|
|
26
61
|
if (!ui) return payload;
|
|
@@ -82,36 +117,15 @@ async function handleToolCall(payload: any, ui: ToolUI) {
|
|
|
82
117
|
}
|
|
83
118
|
|
|
84
119
|
async function handleFileWrite(payload: any, ui: ToolUI) {
|
|
85
|
-
const diff = payload.metadata.diff;
|
|
86
|
-
const filePath = payload.metadata.path ?? payload.title;
|
|
87
|
-
|
|
88
120
|
const answer = await ui.custom<"approve" | "approve_all" | "reject">({
|
|
89
121
|
render(width) {
|
|
90
122
|
const boxW = Math.min(84, width);
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
const stats = diff.isNewFile
|
|
95
|
-
? `(+${diff.added} lines)`
|
|
96
|
-
: `(+${diff.added} / -${diff.removed})`;
|
|
97
|
-
const title = diff.isNewFile
|
|
98
|
-
? `new: ${filePath} ${stats}`
|
|
99
|
-
: `${filePath} ${stats}`;
|
|
100
|
-
|
|
101
|
-
const diffLines = renderDiff(diff, {
|
|
102
|
-
width: contentW,
|
|
103
|
-
filePath,
|
|
104
|
-
maxLines: MAX_DISPLAY,
|
|
105
|
-
trueColor: true,
|
|
106
|
-
mode: "unified",
|
|
107
|
-
});
|
|
108
|
-
const content = ["", ...diffLines.slice(1), ""];
|
|
109
|
-
|
|
110
|
-
return renderBoxFrame(content, {
|
|
123
|
+
// Just show the prompt actions — the diff itself was already rendered
|
|
124
|
+
// by our advise on "tui:render-diff".
|
|
125
|
+
return renderBoxFrame([], {
|
|
111
126
|
width: boxW,
|
|
112
127
|
style: "rounded",
|
|
113
128
|
borderColor: p.warning,
|
|
114
|
-
title,
|
|
115
129
|
footer: [` ${p.bold}[y] Apply [n] Skip [a] Don't ask again${p.reset}`],
|
|
116
130
|
});
|
|
117
131
|
},
|
|
@@ -16,9 +16,9 @@
|
|
|
16
16
|
*
|
|
17
17
|
* Requires: npm install @xterm/headless@5.5.0 @xterm/addon-serialize@0.13.0
|
|
18
18
|
*/
|
|
19
|
-
import type { ExtensionContext, RemoteSession } from "
|
|
20
|
-
import type { RenderSurface } from "
|
|
21
|
-
import { FloatingPanel } from "
|
|
19
|
+
import type { ExtensionContext, RemoteSession } from "agent-sh/types";
|
|
20
|
+
import type { RenderSurface } from "agent-sh/utils/compositor";
|
|
21
|
+
import { FloatingPanel } from "agent-sh/utils/floating-panel";
|
|
22
22
|
|
|
23
23
|
/** Adapt a FloatingPanel to the RenderSurface interface. */
|
|
24
24
|
function createPanelSurface(panel: FloatingPanel): RenderSurface {
|