agent-sh 0.1.0 → 0.3.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 +66 -576
- package/dist/acp-client.d.ts +24 -0
- package/dist/acp-client.js +168 -35
- package/dist/context-manager.d.ts +6 -4
- package/dist/context-manager.js +75 -44
- package/dist/event-bus.d.ts +29 -0
- package/dist/extension-loader.js +3 -14
- package/dist/extensions/shell-exec.d.ts +24 -0
- package/dist/extensions/shell-exec.js +188 -0
- package/dist/extensions/tui-renderer.d.ts +1 -1
- package/dist/extensions/tui-renderer.js +133 -28
- package/dist/index.js +195 -6
- package/dist/input-handler.d.ts +13 -3
- package/dist/input-handler.js +259 -127
- package/dist/mcp-server.d.ts +13 -0
- package/dist/mcp-server.js +234 -0
- package/dist/output-parser.d.ts +5 -26
- package/dist/output-parser.js +16 -78
- package/dist/settings.d.ts +33 -0
- package/dist/settings.js +43 -0
- package/dist/shell.d.ts +9 -4
- package/dist/shell.js +88 -10
- package/dist/types.d.ts +4 -0
- package/dist/utils/ansi.d.ts +4 -1
- package/dist/utils/ansi.js +60 -2
- package/dist/utils/line-editor.d.ts +59 -0
- package/dist/utils/line-editor.js +381 -0
- package/dist/utils/markdown.js +4 -4
- package/dist/utils/tool-display.d.ts +11 -0
- package/dist/utils/tool-display.js +92 -9
- package/examples/pi-agent-sh.ts +166 -0
- package/package.json +1 -1
package/dist/event-bus.d.ts
CHANGED
|
@@ -18,6 +18,8 @@ export interface ShellEvents {
|
|
|
18
18
|
"shell:foreground-busy": {
|
|
19
19
|
busy: boolean;
|
|
20
20
|
};
|
|
21
|
+
"shell:agent-exec-start": Record<string, never>;
|
|
22
|
+
"shell:agent-exec-done": Record<string, never>;
|
|
21
23
|
"agent:submit": {
|
|
22
24
|
query: string;
|
|
23
25
|
};
|
|
@@ -52,10 +54,17 @@ export interface ShellEvents {
|
|
|
52
54
|
"agent:tool-started": {
|
|
53
55
|
title: string;
|
|
54
56
|
toolCallId?: string;
|
|
57
|
+
kind?: string;
|
|
58
|
+
locations?: {
|
|
59
|
+
path: string;
|
|
60
|
+
line?: number | null;
|
|
61
|
+
}[];
|
|
62
|
+
rawInput?: unknown;
|
|
55
63
|
};
|
|
56
64
|
"agent:tool-completed": {
|
|
57
65
|
toolCallId?: string;
|
|
58
66
|
exitCode: number | null;
|
|
67
|
+
rawOutput?: unknown;
|
|
59
68
|
};
|
|
60
69
|
"agent:tool-output-chunk": {
|
|
61
70
|
chunk: string;
|
|
@@ -89,6 +98,26 @@ export interface ShellEvents {
|
|
|
89
98
|
cwd: string;
|
|
90
99
|
handled: boolean;
|
|
91
100
|
};
|
|
101
|
+
"shell:exec-request": {
|
|
102
|
+
command: string;
|
|
103
|
+
output: string;
|
|
104
|
+
cwd: string;
|
|
105
|
+
done: boolean;
|
|
106
|
+
};
|
|
107
|
+
"session:configure": {
|
|
108
|
+
cwd: string;
|
|
109
|
+
mcpServers: {
|
|
110
|
+
name: string;
|
|
111
|
+
command: string;
|
|
112
|
+
args: string[];
|
|
113
|
+
env: {
|
|
114
|
+
name: string;
|
|
115
|
+
value: string;
|
|
116
|
+
}[];
|
|
117
|
+
}[];
|
|
118
|
+
};
|
|
119
|
+
"config:changed": Record<string, never>;
|
|
120
|
+
"config:cycle": Record<string, never>;
|
|
92
121
|
"autocomplete:request": {
|
|
93
122
|
buffer: string;
|
|
94
123
|
items: {
|
package/dist/extension-loader.js
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import * as fs from "node:fs/promises";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
-
import
|
|
4
|
-
const CONFIG_DIR = path.join(os.homedir(), ".agent-sh");
|
|
3
|
+
import { CONFIG_DIR, getSettings } from "./settings.js";
|
|
5
4
|
const EXT_DIR = path.join(CONFIG_DIR, "extensions");
|
|
6
|
-
const SETTINGS_PATH = path.join(CONFIG_DIR, "settings.json");
|
|
7
5
|
const TS_EXTS = [".ts", ".tsx", ".mts"];
|
|
8
6
|
const SCRIPT_EXTS = [".js", ".mjs", ".ts", ".tsx", ".mts"];
|
|
9
7
|
let tsRegistered = false;
|
|
@@ -19,15 +17,6 @@ async function ensureTsSupport() {
|
|
|
19
17
|
// tsx not available — TS extensions will fail with a clear error
|
|
20
18
|
}
|
|
21
19
|
}
|
|
22
|
-
async function loadSettings() {
|
|
23
|
-
try {
|
|
24
|
-
const raw = await fs.readFile(SETTINGS_PATH, "utf-8");
|
|
25
|
-
return JSON.parse(raw);
|
|
26
|
-
}
|
|
27
|
-
catch {
|
|
28
|
-
return {};
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
20
|
/**
|
|
32
21
|
* Load extensions from three sources (merged, deduplicated):
|
|
33
22
|
*
|
|
@@ -49,8 +38,8 @@ export async function loadExtensions(ctx, cliExtensions) {
|
|
|
49
38
|
specifiers.push(...cliExtensions);
|
|
50
39
|
}
|
|
51
40
|
// 2. settings.json
|
|
52
|
-
const settings =
|
|
53
|
-
if (settings.extensions) {
|
|
41
|
+
const settings = getSettings();
|
|
42
|
+
if (settings.extensions.length > 0) {
|
|
54
43
|
specifiers.push(...settings.extensions);
|
|
55
44
|
}
|
|
56
45
|
// 3. ~/.agent-sh/extensions/ directory
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shell exec extension.
|
|
3
|
+
*
|
|
4
|
+
* Runs a Unix domain socket server speaking JSON-RPC 2.0 that external
|
|
5
|
+
* tools (MCP server, pi extensions, etc.) connect to for interacting
|
|
6
|
+
* with the user's live PTY shell.
|
|
7
|
+
*
|
|
8
|
+
* Also registers the MCP server via the `session:configure` pipe so
|
|
9
|
+
* ACP agents discover the `user_shell` tool automatically.
|
|
10
|
+
*
|
|
11
|
+
* This extension has no direct PTY or Shell knowledge — it communicates
|
|
12
|
+
* exclusively through the bus, following the headless-core philosophy.
|
|
13
|
+
*
|
|
14
|
+
* ## Socket protocol (JSON-RPC 2.0, newline-delimited)
|
|
15
|
+
*
|
|
16
|
+
* shell/exec { command: string } → { output, cwd }
|
|
17
|
+
* shell/cwd {} → { cwd }
|
|
18
|
+
* shell/info {} → { busy, shell }
|
|
19
|
+
* shell/recall { operation, ... } → { result }
|
|
20
|
+
*/
|
|
21
|
+
import type { ExtensionContext } from "../types.js";
|
|
22
|
+
export default function activate({ bus, contextManager }: ExtensionContext, opts: {
|
|
23
|
+
socketPath: string;
|
|
24
|
+
}): void;
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shell exec extension.
|
|
3
|
+
*
|
|
4
|
+
* Runs a Unix domain socket server speaking JSON-RPC 2.0 that external
|
|
5
|
+
* tools (MCP server, pi extensions, etc.) connect to for interacting
|
|
6
|
+
* with the user's live PTY shell.
|
|
7
|
+
*
|
|
8
|
+
* Also registers the MCP server via the `session:configure` pipe so
|
|
9
|
+
* ACP agents discover the `user_shell` tool automatically.
|
|
10
|
+
*
|
|
11
|
+
* This extension has no direct PTY or Shell knowledge — it communicates
|
|
12
|
+
* exclusively through the bus, following the headless-core philosophy.
|
|
13
|
+
*
|
|
14
|
+
* ## Socket protocol (JSON-RPC 2.0, newline-delimited)
|
|
15
|
+
*
|
|
16
|
+
* shell/exec { command: string } → { output, cwd }
|
|
17
|
+
* shell/cwd {} → { cwd }
|
|
18
|
+
* shell/info {} → { busy, shell }
|
|
19
|
+
* shell/recall { operation, ... } → { result }
|
|
20
|
+
*/
|
|
21
|
+
import * as net from "node:net";
|
|
22
|
+
import * as fs from "node:fs";
|
|
23
|
+
import * as path from "node:path";
|
|
24
|
+
import { fileURLToPath } from "node:url";
|
|
25
|
+
import { getSettings } from "../settings.js";
|
|
26
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
27
|
+
export default function activate({ bus, contextManager }, opts) {
|
|
28
|
+
const { socketPath } = opts;
|
|
29
|
+
// Register MCP server so ACP agents discover the bridge tools.
|
|
30
|
+
// Agents that don't support MCP (e.g. pi-acp) simply ignore it.
|
|
31
|
+
// Can be disabled via settings.json if not needed.
|
|
32
|
+
if (getSettings().enableMcp) {
|
|
33
|
+
bus.onPipe("session:configure", (payload) => {
|
|
34
|
+
return {
|
|
35
|
+
...payload,
|
|
36
|
+
mcpServers: [
|
|
37
|
+
...payload.mcpServers,
|
|
38
|
+
{
|
|
39
|
+
name: "agent-sh",
|
|
40
|
+
command: process.execPath,
|
|
41
|
+
args: [path.join(__dirname, "..", "mcp-server.js")],
|
|
42
|
+
env: [{ name: "AGENT_SH_SOCKET", value: socketPath }],
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
};
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
// Set AGENT_SH_SOCKET for agent extensions that connect directly
|
|
49
|
+
process.env.AGENT_SH_SOCKET = socketPath;
|
|
50
|
+
// Serialize shell/exec requests — only one PTY command at a time
|
|
51
|
+
let execPending = Promise.resolve();
|
|
52
|
+
// ── JSON-RPC handler ────────────────────────────────────────────
|
|
53
|
+
async function handleRequest(method, params) {
|
|
54
|
+
switch (method) {
|
|
55
|
+
case "shell/exec": {
|
|
56
|
+
const command = params?.command;
|
|
57
|
+
if (typeof command !== "string" || !command) {
|
|
58
|
+
throw rpcError(-32602, "Missing required parameter: command");
|
|
59
|
+
}
|
|
60
|
+
// Serialize — one PTY command at a time
|
|
61
|
+
return new Promise((resolve, reject) => {
|
|
62
|
+
execPending = execPending.then(async () => {
|
|
63
|
+
try {
|
|
64
|
+
bus.emit("shell:agent-exec-start", {});
|
|
65
|
+
const result = await bus.emitPipeAsync("shell:exec-request", {
|
|
66
|
+
command,
|
|
67
|
+
output: "",
|
|
68
|
+
cwd: "",
|
|
69
|
+
done: false,
|
|
70
|
+
});
|
|
71
|
+
bus.emit("shell:agent-exec-done", {});
|
|
72
|
+
resolve({ output: result.output, cwd: result.cwd });
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
bus.emit("shell:agent-exec-done", {});
|
|
76
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
77
|
+
reject(rpcError(-32000, message));
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
case "shell/cwd":
|
|
83
|
+
return { cwd: contextManager.getCwd() };
|
|
84
|
+
case "shell/info":
|
|
85
|
+
return {
|
|
86
|
+
shell: process.env.SHELL || "unknown",
|
|
87
|
+
agentSh: true,
|
|
88
|
+
};
|
|
89
|
+
case "shell/recall": {
|
|
90
|
+
const operation = params?.operation || "browse";
|
|
91
|
+
switch (operation) {
|
|
92
|
+
case "search": {
|
|
93
|
+
const query = params?.query;
|
|
94
|
+
if (typeof query !== "string" || !query) {
|
|
95
|
+
throw rpcError(-32602, "Missing required parameter: query");
|
|
96
|
+
}
|
|
97
|
+
return { result: contextManager.search(query) };
|
|
98
|
+
}
|
|
99
|
+
case "expand": {
|
|
100
|
+
const ids = params?.ids;
|
|
101
|
+
if (!Array.isArray(ids) || ids.length === 0) {
|
|
102
|
+
throw rpcError(-32602, "Missing required parameter: ids (array of numbers)");
|
|
103
|
+
}
|
|
104
|
+
const start = typeof params?.start === "number" ? params.start : undefined;
|
|
105
|
+
const end = typeof params?.end === "number" ? params.end : undefined;
|
|
106
|
+
return { result: contextManager.expand(ids.map(Number), start, end) };
|
|
107
|
+
}
|
|
108
|
+
case "browse":
|
|
109
|
+
return { result: contextManager.getRecentSummary() };
|
|
110
|
+
default:
|
|
111
|
+
throw rpcError(-32602, `Unknown recall operation: ${operation}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
default:
|
|
115
|
+
throw rpcError(-32601, `Method not found: ${method}`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
// ── Socket server ───────────────────────────────────────────────
|
|
119
|
+
const server = net.createServer((conn) => {
|
|
120
|
+
let buffer = "";
|
|
121
|
+
conn.on("data", (chunk) => {
|
|
122
|
+
buffer += chunk.toString();
|
|
123
|
+
// Process complete lines (newline-delimited JSON-RPC)
|
|
124
|
+
let newlineIdx;
|
|
125
|
+
while ((newlineIdx = buffer.indexOf("\n")) !== -1) {
|
|
126
|
+
const line = buffer.slice(0, newlineIdx).trim();
|
|
127
|
+
buffer = buffer.slice(newlineIdx + 1);
|
|
128
|
+
if (!line)
|
|
129
|
+
continue;
|
|
130
|
+
processMessage(conn, line);
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
function processMessage(conn, line) {
|
|
135
|
+
let id = null;
|
|
136
|
+
try {
|
|
137
|
+
const msg = JSON.parse(line);
|
|
138
|
+
id = msg.id ?? null;
|
|
139
|
+
const method = msg.method;
|
|
140
|
+
if (!method) {
|
|
141
|
+
sendError(conn, id, -32600, "Invalid request: missing method");
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
handleRequest(method, msg.params)
|
|
145
|
+
.then((result) => sendResult(conn, id, result))
|
|
146
|
+
.catch((err) => {
|
|
147
|
+
if (err && typeof err === "object" && "rpcCode" in err) {
|
|
148
|
+
sendError(conn, id, err.rpcCode, err.message);
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
sendError(conn, id, -32603, String(err));
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
sendError(conn, id, -32700, "Parse error");
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
// Clean up stale socket file
|
|
160
|
+
try {
|
|
161
|
+
fs.unlinkSync(socketPath);
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
// Doesn't exist — fine
|
|
165
|
+
}
|
|
166
|
+
server.listen(socketPath);
|
|
167
|
+
// Cleanup on exit
|
|
168
|
+
const cleanup = () => {
|
|
169
|
+
server.close();
|
|
170
|
+
try {
|
|
171
|
+
fs.unlinkSync(socketPath);
|
|
172
|
+
}
|
|
173
|
+
catch { }
|
|
174
|
+
};
|
|
175
|
+
process.on("exit", cleanup);
|
|
176
|
+
}
|
|
177
|
+
// ── JSON-RPC helpers ──────────────────────────────────────────────
|
|
178
|
+
function sendResult(conn, id, result) {
|
|
179
|
+
conn.write(JSON.stringify({ jsonrpc: "2.0", id, result }) + "\n");
|
|
180
|
+
}
|
|
181
|
+
function sendError(conn, id, code, message) {
|
|
182
|
+
conn.write(JSON.stringify({ jsonrpc: "2.0", id, error: { code, message } }) + "\n");
|
|
183
|
+
}
|
|
184
|
+
function rpcError(code, message) {
|
|
185
|
+
const err = new Error(message);
|
|
186
|
+
err.rpcCode = code;
|
|
187
|
+
return err;
|
|
188
|
+
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import type { ExtensionContext } from "../types.js";
|
|
2
|
-
export default function activate({ bus }: ExtensionContext): void;
|
|
2
|
+
export default function activate({ bus, getAcpClient }: ExtensionContext): void;
|
|
@@ -12,22 +12,27 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import { MarkdownRenderer } from "../utils/markdown.js";
|
|
14
14
|
import { palette as p } from "../utils/palette.js";
|
|
15
|
-
import { renderToolCall,
|
|
15
|
+
import { renderToolCall, startSpinner, stopSpinner as stopToolSpinner, } from "../utils/tool-display.js";
|
|
16
16
|
import { renderDiff } from "../utils/diff-renderer.js";
|
|
17
17
|
import { renderBoxFrame } from "../utils/box-frame.js";
|
|
18
|
-
|
|
19
|
-
export default function activate({ bus }) {
|
|
18
|
+
import { getSettings } from "../settings.js";
|
|
19
|
+
export default function activate({ bus, getAcpClient }) {
|
|
20
20
|
let spinner = null;
|
|
21
21
|
let renderer = null;
|
|
22
22
|
let commandOutputBuffer = "";
|
|
23
23
|
let commandOutputLineCount = 0;
|
|
24
24
|
let commandOutputOverflow = 0;
|
|
25
25
|
let lastCommand = "";
|
|
26
|
+
let toolLineOpen = false; // true when tool header was written without \n
|
|
27
|
+
let hadToolCalls = false; // true after any tool call in current response
|
|
28
|
+
let currentToolKind; // kind of the currently executing tool
|
|
26
29
|
let isThinking = false;
|
|
27
30
|
let showThinkingText = false;
|
|
31
|
+
let spinnerStartTime = 0; // preserved across spinner restarts
|
|
28
32
|
let lastTruncatedDiff = null;
|
|
29
33
|
// ── Event subscriptions ─────────────────────────────────────
|
|
30
34
|
bus.on("agent:query", (e) => {
|
|
35
|
+
spinnerStartTime = 0;
|
|
31
36
|
showUserQuery(e.query);
|
|
32
37
|
startAgentResponse();
|
|
33
38
|
startThinkingSpinner();
|
|
@@ -35,14 +40,15 @@ export default function activate({ bus }) {
|
|
|
35
40
|
bus.on("agent:thinking-chunk", (e) => {
|
|
36
41
|
if (!isThinking) {
|
|
37
42
|
isThinking = true;
|
|
38
|
-
stopCurrentSpinner();
|
|
39
43
|
if (showThinkingText) {
|
|
44
|
+
stopCurrentSpinner();
|
|
40
45
|
if (!renderer)
|
|
41
46
|
startAgentResponse();
|
|
42
|
-
renderer.writeLine(`${p.dim}
|
|
47
|
+
renderer.writeLine(`${p.dim}Thinking (ctrl+t to collapse)${p.reset}`);
|
|
43
48
|
}
|
|
44
49
|
else {
|
|
45
|
-
|
|
50
|
+
// Restart spinner with ctrl+t hint now that we know thinking is available
|
|
51
|
+
startThinkingSpinner();
|
|
46
52
|
}
|
|
47
53
|
}
|
|
48
54
|
if (showThinkingText && e.text) {
|
|
@@ -62,10 +68,28 @@ export default function activate({ bus }) {
|
|
|
62
68
|
});
|
|
63
69
|
bus.on("agent:tool-started", (e) => {
|
|
64
70
|
stopCurrentSpinner();
|
|
65
|
-
|
|
71
|
+
currentToolKind = e.kind;
|
|
72
|
+
if (e.title === "user_shell") {
|
|
73
|
+
// Minimal annotation — PTY echo will show the output
|
|
74
|
+
closeToolLine();
|
|
75
|
+
if (!renderer)
|
|
76
|
+
startAgentResponse();
|
|
77
|
+
renderer.flush();
|
|
78
|
+
const cmd = e.rawInput?.command || "";
|
|
79
|
+
renderer.writeLine(`${p.dim}▶ user_shell: ${cmd}${p.reset}`);
|
|
80
|
+
hadToolCalls = true;
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
showToolCall(e.title, lastCommand, e);
|
|
84
|
+
}
|
|
66
85
|
lastCommand = "";
|
|
67
86
|
});
|
|
68
|
-
bus.on("agent:tool-completed", (e) =>
|
|
87
|
+
bus.on("agent:tool-completed", (e) => {
|
|
88
|
+
showToolComplete(e.exitCode);
|
|
89
|
+
currentToolKind = undefined;
|
|
90
|
+
spinnerStartTime = 0;
|
|
91
|
+
startThinkingSpinner();
|
|
92
|
+
});
|
|
69
93
|
bus.on("agent:tool-output-chunk", (e) => writeCommandOutput(e.chunk));
|
|
70
94
|
bus.on("agent:tool-output", () => flushCommandOutput());
|
|
71
95
|
bus.on("agent:cancelled", () => {
|
|
@@ -74,6 +98,11 @@ export default function activate({ bus }) {
|
|
|
74
98
|
showInfo("(cancelled)");
|
|
75
99
|
endAgentResponse();
|
|
76
100
|
});
|
|
101
|
+
bus.on("agent:processing-done", () => {
|
|
102
|
+
isThinking = false;
|
|
103
|
+
stopCurrentSpinner();
|
|
104
|
+
endAgentResponse();
|
|
105
|
+
});
|
|
77
106
|
bus.on("agent:error", (e) => showError(e.message));
|
|
78
107
|
// Flush rendering state and show inline diff for file writes
|
|
79
108
|
bus.on("permission:request", (e) => {
|
|
@@ -108,10 +137,11 @@ export default function activate({ bus }) {
|
|
|
108
137
|
}
|
|
109
138
|
function startAgentResponse() {
|
|
110
139
|
renderer = new MarkdownRenderer();
|
|
111
|
-
|
|
140
|
+
hadToolCalls = false;
|
|
112
141
|
renderer.printTopBorder();
|
|
113
142
|
}
|
|
114
143
|
function endAgentResponse() {
|
|
144
|
+
closeToolLine();
|
|
115
145
|
if (renderer) {
|
|
116
146
|
renderer.flush();
|
|
117
147
|
renderer.printBottomBorder();
|
|
@@ -154,6 +184,9 @@ export default function activate({ bus }) {
|
|
|
154
184
|
}
|
|
155
185
|
}
|
|
156
186
|
function writeAgentText(text) {
|
|
187
|
+
closeToolLine();
|
|
188
|
+
const needsGap = hadToolCalls;
|
|
189
|
+
hadToolCalls = false;
|
|
157
190
|
if (isThinking) {
|
|
158
191
|
isThinking = false;
|
|
159
192
|
if (showThinkingText && renderer) {
|
|
@@ -166,19 +199,34 @@ export default function activate({ bus }) {
|
|
|
166
199
|
stopCurrentSpinner();
|
|
167
200
|
if (!renderer)
|
|
168
201
|
startAgentResponse();
|
|
202
|
+
if (needsGap)
|
|
203
|
+
process.stdout.write("\n");
|
|
169
204
|
renderer.push(text);
|
|
170
205
|
flushOutput();
|
|
171
206
|
}
|
|
172
|
-
function showToolCall(title, command) {
|
|
207
|
+
function showToolCall(title, command, extra) {
|
|
208
|
+
closeToolLine();
|
|
173
209
|
stopCurrentSpinner();
|
|
174
210
|
if (!renderer)
|
|
175
211
|
startAgentResponse();
|
|
176
212
|
renderer.flush();
|
|
177
213
|
const termW = process.stdout.columns || 80;
|
|
178
|
-
const lines = renderToolCall({
|
|
179
|
-
|
|
180
|
-
|
|
214
|
+
const lines = renderToolCall({
|
|
215
|
+
title,
|
|
216
|
+
command: command || undefined,
|
|
217
|
+
kind: extra?.kind,
|
|
218
|
+
locations: extra?.locations,
|
|
219
|
+
rawInput: extra?.rawInput,
|
|
220
|
+
}, termW);
|
|
221
|
+
// Write all lines except the last normally, write last without \n
|
|
222
|
+
for (let i = 0; i < lines.length - 1; i++) {
|
|
223
|
+
renderer.writeLine(lines[i]);
|
|
181
224
|
}
|
|
225
|
+
if (lines.length > 0) {
|
|
226
|
+
process.stdout.write(` ${lines[lines.length - 1]}`);
|
|
227
|
+
toolLineOpen = true;
|
|
228
|
+
}
|
|
229
|
+
hadToolCalls = true;
|
|
182
230
|
// Reset output tracking for the new tool
|
|
183
231
|
commandOutputLineCount = 0;
|
|
184
232
|
commandOutputOverflow = 0;
|
|
@@ -186,15 +234,37 @@ export default function activate({ bus }) {
|
|
|
186
234
|
function showToolComplete(exitCode) {
|
|
187
235
|
if (!renderer)
|
|
188
236
|
return;
|
|
189
|
-
const
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
237
|
+
const mark = exitCode === null
|
|
238
|
+
? `${p.muted}(timed out)${p.reset}`
|
|
239
|
+
: exitCode === 0
|
|
240
|
+
? `${p.success}✓${p.reset}`
|
|
241
|
+
: `${p.error}✗ exit ${exitCode}${p.reset}`;
|
|
242
|
+
if (toolLineOpen && commandOutputLineCount === 0) {
|
|
243
|
+
// No output written — append mark on same line as tool header
|
|
244
|
+
process.stdout.write(` ${mark}\n`);
|
|
245
|
+
toolLineOpen = false;
|
|
246
|
+
}
|
|
247
|
+
else {
|
|
248
|
+
closeToolLine();
|
|
249
|
+
flushCommandOutput();
|
|
250
|
+
renderer.writeLine(` ${mark}`);
|
|
193
251
|
}
|
|
194
252
|
}
|
|
195
|
-
function
|
|
253
|
+
function hasThinkingMode() {
|
|
254
|
+
const mode = getAcpClient().getCurrentMode();
|
|
255
|
+
return !mode || mode.id !== "off";
|
|
256
|
+
}
|
|
257
|
+
function startThinkingSpinner() {
|
|
258
|
+
// Preserve start time if restarting (e.g. toggle), otherwise reset
|
|
259
|
+
if (!spinnerStartTime)
|
|
260
|
+
spinnerStartTime = Date.now();
|
|
196
261
|
stopCurrentSpinner();
|
|
197
|
-
|
|
262
|
+
const thinking = hasThinkingMode();
|
|
263
|
+
const label = thinking ? "Thinking" : "Working";
|
|
264
|
+
const hint = thinking
|
|
265
|
+
? (showThinkingText ? "(ctrl+t to collapse)" : "(ctrl+t to expand)")
|
|
266
|
+
: "";
|
|
267
|
+
spinner = startSpinner(label, { hint: hint || undefined, startTime: spinnerStartTime });
|
|
198
268
|
}
|
|
199
269
|
function stopCurrentSpinner() {
|
|
200
270
|
if (spinner) {
|
|
@@ -202,14 +272,24 @@ export default function activate({ bus }) {
|
|
|
202
272
|
spinner = null;
|
|
203
273
|
}
|
|
204
274
|
}
|
|
275
|
+
function closeToolLine() {
|
|
276
|
+
if (toolLineOpen) {
|
|
277
|
+
process.stdout.write("\n");
|
|
278
|
+
toolLineOpen = false;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
205
281
|
function writeCommandOutput(chunk) {
|
|
206
282
|
if (!renderer)
|
|
207
283
|
return;
|
|
284
|
+
closeToolLine();
|
|
285
|
+
const maxLines = currentToolKind === "read"
|
|
286
|
+
? getSettings().readOutputMaxLines
|
|
287
|
+
: getSettings().maxCommandOutputLines;
|
|
208
288
|
commandOutputBuffer += chunk;
|
|
209
289
|
const lines = commandOutputBuffer.split("\n");
|
|
210
290
|
commandOutputBuffer = lines.pop();
|
|
211
291
|
for (const line of lines) {
|
|
212
|
-
if (commandOutputLineCount <
|
|
292
|
+
if (commandOutputLineCount < maxLines) {
|
|
213
293
|
renderer.writeLine(`${p.dim} ${line}${p.reset}`);
|
|
214
294
|
commandOutputLineCount++;
|
|
215
295
|
}
|
|
@@ -221,8 +301,11 @@ export default function activate({ bus }) {
|
|
|
221
301
|
function flushCommandOutput() {
|
|
222
302
|
if (!renderer)
|
|
223
303
|
return;
|
|
304
|
+
const maxLines = currentToolKind === "read"
|
|
305
|
+
? getSettings().readOutputMaxLines
|
|
306
|
+
: getSettings().maxCommandOutputLines;
|
|
224
307
|
if (commandOutputBuffer) {
|
|
225
|
-
if (commandOutputLineCount <
|
|
308
|
+
if (commandOutputLineCount < maxLines) {
|
|
226
309
|
renderer.writeLine(`${p.dim} ${commandOutputBuffer}${p.reset}`);
|
|
227
310
|
commandOutputLineCount++;
|
|
228
311
|
}
|
|
@@ -231,12 +314,11 @@ export default function activate({ bus }) {
|
|
|
231
314
|
}
|
|
232
315
|
commandOutputBuffer = "";
|
|
233
316
|
}
|
|
234
|
-
if (commandOutputOverflow > 0) {
|
|
317
|
+
if (commandOutputOverflow > 0 && maxLines > 0) {
|
|
235
318
|
renderer.writeLine(`${p.dim} … ${commandOutputOverflow} more lines${p.reset}`);
|
|
236
|
-
commandOutputOverflow = 0;
|
|
237
319
|
}
|
|
320
|
+
commandOutputOverflow = 0;
|
|
238
321
|
}
|
|
239
|
-
const DIFF_MAX_LINES = 20;
|
|
240
322
|
function diffTitle(filePath, diff) {
|
|
241
323
|
const stats = diff.isNewFile
|
|
242
324
|
? `${p.success}+${diff.added}${p.reset}`
|
|
@@ -252,7 +334,7 @@ export default function activate({ bus }) {
|
|
|
252
334
|
const diffLines = renderDiff(diff, {
|
|
253
335
|
width: contentW,
|
|
254
336
|
filePath,
|
|
255
|
-
maxLines:
|
|
337
|
+
maxLines: getSettings().diffMaxLines,
|
|
256
338
|
trueColor: true,
|
|
257
339
|
mode: "unified",
|
|
258
340
|
});
|
|
@@ -323,7 +405,7 @@ export default function activate({ bus }) {
|
|
|
323
405
|
const diffLines = renderDiff(diff, {
|
|
324
406
|
width: contentW,
|
|
325
407
|
filePath,
|
|
326
|
-
maxLines:
|
|
408
|
+
maxLines: getSettings().diffMaxLines,
|
|
327
409
|
trueColor: true,
|
|
328
410
|
mode: "unified",
|
|
329
411
|
});
|
|
@@ -342,8 +424,31 @@ export default function activate({ bus }) {
|
|
|
342
424
|
}
|
|
343
425
|
function toggleThinkingDisplay() {
|
|
344
426
|
showThinkingText = !showThinkingText;
|
|
345
|
-
|
|
346
|
-
|
|
427
|
+
// Update spinner hint to reflect new state, even if not actively thinking
|
|
428
|
+
if (spinner) {
|
|
429
|
+
stopCurrentSpinner();
|
|
430
|
+
startThinkingSpinner();
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
if (!isThinking)
|
|
434
|
+
return;
|
|
435
|
+
if (showThinkingText) {
|
|
436
|
+
// Switch from spinner to streaming text
|
|
437
|
+
stopCurrentSpinner();
|
|
438
|
+
if (!renderer)
|
|
439
|
+
startAgentResponse();
|
|
440
|
+
renderer.writeLine(`${p.dim}Thinking (ctrl+t to collapse)${p.reset}`);
|
|
441
|
+
}
|
|
442
|
+
else {
|
|
443
|
+
// Switch from streaming text to spinner
|
|
444
|
+
if (renderer) {
|
|
445
|
+
renderer.flush();
|
|
446
|
+
const termW = process.stdout.columns || 80;
|
|
447
|
+
const w = Math.min(80, termW);
|
|
448
|
+
renderer.writeLine(`${p.dim}${"─".repeat(w)}${p.reset}`);
|
|
449
|
+
}
|
|
450
|
+
startThinkingSpinner();
|
|
451
|
+
}
|
|
347
452
|
}
|
|
348
453
|
function showError(message) {
|
|
349
454
|
process.stdout.write(`\n${p.error}Error: ${message}${p.reset}\n`);
|