agent-sh 0.8.0 → 0.9.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 +25 -34
- package/dist/agent/agent-loop.d.ts +29 -6
- package/dist/agent/agent-loop.js +177 -59
- package/dist/agent/conversation-state.d.ts +3 -1
- package/dist/agent/conversation-state.js +6 -2
- package/dist/agent/nuclear-form.js +5 -4
- package/dist/agent/system-prompt.d.ts +4 -5
- package/dist/agent/system-prompt.js +12 -28
- package/dist/{token-budget.js → agent/token-budget.js} +1 -1
- package/dist/agent/tool-protocol.d.ts +83 -0
- package/dist/agent/tool-protocol.js +386 -0
- package/dist/agent/types.d.ts +21 -1
- package/dist/core.d.ts +7 -7
- package/dist/core.js +76 -194
- package/dist/event-bus.d.ts +26 -0
- package/dist/event-bus.js +20 -1
- package/dist/extension-loader.d.ts +5 -0
- package/dist/extension-loader.js +104 -17
- package/dist/extensions/agent-backend.d.ts +13 -0
- package/dist/extensions/agent-backend.js +167 -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 +25 -0
- package/dist/extensions/slash-commands.d.ts +1 -1
- package/dist/extensions/slash-commands.js +16 -1
- package/dist/extensions/terminal-buffer.d.ts +1 -1
- package/dist/extensions/terminal-buffer.js +13 -4
- package/dist/extensions/tui-renderer.js +63 -43
- package/dist/index.js +14 -20
- package/dist/settings.d.ts +6 -0
- package/dist/settings.js +4 -1
- package/dist/{input-handler.d.ts → shell/input-handler.d.ts} +1 -1
- package/dist/{input-handler.js → shell/input-handler.js} +60 -43
- 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} +20 -6
- package/dist/types.d.ts +49 -10
- package/dist/utils/compositor.d.ts +62 -0
- package/dist/utils/compositor.js +88 -0
- package/dist/utils/diff-renderer.js +92 -4
- package/dist/utils/floating-panel.d.ts +2 -0
- package/dist/utils/floating-panel.js +30 -14
- package/dist/utils/handler-registry.d.ts +26 -10
- package/dist/utils/handler-registry.js +52 -16
- package/dist/utils/line-editor.d.ts +23 -3
- package/dist/utils/line-editor.js +180 -42
- 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-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 +571 -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 +154 -0
- package/examples/extensions/ash-mcp-bridge/package.json +9 -0
- package/examples/extensions/interactive-prompts.ts +82 -110
- package/examples/extensions/overlay-agent.ts +84 -38
- package/examples/extensions/peer-mesh.ts +450 -0
- package/examples/extensions/questionnaire.ts +249 -0
- package/examples/extensions/tmux-pane.ts +307 -0
- package/examples/extensions/web-access.ts +327 -0
- package/package.json +9 -1
- package/dist/extensions/overlay-agent.d.ts +0 -14
- package/dist/extensions/overlay-agent.js +0 -147
- package/examples/extensions/terminal-buffer.ts +0 -184
- /package/dist/{token-budget.d.ts → agent/token-budget.d.ts} +0 -0
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# ash-mcp-bridge
|
|
2
|
+
|
|
3
|
+
Connects any MCP (Model Context Protocol) server to ash. Spawns servers as child processes over stdio, discovers their tools, and registers each as a native ash tool.
|
|
4
|
+
|
|
5
|
+
## Setup
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
cp -r examples/extensions/ash-mcp-bridge ~/.agent-sh/extensions/
|
|
9
|
+
cd ~/.agent-sh/extensions/ash-mcp-bridge && npm install
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Configuration
|
|
13
|
+
|
|
14
|
+
Add server definitions to `~/.agent-sh/settings.json`:
|
|
15
|
+
|
|
16
|
+
```json
|
|
17
|
+
{
|
|
18
|
+
"mcp-bridge": {
|
|
19
|
+
"servers": {
|
|
20
|
+
"vision": {
|
|
21
|
+
"command": "npx",
|
|
22
|
+
"args": ["-y", "@z_ai/mcp-server"],
|
|
23
|
+
"env": {
|
|
24
|
+
"Z_AI_API_KEY": "your-key",
|
|
25
|
+
"Z_AI_MODE": "ZAI"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Each server entry:
|
|
34
|
+
|
|
35
|
+
| Field | Type | Description |
|
|
36
|
+
|-------|------|-------------|
|
|
37
|
+
| `command` | `string` | Executable to spawn (e.g. `npx`, `node`) |
|
|
38
|
+
| `args` | `string[]` | Command arguments |
|
|
39
|
+
| `env` | `Record<string, string>` | Extra environment variables (merged with `process.env`) |
|
|
40
|
+
|
|
41
|
+
## How it works
|
|
42
|
+
|
|
43
|
+
On activation, the extension:
|
|
44
|
+
|
|
45
|
+
1. Reads `mcp-bridge.servers` from settings
|
|
46
|
+
2. Spawns each server as a child process with stdio transport
|
|
47
|
+
3. Connects via the MCP SDK client
|
|
48
|
+
4. Calls `listTools()` to discover available tools
|
|
49
|
+
5. Registers each tool as `mcp_{server}_{tool}` (e.g. `mcp_vision_image_analysis`)
|
|
50
|
+
|
|
51
|
+
Tools are then available to the agent like any built-in tool.
|
|
52
|
+
|
|
53
|
+
## Example: Z.AI Vision
|
|
54
|
+
|
|
55
|
+
```json
|
|
56
|
+
{
|
|
57
|
+
"mcp-bridge": {
|
|
58
|
+
"servers": {
|
|
59
|
+
"vision": {
|
|
60
|
+
"command": "npx",
|
|
61
|
+
"args": ["-y", "@z_ai/mcp-server"],
|
|
62
|
+
"env": {
|
|
63
|
+
"Z_AI_API_KEY": "your-key",
|
|
64
|
+
"Z_AI_MODE": "ZAI"
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
This gives the agent access to tools like `mcp_vision_image_analysis`, `mcp_vision_ui_to_artifact`, `mcp_vision_extract_text_from_screenshot`, etc.
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Bridge — connects external MCP servers to agent-sh.
|
|
3
|
+
*
|
|
4
|
+
* Spawns MCP servers as child processes, discovers their tools,
|
|
5
|
+
* and registers each tool as an agent-sh ToolDefinition.
|
|
6
|
+
*
|
|
7
|
+
* Configure in ~/.agent-sh/settings.json:
|
|
8
|
+
*
|
|
9
|
+
* {
|
|
10
|
+
* "extensions": ["./path/to/mcp-bridge"],
|
|
11
|
+
* "mcp-bridge": {
|
|
12
|
+
* "servers": {
|
|
13
|
+
* "vision": {
|
|
14
|
+
* "command": "npx",
|
|
15
|
+
* "args": ["-y", "@z_ai/mcp-server"],
|
|
16
|
+
* "env": {
|
|
17
|
+
* "Z_AI_API_KEY": "your-key",
|
|
18
|
+
* "Z_AI_MODE": "ZAI"
|
|
19
|
+
* }
|
|
20
|
+
* }
|
|
21
|
+
* }
|
|
22
|
+
* }
|
|
23
|
+
* }
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
27
|
+
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
28
|
+
|
|
29
|
+
interface McpServerConfig {
|
|
30
|
+
command: string;
|
|
31
|
+
args?: string[];
|
|
32
|
+
env?: Record<string, string>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface McpBridgeSettings {
|
|
36
|
+
servers: Record<string, McpServerConfig>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface ConnectedServer {
|
|
40
|
+
name: string;
|
|
41
|
+
client: Client;
|
|
42
|
+
transport: StdioClientTransport;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export default async function activate(ctx: any): Promise<void> {
|
|
46
|
+
const { bus } = ctx;
|
|
47
|
+
|
|
48
|
+
const settings = ctx.getExtensionSettings("mcp-bridge", {
|
|
49
|
+
servers: {},
|
|
50
|
+
}) as McpBridgeSettings;
|
|
51
|
+
|
|
52
|
+
const serverEntries = Object.entries(settings.servers);
|
|
53
|
+
if (serverEntries.length === 0) return;
|
|
54
|
+
|
|
55
|
+
const connected: ConnectedServer[] = [];
|
|
56
|
+
|
|
57
|
+
for (const [name, config] of serverEntries) {
|
|
58
|
+
try {
|
|
59
|
+
const server = await connectServer(name, config, ctx);
|
|
60
|
+
connected.push(server);
|
|
61
|
+
} catch (err: any) {
|
|
62
|
+
bus.emit("ui:info", {
|
|
63
|
+
message: `mcp-bridge: failed to connect "${name}": ${err.message}`,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Clean up on exit
|
|
69
|
+
bus.on("app:quit", () => {
|
|
70
|
+
for (const server of connected) {
|
|
71
|
+
try {
|
|
72
|
+
server.transport.close();
|
|
73
|
+
} catch {}
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function connectServer(
|
|
79
|
+
name: string,
|
|
80
|
+
config: McpServerConfig,
|
|
81
|
+
ctx: any,
|
|
82
|
+
): Promise<ConnectedServer> {
|
|
83
|
+
const transport = new StdioClientTransport({
|
|
84
|
+
command: config.command,
|
|
85
|
+
args: config.args,
|
|
86
|
+
env: { ...process.env, ...config.env } as Record<string, string>,
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const client = new Client({ name: `ash-${name}`, version: "0.1.0" });
|
|
90
|
+
await client.connect(transport);
|
|
91
|
+
|
|
92
|
+
// Discover and register tools
|
|
93
|
+
const { tools } = await client.listTools();
|
|
94
|
+
for (const tool of tools) {
|
|
95
|
+
const toolName = `mcp_${name}_${tool.name}`;
|
|
96
|
+
ctx.registerTool({
|
|
97
|
+
name: toolName,
|
|
98
|
+
displayName: tool.name,
|
|
99
|
+
description: `[${name}] ${tool.description ?? ""}`,
|
|
100
|
+
input_schema: tool.inputSchema as Record<string, unknown>,
|
|
101
|
+
|
|
102
|
+
async execute(args: Record<string, unknown>) {
|
|
103
|
+
try {
|
|
104
|
+
const result = await client.callTool({
|
|
105
|
+
name: tool.name,
|
|
106
|
+
arguments: args,
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
const text = (result.content as any[])
|
|
110
|
+
.map((c: any) => {
|
|
111
|
+
if (c.type === "text") return c.text;
|
|
112
|
+
if (c.type === "image") return `[image: ${c.mimeType}]`;
|
|
113
|
+
return JSON.stringify(c);
|
|
114
|
+
})
|
|
115
|
+
.join("\n");
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
content: text,
|
|
119
|
+
exitCode: result.isError ? 1 : 0,
|
|
120
|
+
isError: !!result.isError,
|
|
121
|
+
};
|
|
122
|
+
} catch (err: any) {
|
|
123
|
+
return {
|
|
124
|
+
content: `MCP error: ${err.message}`,
|
|
125
|
+
exitCode: 1,
|
|
126
|
+
isError: true,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
|
|
131
|
+
getDisplayInfo() {
|
|
132
|
+
return { kind: "execute" as const };
|
|
133
|
+
},
|
|
134
|
+
|
|
135
|
+
formatCall(args: Record<string, unknown>) {
|
|
136
|
+
// Show a compact summary of the args
|
|
137
|
+
const keys = Object.keys(args);
|
|
138
|
+
if (keys.length === 0) return tool.name;
|
|
139
|
+
const first = args[keys[0]];
|
|
140
|
+
const preview =
|
|
141
|
+
typeof first === "string"
|
|
142
|
+
? first.slice(0, 60) + (first.length > 60 ? "…" : "")
|
|
143
|
+
: JSON.stringify(first).slice(0, 60);
|
|
144
|
+
return `${tool.name}: ${preview}`;
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
ctx.bus.emit("ui:info", {
|
|
150
|
+
message: `mcp-bridge: "${name}" connected (${tools.length} tools)`,
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
return { name, client, transport };
|
|
154
|
+
}
|
|
@@ -4,34 +4,36 @@
|
|
|
4
4
|
* Adds permission gates for tool calls and file writes.
|
|
5
5
|
* Without this extension, agent-sh runs in yolo mode (auto-approve).
|
|
6
6
|
*
|
|
7
|
+
* Uses the interactive UI primitive for compositor-aware, themed rendering.
|
|
8
|
+
*
|
|
7
9
|
* Usage:
|
|
8
|
-
*
|
|
9
|
-
* agent-sh --extensions interactive-prompts
|
|
10
|
+
* agent-sh -e ./examples/extensions/interactive-prompts.ts
|
|
10
11
|
*
|
|
11
12
|
* # Or copy to ~/.agent-sh/extensions/ for permanent use:
|
|
12
13
|
* cp examples/extensions/interactive-prompts.ts ~/.agent-sh/extensions/
|
|
13
|
-
*
|
|
14
|
-
* # Or install as an npm package and load by name:
|
|
15
|
-
* agent-sh --extensions my-prompts-package
|
|
16
14
|
*/
|
|
17
15
|
import { renderDiff } from "agent-sh/utils/diff-renderer.js";
|
|
18
16
|
import { renderBoxFrame } from "agent-sh/utils/box-frame.js";
|
|
19
17
|
import { palette as p } from "agent-sh/utils/palette.js";
|
|
20
18
|
import type { ExtensionContext } from "agent-sh/types";
|
|
19
|
+
import type { ToolUI } from "agent-sh/agent/types.js";
|
|
21
20
|
|
|
22
21
|
export default function activate({ bus }: ExtensionContext) {
|
|
23
22
|
let autoApproveWrites = false;
|
|
24
23
|
|
|
25
24
|
bus.onPipeAsync("permission:request", async (payload) => {
|
|
25
|
+
const ui = payload.ui as ToolUI | undefined;
|
|
26
|
+
if (!ui) return payload;
|
|
27
|
+
|
|
26
28
|
switch (payload.kind) {
|
|
27
29
|
case "tool-call":
|
|
28
|
-
return
|
|
30
|
+
return handleToolCall(payload, ui);
|
|
29
31
|
case "file-write": {
|
|
30
32
|
if (autoApproveWrites) {
|
|
31
|
-
return { ...payload, decision: {
|
|
33
|
+
return { ...payload, decision: { outcome: "approved" } };
|
|
32
34
|
}
|
|
33
|
-
const result = await
|
|
34
|
-
if (result.decision.autoApprove) {
|
|
35
|
+
const result = await handleFileWrite(payload, ui);
|
|
36
|
+
if ((result.decision as any).autoApprove) {
|
|
35
37
|
autoApproveWrites = true;
|
|
36
38
|
}
|
|
37
39
|
return result;
|
|
@@ -42,120 +44,90 @@ export default function activate({ bus }: ExtensionContext) {
|
|
|
42
44
|
});
|
|
43
45
|
}
|
|
44
46
|
|
|
45
|
-
async function
|
|
47
|
+
async function handleToolCall(payload: any, ui: ToolUI) {
|
|
46
48
|
const options = payload.metadata.options;
|
|
47
|
-
|
|
49
|
+
|
|
50
|
+
const answer = await ui.custom<"approve" | "approve_all" | "deny">({
|
|
51
|
+
render(width) {
|
|
52
|
+
const boxW = Math.min(84, width);
|
|
53
|
+
return renderBoxFrame(
|
|
54
|
+
[`${p.bold}⚠ ${payload.title}${p.reset}`],
|
|
55
|
+
{
|
|
56
|
+
width: boxW,
|
|
57
|
+
style: "rounded",
|
|
58
|
+
borderColor: p.warning,
|
|
59
|
+
title: "Permission required",
|
|
60
|
+
footer: [` ${p.dim}[y]es / [n]o / [a]llow all${p.reset}`],
|
|
61
|
+
},
|
|
62
|
+
);
|
|
63
|
+
},
|
|
64
|
+
handleInput(data, done) {
|
|
65
|
+
const ch = data.toLowerCase();
|
|
66
|
+
if (ch === "y") done("approve");
|
|
67
|
+
else if (ch === "a") done("approve_all");
|
|
68
|
+
else if (ch === "n" || ch === "\x1b") done("deny");
|
|
69
|
+
},
|
|
70
|
+
});
|
|
48
71
|
|
|
49
72
|
if (answer === "approve" || answer === "approve_all") {
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
73
|
+
const kind = answer === "approve_all" ? "allow_always" : "allow_once";
|
|
74
|
+
const option = options?.find((o: any) => o.kind === kind)
|
|
75
|
+
?? options?.find((o: any) => o.kind === "allow_once" || o.kind === "allow_always");
|
|
53
76
|
if (option) {
|
|
54
77
|
return { ...payload, decision: { outcome: "selected", optionId: option.optionId } };
|
|
55
78
|
}
|
|
79
|
+
return { ...payload, decision: { outcome: "approved" } };
|
|
56
80
|
}
|
|
57
81
|
return { ...payload, decision: { outcome: "cancelled" } };
|
|
58
82
|
}
|
|
59
83
|
|
|
60
|
-
async function
|
|
84
|
+
async function handleFileWrite(payload: any, ui: ToolUI) {
|
|
61
85
|
const diff = payload.metadata.diff;
|
|
62
|
-
const filePath = payload.metadata.path;
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
86
|
+
const filePath = payload.metadata.path ?? payload.title;
|
|
87
|
+
|
|
88
|
+
const answer = await ui.custom<"approve" | "approve_all" | "reject">({
|
|
89
|
+
render(width) {
|
|
90
|
+
const boxW = Math.min(84, width);
|
|
91
|
+
const contentW = boxW - 4;
|
|
92
|
+
const MAX_DISPLAY = 25;
|
|
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, {
|
|
111
|
+
width: boxW,
|
|
112
|
+
style: "rounded",
|
|
113
|
+
borderColor: p.warning,
|
|
114
|
+
title,
|
|
115
|
+
footer: [` ${p.bold}[y] Apply [n] Skip [a] Don't ask again${p.reset}`],
|
|
116
|
+
});
|
|
117
|
+
},
|
|
118
|
+
handleInput(data, done) {
|
|
119
|
+
const ch = data.toLowerCase();
|
|
120
|
+
if (ch === "y") done("approve");
|
|
121
|
+
else if (ch === "a") done("approve_all");
|
|
122
|
+
else if (ch === "n" || ch === "\x1b") done("reject");
|
|
85
123
|
},
|
|
86
|
-
);
|
|
87
|
-
|
|
88
|
-
process.stdout.write("\n");
|
|
89
|
-
for (const line of framed) {
|
|
90
|
-
process.stdout.write(line + "\n");
|
|
91
|
-
}
|
|
92
|
-
process.stdout.write(" ");
|
|
93
|
-
|
|
94
|
-
return new Promise((resolve) => {
|
|
95
|
-
const handler = (data) => {
|
|
96
|
-
const ch = data.toString("utf-8").toLowerCase();
|
|
97
|
-
process.stdin.removeListener("data", handler);
|
|
98
|
-
process.stdout.write("\n");
|
|
99
|
-
|
|
100
|
-
if (ch === "y") resolve("approve");
|
|
101
|
-
else if (ch === "a") resolve("approve_all");
|
|
102
|
-
else resolve(null);
|
|
103
|
-
};
|
|
104
|
-
process.stdin.on("data", handler);
|
|
105
|
-
});
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
async function previewDiff(opts) {
|
|
109
|
-
const termW = process.stdout.columns || 80;
|
|
110
|
-
const boxW = Math.min(84, termW);
|
|
111
|
-
const contentW = boxW - 4;
|
|
112
|
-
const MAX_DISPLAY = 25;
|
|
113
|
-
|
|
114
|
-
const stats = opts.diff.isNewFile
|
|
115
|
-
? `(+${opts.diff.added} lines)`
|
|
116
|
-
: `(+${opts.diff.added} / -${opts.diff.removed})`;
|
|
117
|
-
const title = opts.diff.isNewFile
|
|
118
|
-
? `new: ${opts.path} ${stats}`
|
|
119
|
-
: `${opts.path} ${stats}`;
|
|
120
|
-
|
|
121
|
-
const diffLines = renderDiff(opts.diff, {
|
|
122
|
-
width: contentW,
|
|
123
|
-
filePath: opts.path,
|
|
124
|
-
maxLines: MAX_DISPLAY,
|
|
125
|
-
trueColor: true,
|
|
126
|
-
mode: "unified",
|
|
127
|
-
});
|
|
128
|
-
const content = ["", ...diffLines.slice(1), ""];
|
|
129
|
-
|
|
130
|
-
const framed = renderBoxFrame(content, {
|
|
131
|
-
width: boxW,
|
|
132
|
-
style: "rounded",
|
|
133
|
-
borderColor: p.warning,
|
|
134
|
-
title,
|
|
135
|
-
footer: [` ${p.bold}[y] Apply [n] Skip [a] Don't ask again${p.reset}`],
|
|
136
124
|
});
|
|
137
125
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
process.stdout.write(line + "\n");
|
|
126
|
+
if (answer === "approve") {
|
|
127
|
+
return { ...payload, decision: { outcome: "approved" } };
|
|
141
128
|
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
process.stdin.removeListener("data", handler);
|
|
147
|
-
|
|
148
|
-
if (ch === "y") {
|
|
149
|
-
process.stdout.write(` ${p.success}✓ Applied${p.reset}\n`);
|
|
150
|
-
resolve("approve");
|
|
151
|
-
} else if (ch === "a") {
|
|
152
|
-
process.stdout.write(` ${p.success}✓ Applied (auto-approve on)${p.reset}\n`);
|
|
153
|
-
resolve("approve_all");
|
|
154
|
-
} else {
|
|
155
|
-
process.stdout.write(` ${p.error}✗ Skipped${p.reset}\n`);
|
|
156
|
-
resolve("reject");
|
|
157
|
-
}
|
|
158
|
-
};
|
|
159
|
-
process.stdin.on("data", handler);
|
|
160
|
-
});
|
|
129
|
+
if (answer === "approve_all") {
|
|
130
|
+
return { ...payload, decision: { outcome: "approved", autoApprove: true } };
|
|
131
|
+
}
|
|
132
|
+
return { ...payload, decision: { outcome: "cancelled" } };
|
|
161
133
|
}
|
|
@@ -5,66 +5,112 @@
|
|
|
5
5
|
* inside vim, htop, or ssh. Composites a floating response box on top
|
|
6
6
|
* of the current terminal content.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
8
|
+
* Uses createRemoteSession() to route the full tui-renderer pipeline
|
|
9
|
+
* (markdown, tool grouping, spinner, diffs) into the floating panel.
|
|
10
|
+
*
|
|
11
|
+
* Install:
|
|
12
|
+
* cp examples/extensions/overlay-agent.ts ~/.agent-sh/extensions/
|
|
9
13
|
*
|
|
10
|
-
*
|
|
14
|
+
* Or load directly:
|
|
11
15
|
* agent-sh -e ./examples/extensions/overlay-agent.ts
|
|
12
16
|
*
|
|
13
|
-
*
|
|
14
|
-
* cp examples/extensions/overlay-agent.ts ~/.agent-sh/extensions/
|
|
17
|
+
* Requires: npm install @xterm/headless@5.5.0 @xterm/addon-serialize@0.13.0
|
|
15
18
|
*/
|
|
16
|
-
import type { ExtensionContext } from "
|
|
17
|
-
import {
|
|
19
|
+
import type { ExtensionContext, RemoteSession } from "../../src/types.js";
|
|
20
|
+
import type { RenderSurface } from "../../src/utils/compositor.js";
|
|
21
|
+
import { FloatingPanel } from "../../src/utils/floating-panel.js";
|
|
18
22
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
23
|
+
/** Adapt a FloatingPanel to the RenderSurface interface. */
|
|
24
|
+
function createPanelSurface(panel: FloatingPanel): RenderSurface {
|
|
25
|
+
return {
|
|
26
|
+
write(text: string): void {
|
|
27
|
+
// Handle \r (carriage return) — overwrite the current line.
|
|
28
|
+
// The spinner uses "\r <content>\x1b[K" to update in-place.
|
|
29
|
+
if (text.startsWith("\r")) {
|
|
30
|
+
// Strip \r and any erase-line sequences
|
|
31
|
+
const cleaned = text.replace(/^\r/, "").replace(/\x1b\[\d*K/g, "");
|
|
32
|
+
if (cleaned.trim()) {
|
|
33
|
+
panel.updateLastLine(() => cleaned);
|
|
34
|
+
}
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Regular text — may contain newlines
|
|
39
|
+
panel.appendText(text);
|
|
40
|
+
},
|
|
41
|
+
writeLine(line: string): void {
|
|
42
|
+
panel.appendLine(line);
|
|
43
|
+
},
|
|
44
|
+
get columns(): number {
|
|
45
|
+
return panel.computeGeometry().contentW;
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
}
|
|
22
49
|
|
|
23
|
-
export default function activate(
|
|
24
|
-
const
|
|
50
|
+
export default function activate(ctx: ExtensionContext): void {
|
|
51
|
+
const { bus, registerInstruction, createRemoteSession, terminalBuffer } = ctx;
|
|
52
|
+
|
|
53
|
+
const panel = new FloatingPanel(bus, {
|
|
25
54
|
trigger: "\x1c", // Ctrl+\
|
|
26
55
|
dimBackground: true,
|
|
27
|
-
|
|
56
|
+
terminalBuffer: terminalBuffer ?? undefined,
|
|
28
57
|
});
|
|
29
58
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
59
|
+
const panelSurface = createPanelSurface(panel);
|
|
60
|
+
let session: RemoteSession | null = null;
|
|
61
|
+
|
|
62
|
+
registerInstruction("Interactive Overlay Sessions", [
|
|
63
|
+
"When the dynamic context includes `interactive-session: true`, the user has summoned you",
|
|
64
|
+
"via a hotkey overlay from inside their live terminal. They may be in the middle of using",
|
|
65
|
+
"a program (vim, ssh, a REPL, etc.) or at a shell prompt. In this mode:",
|
|
66
|
+
"- Start with terminal_read if you need to understand what's on screen.",
|
|
67
|
+
"- Prefer terminal_keys to interact with whatever is currently running.",
|
|
68
|
+
"- Use user_shell only for running new, standalone commands — not for interacting with",
|
|
69
|
+
" what's already on screen.",
|
|
70
|
+
"- Keep responses concise — the user is in the middle of a workflow.",
|
|
71
|
+
].join("\n"));
|
|
72
|
+
|
|
73
|
+
// ── Panel lifecycle ────────────────────────────────────────────
|
|
36
74
|
|
|
37
|
-
// ── Panel lifecycle ────────────────────────────────────────
|
|
38
75
|
panel.handlers.advise("panel:submit", (_next, query: string) => {
|
|
76
|
+
if (!session) {
|
|
77
|
+
session = createRemoteSession({
|
|
78
|
+
surface: panelSurface,
|
|
79
|
+
suppressQueryBox: true,
|
|
80
|
+
interactive: true,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
39
83
|
panel.setActive();
|
|
40
|
-
|
|
41
|
-
panel.appendLine("");
|
|
42
|
-
bus.emit("agent:submit", { query });
|
|
84
|
+
session.submit(query);
|
|
43
85
|
});
|
|
44
86
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
if (
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
87
|
+
panel.handlers.advise("panel:show", (_next) => {
|
|
88
|
+
// Re-establish session if panel is shown while agent is still working
|
|
89
|
+
if (panel.active && !session) {
|
|
90
|
+
session = createRemoteSession({
|
|
91
|
+
surface: panelSurface,
|
|
92
|
+
suppressQueryBox: true,
|
|
93
|
+
interactive: true,
|
|
94
|
+
});
|
|
52
95
|
}
|
|
53
96
|
});
|
|
54
97
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
98
|
+
// On dismiss: close session only if agent is not actively processing.
|
|
99
|
+
// If agent is still working (phase="active"), keep session alive so
|
|
100
|
+
// output buffers in the panel and agent can keep executing tools.
|
|
101
|
+
panel.handlers.advise("panel:dismiss", (next) => {
|
|
102
|
+
next();
|
|
103
|
+
if (session && !panel.processing) {
|
|
104
|
+
session.close();
|
|
105
|
+
session = null;
|
|
106
|
+
}
|
|
64
107
|
});
|
|
65
108
|
|
|
66
109
|
bus.on("agent:processing-done", () => {
|
|
67
110
|
if (!panel.active) return;
|
|
68
111
|
panel.setDone();
|
|
112
|
+
// If panel was hidden while processing (passthrough), setDone()
|
|
113
|
+
// triggers dismiss() which closes the session above.
|
|
114
|
+
// If panel is still visible, session stays for the follow-up prompt.
|
|
69
115
|
});
|
|
70
116
|
}
|