@agnishc/edb-compact-tools 0.1.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/CHANGELOG.md +5 -0
- package/LICENSE +21 -0
- package/README.md +42 -0
- package/package.json +41 -0
- package/src/index.ts +526 -0
package/CHANGELOG.md
ADDED
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Agnish Chakraborty
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# edb-compact-tools
|
|
2
|
+
|
|
3
|
+
Pi extension that replaces large built-in tool-call blocks with compact outlined rows.
|
|
4
|
+
|
|
5
|
+
## Behavior
|
|
6
|
+
|
|
7
|
+
- Overrides `read`, `bash`, `grep`, `find`, `ls`, `edit`, and `write` renderers.
|
|
8
|
+
- Adds a generic compact blanket renderer for every other tool, without hardcoding individual tool names.
|
|
9
|
+
- Delegates execution to Pi's built-in tools, so tool behavior is unchanged.
|
|
10
|
+
- Collapsed by default: one compact summary row.
|
|
11
|
+
- Expanded with Pi's normal tool expand keybinding, usually `ctrl+o`.
|
|
12
|
+
- Uses `renderShell: "self"`, so collapsed rows have no filled background; expanded output uses Pi's subtle tool-state background colors.
|
|
13
|
+
- Collapsed tools render as one compact full-outline block with two lines: the call line, then the status/summary line. The whole outline turns green on success, red on failure, and yellow while running. Expanding shows the available output inside the same outline.
|
|
14
|
+
- Adds a muted separator before each tool block.
|
|
15
|
+
- Styles user messages as compact outlined cards with an accent border and a random red emoji marker.
|
|
16
|
+
- Styles assistant text messages as compact outlined cards with muted borders.
|
|
17
|
+
- Uses an outline color per tool:
|
|
18
|
+
- `bash` -> `bashMode`
|
|
19
|
+
- `read` -> `toolTitle`
|
|
20
|
+
- `grep` -> `success`
|
|
21
|
+
- `find` -> `accent`
|
|
22
|
+
- `ls` -> `warning`
|
|
23
|
+
- `edit` -> `toolDiffAdded`
|
|
24
|
+
- `write` -> `accent`
|
|
25
|
+
|
|
26
|
+
## Local development
|
|
27
|
+
|
|
28
|
+
Run Pi with the extension directly:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pi -e ./packages/edb-compact-tools/src/index.ts
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Or install as a Pi package after publishing:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pi install npm:@agnishc/edb-compact-tools
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Notes
|
|
41
|
+
|
|
42
|
+
`edit` expansion shows the unified diff from Pi's normal edit result. `write` expansion shows the normal write result text, not a synthetic full-file diff.
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@agnishc/edb-compact-tools",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Pi extension: compact outlined tool-call renderers with ctrl+o expansion",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"pi-package",
|
|
7
|
+
"pi-extension",
|
|
8
|
+
"tools",
|
|
9
|
+
"renderer"
|
|
10
|
+
],
|
|
11
|
+
"type": "module",
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"author": "Agnish Chakraborty",
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "git+https://github.com/agnishcc/pi-extention-monorepo.git",
|
|
17
|
+
"directory": "packages/edb-compact-tools"
|
|
18
|
+
},
|
|
19
|
+
"homepage": "https://github.com/agnishcc/pi-extention-monorepo/tree/main/packages/edb-compact-tools#readme",
|
|
20
|
+
"bugs": {
|
|
21
|
+
"url": "https://github.com/agnishcc/pi-extention-monorepo/issues"
|
|
22
|
+
},
|
|
23
|
+
"publishConfig": {
|
|
24
|
+
"access": "public"
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"src",
|
|
28
|
+
"README.md",
|
|
29
|
+
"LICENSE",
|
|
30
|
+
"CHANGELOG.md"
|
|
31
|
+
],
|
|
32
|
+
"pi": {
|
|
33
|
+
"extensions": [
|
|
34
|
+
"./src/index.ts"
|
|
35
|
+
]
|
|
36
|
+
},
|
|
37
|
+
"peerDependencies": {
|
|
38
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
39
|
+
"@earendil-works/pi-tui": "*"
|
|
40
|
+
}
|
|
41
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,526 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AssistantMessageComponent,
|
|
3
|
+
createBashTool,
|
|
4
|
+
createEditTool,
|
|
5
|
+
createFindTool,
|
|
6
|
+
createGrepTool,
|
|
7
|
+
createLsTool,
|
|
8
|
+
createReadTool,
|
|
9
|
+
createWriteTool,
|
|
10
|
+
type ExtensionAPI,
|
|
11
|
+
keyHint,
|
|
12
|
+
ToolExecutionComponent,
|
|
13
|
+
UserMessageComponent,
|
|
14
|
+
} from "@earendil-works/pi-coding-agent";
|
|
15
|
+
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
16
|
+
|
|
17
|
+
type CompactTheme = {
|
|
18
|
+
fg: (color: any, text: string) => string;
|
|
19
|
+
bg?: (color: any, text: string) => string;
|
|
20
|
+
bold: (text: string) => string;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
type BuiltinToolName = "read" | "bash" | "grep" | "find" | "ls" | "edit" | "write";
|
|
24
|
+
|
|
25
|
+
type BuiltinTool = {
|
|
26
|
+
description: string;
|
|
27
|
+
parameters: unknown;
|
|
28
|
+
execute: (id: string, params: unknown, signal?: AbortSignal, onUpdate?: unknown, ctx?: unknown) => Promise<unknown>;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const MAX_COLLAPSED_TEXT = 120;
|
|
32
|
+
const MAX_EXPANDED_LINES = 4000;
|
|
33
|
+
const MAX_LINE_CHARS = 120;
|
|
34
|
+
const TOOL_EXECUTION_PATCH_SYMBOL = Symbol.for("edb-compact-tools.tool-execution-patch");
|
|
35
|
+
const USER_MESSAGE_PATCH_SYMBOL = Symbol.for("edb-compact-tools.user-message-patch");
|
|
36
|
+
const ASSISTANT_MESSAGE_PATCH_SYMBOL = Symbol.for("edb-compact-tools.assistant-message-patch");
|
|
37
|
+
const USER_MESSAGE_MARKER_SYMBOL = Symbol.for("edb-compact-tools.user-message-marker");
|
|
38
|
+
const USER_MESSAGE_EMOJIS = [
|
|
39
|
+
"✨",
|
|
40
|
+
"🚀",
|
|
41
|
+
"🧠",
|
|
42
|
+
"⚡",
|
|
43
|
+
"🔥",
|
|
44
|
+
"🌿",
|
|
45
|
+
"🌀",
|
|
46
|
+
"💎",
|
|
47
|
+
"🛠️",
|
|
48
|
+
"🎯",
|
|
49
|
+
"🦊",
|
|
50
|
+
"🐙",
|
|
51
|
+
"🌙",
|
|
52
|
+
"☕",
|
|
53
|
+
"🍀",
|
|
54
|
+
"🪄",
|
|
55
|
+
];
|
|
56
|
+
let activeTheme: CompactTheme | undefined;
|
|
57
|
+
const OSC133_ZONE_START = "\x1b]133;A\x07";
|
|
58
|
+
const OSC133_ZONE_END = "\x1b]133;B\x07";
|
|
59
|
+
const OSC133_ZONE_FINAL = "\x1b]133;C\x07";
|
|
60
|
+
|
|
61
|
+
function oneLine(value: unknown): string {
|
|
62
|
+
return String(value ?? "")
|
|
63
|
+
.replace(/\s+/g, " ")
|
|
64
|
+
.trim();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function clip(text: string, max = MAX_COLLAPSED_TEXT): string {
|
|
68
|
+
return text.length > max ? `${text.slice(0, Math.max(0, max - 1))}…` : text;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function lineCount(text: string): number {
|
|
72
|
+
if (!text) return 0;
|
|
73
|
+
return text.split(/\r?\n/).length;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function textContent(result: any): string {
|
|
77
|
+
const content = Array.isArray(result?.content) ? result.content : [];
|
|
78
|
+
return content
|
|
79
|
+
.filter((item: any) => item?.type === "text" && typeof item.text === "string")
|
|
80
|
+
.map((item: any) => item.text)
|
|
81
|
+
.join("\n");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function outputWasTruncated(text: string): boolean {
|
|
85
|
+
return /\btruncated\b|Full output saved to:/i.test(text);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function previewLines(text: string, mode: "head" | "tail", limit = MAX_EXPANDED_LINES): string[] {
|
|
89
|
+
const lines = text.replace(/\s+$/g, "").split(/\r?\n/);
|
|
90
|
+
const selected = mode === "tail" ? lines.slice(-limit) : lines.slice(0, limit);
|
|
91
|
+
return selected.map((line) => clip(line, MAX_LINE_CHARS));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function toolColor(toolName: string): string {
|
|
95
|
+
switch (toolName) {
|
|
96
|
+
case "bash":
|
|
97
|
+
return "bashMode";
|
|
98
|
+
case "read":
|
|
99
|
+
return "toolTitle";
|
|
100
|
+
case "grep":
|
|
101
|
+
return "success";
|
|
102
|
+
case "find":
|
|
103
|
+
return "accent";
|
|
104
|
+
case "ls":
|
|
105
|
+
return "warning";
|
|
106
|
+
case "edit":
|
|
107
|
+
return "toolDiffAdded";
|
|
108
|
+
case "write":
|
|
109
|
+
return "accent";
|
|
110
|
+
default:
|
|
111
|
+
return "accent";
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function toolIcon(toolName: string): string {
|
|
116
|
+
switch (toolName) {
|
|
117
|
+
case "bash":
|
|
118
|
+
return "⚙️";
|
|
119
|
+
case "read":
|
|
120
|
+
return "📖";
|
|
121
|
+
case "grep":
|
|
122
|
+
return "🔎";
|
|
123
|
+
case "find":
|
|
124
|
+
return "🧭";
|
|
125
|
+
case "ls":
|
|
126
|
+
return "📁";
|
|
127
|
+
case "edit":
|
|
128
|
+
return "✏️";
|
|
129
|
+
case "write":
|
|
130
|
+
return "📝";
|
|
131
|
+
default:
|
|
132
|
+
return "🧩";
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function callLabel(toolName: string, args: any): string {
|
|
137
|
+
if (toolName === "bash") return clip(oneLine(args?.command), 140);
|
|
138
|
+
if (toolName === "read") return clip(oneLine(args?.path), 140);
|
|
139
|
+
if (toolName === "grep") {
|
|
140
|
+
const pattern = oneLine(args?.pattern);
|
|
141
|
+
const path = oneLine(args?.path ?? args?.glob ?? ".");
|
|
142
|
+
return clip(`${pattern}${path ? ` in ${path}` : ""}`, 140);
|
|
143
|
+
}
|
|
144
|
+
if (toolName === "find") return clip(oneLine(args?.path ?? args?.pattern ?? "."), 140);
|
|
145
|
+
if (toolName === "edit") {
|
|
146
|
+
const count = Array.isArray(args?.edits) ? args.edits.length : args?.oldText && args?.newText ? 1 : 0;
|
|
147
|
+
return clip(
|
|
148
|
+
`${oneLine(args?.path ?? args?.file_path)}${count ? ` · ${count} replacement${count === 1 ? "" : "s"}` : ""}`,
|
|
149
|
+
140,
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
if (toolName === "write") {
|
|
153
|
+
const bytes = typeof args?.content === "string" ? Buffer.byteLength(args.content, "utf8") : 0;
|
|
154
|
+
return clip(`${oneLine(args?.path ?? args?.file_path)}${bytes ? ` · ${bytes} bytes` : ""}`, 140);
|
|
155
|
+
}
|
|
156
|
+
const compactArgs = oneLine(JSON.stringify(args ?? {}));
|
|
157
|
+
return clip(compactArgs === "{}" ? "" : compactArgs, 140);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function summaryFor(toolName: string, result: any): string {
|
|
161
|
+
const text = textContent(result);
|
|
162
|
+
const lines = lineCount(text);
|
|
163
|
+
const truncated = outputWasTruncated(text) ? " · truncated" : "";
|
|
164
|
+
if (toolName === "bash") {
|
|
165
|
+
const exitMatch = text.match(/Exit code:\s*(-?\d+)/i) ?? text.match(/exit(?:ed)?(?: code)?\s*(-?\d+)/i);
|
|
166
|
+
const exit = exitMatch?.[1] ?? (result?.isError ? "1" : "0");
|
|
167
|
+
return `exit ${exit} · ${lines} line${lines === 1 ? "" : "s"}${truncated}`;
|
|
168
|
+
}
|
|
169
|
+
if (toolName === "read") return `${lines} line${lines === 1 ? "" : "s"}${truncated}`;
|
|
170
|
+
if (toolName === "ls") return `${lines} item${lines === 1 ? "" : "s"}${truncated}`;
|
|
171
|
+
if (toolName === "edit") {
|
|
172
|
+
const diff = typeof result?.details?.diff === "string" ? result.details.diff : "";
|
|
173
|
+
const added = diff
|
|
174
|
+
.split(/\r?\n/)
|
|
175
|
+
.filter((line: string) => line.startsWith("+") && !line.startsWith("+++")).length;
|
|
176
|
+
const removed = diff
|
|
177
|
+
.split(/\r?\n/)
|
|
178
|
+
.filter((line: string) => line.startsWith("-") && !line.startsWith("---")).length;
|
|
179
|
+
return diff ? `+${added} -${removed}` : `${lines} line${lines === 1 ? "" : "s"}${truncated}`;
|
|
180
|
+
}
|
|
181
|
+
if (toolName === "write") return `${lines} line${lines === 1 ? "" : "s"}${truncated}`;
|
|
182
|
+
return `${lines} result${lines === 1 ? "" : "s"}${truncated}`;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
type ToolBlockKind = "call" | "result" | "full";
|
|
186
|
+
|
|
187
|
+
class EmptyBlock {
|
|
188
|
+
render(): string[] {
|
|
189
|
+
return [];
|
|
190
|
+
}
|
|
191
|
+
invalidate(): void {}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
class ToolBlock {
|
|
195
|
+
constructor(
|
|
196
|
+
private readonly kind: ToolBlockKind,
|
|
197
|
+
private readonly toolName: string,
|
|
198
|
+
private readonly lines: string[],
|
|
199
|
+
private readonly theme: CompactTheme,
|
|
200
|
+
private readonly borderColor?: string,
|
|
201
|
+
) {}
|
|
202
|
+
|
|
203
|
+
render(width: number): string[] {
|
|
204
|
+
const renderWidth = Math.max(8, width - 1);
|
|
205
|
+
const separator = this.theme.fg("borderMuted", "─".repeat(Math.max(8, Math.min(32, renderWidth))));
|
|
206
|
+
const block = this.lines.map((line, index) => {
|
|
207
|
+
if (this.kind === "call") return this.renderTop(line, renderWidth);
|
|
208
|
+
if (this.kind === "full" && index === 0) return this.renderTop(line, renderWidth);
|
|
209
|
+
const isLast = index === this.lines.length - 1;
|
|
210
|
+
return isLast ? this.renderBottom(line, renderWidth) : this.renderBody(line, renderWidth);
|
|
211
|
+
});
|
|
212
|
+
return [separator, "", ...block, ""];
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
invalidate(): void {}
|
|
216
|
+
|
|
217
|
+
private color(text: string): string {
|
|
218
|
+
return this.theme.fg(this.borderColor ?? toolColor(this.toolName), text);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
private fit(text: string, width: number): string {
|
|
222
|
+
const clipped = truncateToWidth(text, Math.max(1, width), "");
|
|
223
|
+
return `${clipped}${" ".repeat(Math.max(0, width - visibleWidth(clipped)))}`;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
private renderTop(content: string, width: number): string {
|
|
227
|
+
const prefix = this.color("╭─ ");
|
|
228
|
+
const suffix = this.color("╮");
|
|
229
|
+
const innerWidth = Math.max(1, width - 4);
|
|
230
|
+
const fitted = truncateToWidth(content, innerWidth, "");
|
|
231
|
+
const fill = this.color("─".repeat(Math.max(0, innerWidth - visibleWidth(fitted))));
|
|
232
|
+
return `${prefix}${fitted}${fill}${suffix}`;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
private renderBody(content: string, width: number): string {
|
|
236
|
+
const innerWidth = Math.max(1, width - 2);
|
|
237
|
+
return `${this.color("│")}${this.fit(content, innerWidth)}${this.color("│")}`;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
private renderBottom(content: string, width: number): string {
|
|
241
|
+
const prefix = this.color("╰─ ");
|
|
242
|
+
const suffix = this.color("╯");
|
|
243
|
+
const innerWidth = Math.max(1, width - 4);
|
|
244
|
+
const fitted = truncateToWidth(content, innerWidth, "");
|
|
245
|
+
const fill = this.color("─".repeat(Math.max(0, innerWidth - visibleWidth(fitted))));
|
|
246
|
+
return `${prefix}${fitted}${fill}${suffix}`;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function topLine(toolName: string, theme: CompactTheme, label: string): string {
|
|
251
|
+
const color = toolColor(toolName);
|
|
252
|
+
const title = `${toolIcon(toolName)} ${toolName}`;
|
|
253
|
+
return `${theme.fg(color, theme.bold(title))} ${theme.fg("toolOutput", label)}`;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function midLine(_toolName: string, theme: CompactTheme, text: string): string {
|
|
257
|
+
return theme.fg("toolOutput", text);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function bottomLine(_toolName: string, _theme: CompactTheme, text = ""): string {
|
|
261
|
+
return text.trimEnd();
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function toolText(
|
|
265
|
+
kind: ToolBlockKind,
|
|
266
|
+
toolName: string,
|
|
267
|
+
lines: string[],
|
|
268
|
+
theme: CompactTheme,
|
|
269
|
+
borderColor?: string,
|
|
270
|
+
): ToolBlock {
|
|
271
|
+
return new ToolBlock(kind, toolName, lines, theme, borderColor);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function toolBg(theme: CompactTheme, text: string, state: "pending" | "success" | "error"): string {
|
|
275
|
+
const token = state === "pending" ? "toolPendingBg" : state === "error" ? "toolErrorBg" : "toolSuccessBg";
|
|
276
|
+
return theme.bg ? theme.bg(token, text) : text;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function renderCall(_toolName: string, _args: any, _theme: CompactTheme, _context: any) {
|
|
280
|
+
return new EmptyBlock();
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function renderResult(toolName: string, result: any, options: any, theme: CompactTheme, context: any) {
|
|
284
|
+
if (options?.isPartial) {
|
|
285
|
+
return toolText(
|
|
286
|
+
"full",
|
|
287
|
+
toolName,
|
|
288
|
+
[
|
|
289
|
+
topLine(toolName, theme, callLabel(toolName, context?.args)),
|
|
290
|
+
bottomLine(toolName, theme, theme.fg("muted", "running…")),
|
|
291
|
+
],
|
|
292
|
+
theme,
|
|
293
|
+
"warning",
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const summary = summaryFor(toolName, result);
|
|
298
|
+
const text = textContent(result);
|
|
299
|
+
const failed = Boolean(context?.isError || result?.isError);
|
|
300
|
+
const statusColor = failed ? "error" : "success";
|
|
301
|
+
const statusIcon = failed ? "✗" : "✓";
|
|
302
|
+
const expandHint = options?.expanded ? "" : ` ${theme.fg("dim", keyHint("app.tools.expand", "expand"))}`;
|
|
303
|
+
|
|
304
|
+
const top = topLine(toolName, theme, callLabel(toolName, context?.args));
|
|
305
|
+
const bottom = bottomLine(
|
|
306
|
+
toolName,
|
|
307
|
+
theme,
|
|
308
|
+
`${theme.fg(statusColor, statusIcon)} ${theme.fg("toolOutput", summary)}${expandHint}`,
|
|
309
|
+
);
|
|
310
|
+
const borderColor = failed ? "error" : "success";
|
|
311
|
+
|
|
312
|
+
if (!options?.expanded || !text.trim()) {
|
|
313
|
+
return toolText("full", toolName, [top, bottom], theme, borderColor);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const diff = toolName === "edit" && typeof result?.details?.diff === "string" ? result.details.diff : "";
|
|
317
|
+
const previewText = diff || text;
|
|
318
|
+
const mode = toolName === "bash" ? "tail" : "head";
|
|
319
|
+
const lines = previewLines(previewText, mode).map((line) => midLine(toolName, theme, line));
|
|
320
|
+
if (lineCount(previewText) > MAX_EXPANDED_LINES) {
|
|
321
|
+
const omitted = lineCount(previewText) - MAX_EXPANDED_LINES;
|
|
322
|
+
lines.push(midLine(toolName, theme, theme.fg("dim", `… ${omitted} more line(s)`)));
|
|
323
|
+
}
|
|
324
|
+
lines.unshift(top);
|
|
325
|
+
lines.push(bottomLine(toolName, theme, `${theme.fg(statusColor, statusIcon)} ${theme.fg("toolOutput", summary)}`));
|
|
326
|
+
return toolText(
|
|
327
|
+
"full",
|
|
328
|
+
toolName,
|
|
329
|
+
lines,
|
|
330
|
+
theme,
|
|
331
|
+
borderColor,
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function padVisible(text: string, width: number): string {
|
|
336
|
+
const clipped = truncateToWidth(text, width, "");
|
|
337
|
+
return `${clipped}${" ".repeat(Math.max(0, width - visibleWidth(clipped)))}`;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function stripUserZoneMarkers(line: string): string {
|
|
341
|
+
return line.replaceAll(OSC133_ZONE_START, "").replaceAll(OSC133_ZONE_END, "").replaceAll(OSC133_ZONE_FINAL, "");
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function randomUserMessageMarker(): string {
|
|
345
|
+
return USER_MESSAGE_EMOJIS[Math.floor(Math.random() * USER_MESSAGE_EMOJIS.length)] ?? "✨";
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function trimVisualBlankLines(lines: string[]): string[] {
|
|
349
|
+
let start = 0;
|
|
350
|
+
let end = lines.length;
|
|
351
|
+
while (start < end && stripUserZoneMarkers(lines[start] ?? "").trim() === "") start++;
|
|
352
|
+
while (end > start && stripUserZoneMarkers(lines[end - 1] ?? "").trim() === "") end--;
|
|
353
|
+
return lines.slice(start, end);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function frameMessage(
|
|
357
|
+
lines: string[],
|
|
358
|
+
width: number,
|
|
359
|
+
theme: CompactTheme,
|
|
360
|
+
markerText: string,
|
|
361
|
+
borderColor: string,
|
|
362
|
+
markerColor: string,
|
|
363
|
+
): string[] {
|
|
364
|
+
if (width < 6) return lines;
|
|
365
|
+
const innerWidth = Math.max(1, width - 2);
|
|
366
|
+
const border = (text: string) => theme.fg(borderColor, text);
|
|
367
|
+
const marker = theme.fg(markerColor, markerText);
|
|
368
|
+
const topFill = Math.max(0, innerWidth - visibleWidth(marker) - 2);
|
|
369
|
+
const top = `${border("╭─")} ${marker}${border("─".repeat(topFill))}${border("╮")}`;
|
|
370
|
+
const body = trimVisualBlankLines(lines).map(
|
|
371
|
+
(line) => `${border("│")}${padVisible(stripUserZoneMarkers(line).trimEnd(), innerWidth)}${border("│")}`,
|
|
372
|
+
);
|
|
373
|
+
const bottom = `${border("╰")}${border("─".repeat(innerWidth))}${border("╯")}`;
|
|
374
|
+
return [`${OSC133_ZONE_START}${top}`, ...body, `${OSC133_ZONE_END}${OSC133_ZONE_FINAL}${bottom}`, ""];
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function frameUserMessage(lines: string[], width: number, theme: CompactTheme, markerText: string): string[] {
|
|
378
|
+
return frameMessage(lines, width, theme, markerText, "accent", "error");
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function frameAssistantMessage(lines: string[], width: number, theme: CompactTheme): string[] {
|
|
382
|
+
return frameMessage(lines, width, theme, "AI", "borderMuted", "toolTitle");
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function installGenericToolRendererPatch(pi: ExtensionAPI): void {
|
|
386
|
+
const proto = ToolExecutionComponent?.prototype as any;
|
|
387
|
+
if (!proto || proto[TOOL_EXECUTION_PATCH_SYMBOL]) return;
|
|
388
|
+
const originalGetCallRenderer = proto.getCallRenderer;
|
|
389
|
+
const originalGetResultRenderer = proto.getResultRenderer;
|
|
390
|
+
const originalGetRenderShell = proto.getRenderShell;
|
|
391
|
+
if (
|
|
392
|
+
typeof originalGetCallRenderer !== "function" ||
|
|
393
|
+
typeof originalGetResultRenderer !== "function" ||
|
|
394
|
+
typeof originalGetRenderShell !== "function"
|
|
395
|
+
) {
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
proto.getCallRenderer = function compactFallbackCallRenderer(this: any) {
|
|
400
|
+
const toolName = typeof this?.toolName === "string" ? this.toolName : "tool";
|
|
401
|
+
return (args: any, theme: CompactTheme, context: any) => renderCall(toolName, args, theme, context);
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
proto.getResultRenderer = function compactFallbackResultRenderer(this: any) {
|
|
405
|
+
const toolName = typeof this?.toolName === "string" ? this.toolName : "tool";
|
|
406
|
+
return (result: any, options: any, theme: CompactTheme, context: any) =>
|
|
407
|
+
renderResult(toolName, result, options, theme, context);
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
proto.getRenderShell = function compactFallbackRenderShell(this: any) {
|
|
411
|
+
return "self";
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
proto[TOOL_EXECUTION_PATCH_SYMBOL] = { originalGetCallRenderer, originalGetResultRenderer, originalGetRenderShell };
|
|
415
|
+
pi.on("session_shutdown", () => {
|
|
416
|
+
const state = proto[TOOL_EXECUTION_PATCH_SYMBOL];
|
|
417
|
+
if (!state) return;
|
|
418
|
+
proto.getCallRenderer = state.originalGetCallRenderer;
|
|
419
|
+
proto.getResultRenderer = state.originalGetResultRenderer;
|
|
420
|
+
proto.getRenderShell = state.originalGetRenderShell;
|
|
421
|
+
delete proto[TOOL_EXECUTION_PATCH_SYMBOL];
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function installMessageRenderers(pi: ExtensionAPI): void {
|
|
426
|
+
const userProto = UserMessageComponent?.prototype as any;
|
|
427
|
+
if (userProto && !userProto[USER_MESSAGE_PATCH_SYMBOL] && typeof userProto.render === "function") {
|
|
428
|
+
const originalRender = userProto.render as (width: number) => string[];
|
|
429
|
+
userProto.render = function compactUserMessageRender(this: any, width: number): string[] {
|
|
430
|
+
const box = this?.contentBox;
|
|
431
|
+
if (box) {
|
|
432
|
+
box.paddingY = 0;
|
|
433
|
+
box.setBgFn?.(undefined);
|
|
434
|
+
box.invalidate?.();
|
|
435
|
+
}
|
|
436
|
+
const frameWidth = Math.max(1, width - 1);
|
|
437
|
+
if (!this[USER_MESSAGE_MARKER_SYMBOL]) this[USER_MESSAGE_MARKER_SYMBOL] = randomUserMessageMarker();
|
|
438
|
+
const rendered = originalRender.call(this, Math.max(1, frameWidth - 2));
|
|
439
|
+
return frameUserMessage(
|
|
440
|
+
rendered,
|
|
441
|
+
frameWidth,
|
|
442
|
+
activeTheme ?? fallbackTheme(),
|
|
443
|
+
this[USER_MESSAGE_MARKER_SYMBOL],
|
|
444
|
+
);
|
|
445
|
+
};
|
|
446
|
+
userProto[USER_MESSAGE_PATCH_SYMBOL] = { originalRender };
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
const assistantProto = AssistantMessageComponent?.prototype as any;
|
|
450
|
+
if (
|
|
451
|
+
assistantProto &&
|
|
452
|
+
!assistantProto[ASSISTANT_MESSAGE_PATCH_SYMBOL] &&
|
|
453
|
+
typeof assistantProto.render === "function"
|
|
454
|
+
) {
|
|
455
|
+
const originalRender = assistantProto.render as (width: number) => string[];
|
|
456
|
+
assistantProto.render = function compactAssistantMessageRender(this: any, width: number): string[] {
|
|
457
|
+
const rendered = originalRender.call(this, Math.max(1, width - 3));
|
|
458
|
+
if (this?.hasToolCalls || rendered.length === 0) return rendered;
|
|
459
|
+
const frameWidth = Math.max(1, width - 1);
|
|
460
|
+
return frameAssistantMessage(rendered, frameWidth, activeTheme ?? fallbackTheme());
|
|
461
|
+
};
|
|
462
|
+
assistantProto[ASSISTANT_MESSAGE_PATCH_SYMBOL] = { originalRender };
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
pi.on("session_start", (_event, ctx) => {
|
|
466
|
+
if (ctx.hasUI) activeTheme = ctx.ui.theme as unknown as CompactTheme;
|
|
467
|
+
});
|
|
468
|
+
pi.on("session_shutdown", () => {
|
|
469
|
+
const userState = userProto?.[USER_MESSAGE_PATCH_SYMBOL];
|
|
470
|
+
if (userState) {
|
|
471
|
+
userProto.render = userState.originalRender;
|
|
472
|
+
delete userProto[USER_MESSAGE_PATCH_SYMBOL];
|
|
473
|
+
}
|
|
474
|
+
const assistantState = assistantProto?.[ASSISTANT_MESSAGE_PATCH_SYMBOL];
|
|
475
|
+
if (assistantState) {
|
|
476
|
+
assistantProto.render = assistantState.originalRender;
|
|
477
|
+
delete assistantProto[ASSISTANT_MESSAGE_PATCH_SYMBOL];
|
|
478
|
+
}
|
|
479
|
+
activeTheme = undefined;
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function fallbackTheme(): CompactTheme {
|
|
484
|
+
return {
|
|
485
|
+
fg: (_color: any, text: string) => text,
|
|
486
|
+
bg: (_color: any, text: string) => text,
|
|
487
|
+
bold: (text: string) => text,
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function registerDelegatingTool(
|
|
492
|
+
pi: ExtensionAPI,
|
|
493
|
+
name: BuiltinToolName,
|
|
494
|
+
createTool: (cwd: string) => BuiltinTool,
|
|
495
|
+
): void {
|
|
496
|
+
const cwd = process.cwd();
|
|
497
|
+
const original = createTool(cwd);
|
|
498
|
+
pi.registerTool({
|
|
499
|
+
name,
|
|
500
|
+
label: name,
|
|
501
|
+
description: original.description,
|
|
502
|
+
parameters: original.parameters as any,
|
|
503
|
+
renderShell: "self",
|
|
504
|
+
async execute(id: string, params: unknown, signal?: AbortSignal, onUpdate?: unknown, ctx?: any) {
|
|
505
|
+
return createTool(ctx?.cwd ?? cwd).execute(id, params, signal, onUpdate, ctx) as any;
|
|
506
|
+
},
|
|
507
|
+
renderCall(args: any, theme: CompactTheme, context: any) {
|
|
508
|
+
return renderCall(name, args, theme, context);
|
|
509
|
+
},
|
|
510
|
+
renderResult(result: any, options: any, theme: CompactTheme, context: any) {
|
|
511
|
+
return renderResult(name, result, options, theme, context);
|
|
512
|
+
},
|
|
513
|
+
} as any);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
export default function compactTools(pi: ExtensionAPI): void {
|
|
517
|
+
installGenericToolRendererPatch(pi);
|
|
518
|
+
installMessageRenderers(pi);
|
|
519
|
+
registerDelegatingTool(pi, "read", createReadTool as unknown as (cwd: string) => BuiltinTool);
|
|
520
|
+
registerDelegatingTool(pi, "bash", createBashTool as unknown as (cwd: string) => BuiltinTool);
|
|
521
|
+
registerDelegatingTool(pi, "grep", createGrepTool as unknown as (cwd: string) => BuiltinTool);
|
|
522
|
+
registerDelegatingTool(pi, "find", createFindTool as unknown as (cwd: string) => BuiltinTool);
|
|
523
|
+
registerDelegatingTool(pi, "ls", createLsTool as unknown as (cwd: string) => BuiltinTool);
|
|
524
|
+
registerDelegatingTool(pi, "edit", createEditTool as unknown as (cwd: string) => BuiltinTool);
|
|
525
|
+
registerDelegatingTool(pi, "write", createWriteTool as unknown as (cwd: string) => BuiltinTool);
|
|
526
|
+
}
|