@agnishc/edb-compact-tools 0.10.6 → 0.10.9

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 CHANGED
@@ -1,5 +1,28 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.10.9] - 2026-05-18
4
+
5
+ ## [0.10.8] - 2026-05-18
6
+
7
+ ## [0.11.0] - 2026-05-18
8
+
9
+ ### Changed
10
+ - **Breaking:** Modularized codebase into 9 focused modules for better maintainability
11
+ - `types.ts` — shared TypeScript types (`CompactTheme`, `BuiltinToolName`, `BuiltinTool`, `ToolBlockKind`)
12
+ - `constants.ts` — rendering limits, patch symbols, emoji sets, OSC133 markers, ANSI codes
13
+ - `text.ts` — text utility functions (`oneLine`, `clip`, `lineCount`, `textContent`, etc.)
14
+ - `tool-meta.ts` — tool metadata registry with per-tool colors, icons, labels, and summaries
15
+ - `tool-block.ts` — `EmptyBlock` and `ToolBlock` classes (box-drawing renderer)
16
+ - `tool-renderer.ts` — `renderCall` and `renderResult` functions
17
+ - `message-frame.ts` — message framing functions for user/assistant messages
18
+ - `patches.ts` — tool and message renderer installation
19
+ - `index.ts` — thin entry point
20
+
21
+ ### Added
22
+ - Purple outline color for `read` tool when reading skill files (`.agents/skills/` or `.pi/agent/skills/`)
23
+ - `tool-meta.ts` uses a declarative registry object pattern for tool metadata
24
+ - `frameAssistantMessage` export (renamed from `_frameAssistantMessage`)
25
+
3
26
  ## [0.10.6] - 2026-05-15
4
27
 
5
28
  ## [0.10.5] - 2026-05-15
package/README.md CHANGED
@@ -16,12 +16,13 @@ Pi extension that replaces large built-in tool-call blocks with compact outlined
16
16
  - Styles assistant text messages as compact outlined cards with muted borders.
17
17
  - Uses an outline color per tool:
18
18
  - `bash` -> `bashMode`
19
- - `read` -> `toolTitle`
19
+ - `read` -> `toolTitle` (or purple when reading skill files)
20
20
  - `grep` -> `success`
21
21
  - `find` -> `accent`
22
22
  - `ls` -> `warning`
23
23
  - `edit` -> `toolDiffAdded`
24
24
  - `write` -> `accent`
25
+ - Detects skill files (paths containing `.agents/skills/` or `.pi/agent/skills/`) and renders them with a purple outline
25
26
 
26
27
  ## Local development
27
28
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agnishc/edb-compact-tools",
3
- "version": "0.10.6",
3
+ "version": "0.10.9",
4
4
  "description": "Pi extension: compact outlined tool-call renderers with ctrl+o expansion",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -0,0 +1,22 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import * as constants from "./constants.js";
3
+
4
+ // ── constants ──────────────────────────────────────────────────────────────
5
+
6
+ describe("ANSI constants", () => {
7
+ it("exports ANSI_PURPLE and ANSI_RESET", () => {
8
+ expect(constants.ANSI_PURPLE).toMatch(/^\x1b\[38;5;\d+m$/);
9
+ expect(constants.ANSI_RESET).toBe("\x1b[0m");
10
+ });
11
+
12
+ it("exports emoji arrays", () => {
13
+ expect(constants.USER_MESSAGE_EMOJIS.length).toBeGreaterThan(0);
14
+ expect(constants.ASSISTANT_MESSAGE_EMOJIS.length).toBeGreaterThan(0);
15
+ });
16
+
17
+ it("exports OSC133 markers", () => {
18
+ expect(constants.OSC133_ZONE_START).toBe("\x1b]133;A\x07");
19
+ expect(constants.OSC133_ZONE_END).toBe("\x1b]133;B\x07");
20
+ expect(constants.OSC133_ZONE_FINAL).toBe("\x1b]133;C\x07");
21
+ });
22
+ });
@@ -0,0 +1,42 @@
1
+ // ── Rendering limits ──────────────────────────────────────────────
2
+ export const MAX_COLLAPSED_TEXT = 120;
3
+ export const MAX_EXPANDED_LINES = 4000;
4
+ export const MAX_LINE_CHARS = 120;
5
+
6
+ // ── Patch markers (prevent double-patching) ──────────────────────
7
+ export const TOOL_EXECUTION_PATCH_SYMBOL = Symbol.for("edb-compact-tools.tool-execution-patch");
8
+ export const USER_MESSAGE_PATCH_SYMBOL = Symbol.for("edb-compact-tools.user-message-patch");
9
+ export const ASSISTANT_MESSAGE_PATCH_SYMBOL = Symbol.for("edb-compact-tools.assistant-message-patch");
10
+ export const USER_MESSAGE_MARKER_SYMBOL = Symbol.for("edb-compact-tools.user-message-marker");
11
+ export const ASSISTANT_MESSAGE_MARKER_SYMBOL = Symbol.for("edb-compact-tools.assistant-message-marker");
12
+
13
+ // ── Emoji sets ───────────────────────────────────────────────────
14
+ export const USER_MESSAGE_EMOJIS = [
15
+ "🦊",
16
+ "🐙",
17
+ "🐸",
18
+ "🐻",
19
+ "🐼",
20
+ "🐨",
21
+ "🦥",
22
+ "🦔",
23
+ "🦫",
24
+ "🦚",
25
+ "🦩",
26
+ "🦉",
27
+ "🐰",
28
+ "🐢",
29
+ "🦎",
30
+ "🦖",
31
+ ];
32
+
33
+ export const ASSISTANT_MESSAGE_EMOJIS = ["🤖", "🧠", "🦾", "🛸", "💡"];
34
+
35
+ // ── OSC133 shell integration markers ─────────────────────────────
36
+ export const OSC133_ZONE_START = "\x1b]133;A\x07";
37
+ export const OSC133_ZONE_END = "\x1b]133;B\x07";
38
+ export const OSC133_ZONE_FINAL = "\x1b]133;C\x07";
39
+
40
+ // ── ANSI color codes ─────────────────────────────────────────────
41
+ export const ANSI_PURPLE = "\x1b[38;5;141m";
42
+ export const ANSI_RESET = "\x1b[0m";
package/src/index.ts CHANGED
@@ -1,5 +1,4 @@
1
1
  import {
2
- AssistantMessageComponent,
3
2
  createBashTool,
4
3
  createEditTool,
5
4
  createFindTool,
@@ -8,516 +7,9 @@ import {
8
7
  createReadTool,
9
8
  createWriteTool,
10
9
  type ExtensionAPI,
11
- keyHint,
12
- ToolExecutionComponent,
13
- UserMessageComponent,
14
10
  } 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 ASSISTANT_MESSAGE_MARKER_SYMBOL = Symbol.for("edb-compact-tools.assistant-message-marker");
39
- const USER_MESSAGE_EMOJIS = [
40
- "🦊",
41
- "🐙",
42
- "🐸",
43
- "🐻",
44
- "🐼",
45
- "🐨",
46
- "🦥",
47
- "🦔",
48
- "🦫",
49
- "🦚",
50
- "🦩",
51
- "🦉",
52
- "🐰",
53
- "🐢",
54
- "🦎",
55
- "🦖",
56
- ];
57
- const ASSISTANT_MESSAGE_EMOJIS = ["🤖", "🧠", "🦾", "🛸", "💡"];
58
- let activeTheme: CompactTheme | undefined;
59
- const OSC133_ZONE_START = "\x1b]133;A\x07";
60
- const OSC133_ZONE_END = "\x1b]133;B\x07";
61
- const OSC133_ZONE_FINAL = "\x1b]133;C\x07";
62
-
63
- function oneLine(value: unknown): string {
64
- return String(value ?? "")
65
- .replace(/\s+/g, " ")
66
- .trim();
67
- }
68
-
69
- function clip(text: string, max = MAX_COLLAPSED_TEXT): string {
70
- return text.length > max ? `${text.slice(0, Math.max(0, max - 1))}…` : text;
71
- }
72
-
73
- function lineCount(text: string): number {
74
- if (!text) return 0;
75
- return text.split(/\r?\n/).length;
76
- }
77
-
78
- function textContent(result: any): string {
79
- const content = Array.isArray(result?.content) ? result.content : [];
80
- return content
81
- .filter((item: any) => item?.type === "text" && typeof item.text === "string")
82
- .map((item: any) => item.text)
83
- .join("\n");
84
- }
85
-
86
- function outputWasTruncated(text: string): boolean {
87
- return /\btruncated\b|Full output saved to:/i.test(text);
88
- }
89
-
90
- function previewLines(text: string, mode: "head" | "tail", limit = MAX_EXPANDED_LINES): string[] {
91
- const lines = text.replace(/\s+$/g, "").split(/\r?\n/);
92
- const selected = mode === "tail" ? lines.slice(-limit) : lines.slice(0, limit);
93
- return selected.map((line) => clip(line, MAX_LINE_CHARS));
94
- }
95
-
96
- function toolColor(toolName: string): string {
97
- switch (toolName) {
98
- case "bash":
99
- return "bashMode";
100
- case "read":
101
- return "toolTitle";
102
- case "grep":
103
- return "success";
104
- case "find":
105
- return "accent";
106
- case "ls":
107
- return "warning";
108
- case "edit":
109
- return "toolDiffAdded";
110
- case "write":
111
- return "accent";
112
- default:
113
- return "accent";
114
- }
115
- }
116
-
117
- function toolIcon(toolName: string): string {
118
- switch (toolName) {
119
- case "bash":
120
- return "⚙️";
121
- case "read":
122
- return "📖";
123
- case "grep":
124
- return "🔎";
125
- case "find":
126
- return "🧭";
127
- case "ls":
128
- return "📁";
129
- case "edit":
130
- return "✏️";
131
- case "write":
132
- return "📝";
133
- default:
134
- return "🧩";
135
- }
136
- }
137
-
138
- function callLabel(toolName: string, args: any): string {
139
- if (toolName === "bash") return clip(oneLine(args?.command), 140);
140
- if (toolName === "read") return clip(oneLine(args?.path), 140);
141
- if (toolName === "grep") {
142
- const pattern = oneLine(args?.pattern);
143
- const path = oneLine(args?.path ?? args?.glob ?? ".");
144
- return clip(`${pattern}${path ? ` in ${path}` : ""}`, 140);
145
- }
146
- if (toolName === "find") return clip(oneLine(args?.path ?? args?.pattern ?? "."), 140);
147
- if (toolName === "edit") {
148
- const count = Array.isArray(args?.edits) ? args.edits.length : args?.oldText && args?.newText ? 1 : 0;
149
- return clip(
150
- `${oneLine(args?.path ?? args?.file_path)}${count ? ` · ${count} replacement${count === 1 ? "" : "s"}` : ""}`,
151
- 140,
152
- );
153
- }
154
- if (toolName === "write") {
155
- const bytes = typeof args?.content === "string" ? Buffer.byteLength(args.content, "utf8") : 0;
156
- return clip(`${oneLine(args?.path ?? args?.file_path)}${bytes ? ` · ${bytes} bytes` : ""}`, 140);
157
- }
158
- const compactArgs = oneLine(JSON.stringify(args ?? {}));
159
- return clip(compactArgs === "{}" ? "" : compactArgs, 140);
160
- }
161
-
162
- function summaryFor(toolName: string, result: any): string {
163
- const text = textContent(result);
164
- const lines = lineCount(text);
165
- const truncated = outputWasTruncated(text) ? " · truncated" : "";
166
- if (toolName === "bash") {
167
- const exitMatch = text.match(/Exit code:\s*(-?\d+)/i) ?? text.match(/exit(?:ed)?(?: code)?\s*(-?\d+)/i);
168
- const exit = exitMatch?.[1] ?? (result?.isError ? "1" : "0");
169
- return `exit ${exit} · ${lines} line${lines === 1 ? "" : "s"}${truncated}`;
170
- }
171
- if (toolName === "read") return `${lines} line${lines === 1 ? "" : "s"}${truncated}`;
172
- if (toolName === "ls") return `${lines} item${lines === 1 ? "" : "s"}${truncated}`;
173
- if (toolName === "edit") {
174
- const diff = typeof result?.details?.diff === "string" ? result.details.diff : "";
175
- const added = diff
176
- .split(/\r?\n/)
177
- .filter((line: string) => line.startsWith("+") && !line.startsWith("+++")).length;
178
- const removed = diff
179
- .split(/\r?\n/)
180
- .filter((line: string) => line.startsWith("-") && !line.startsWith("---")).length;
181
- return diff ? `+${added} -${removed}` : `${lines} line${lines === 1 ? "" : "s"}${truncated}`;
182
- }
183
- if (toolName === "write") return `${lines} line${lines === 1 ? "" : "s"}${truncated}`;
184
- return `${lines} result${lines === 1 ? "" : "s"}${truncated}`;
185
- }
186
-
187
- type ToolBlockKind = "call" | "result" | "full";
188
-
189
- class EmptyBlock {
190
- render(): string[] {
191
- return [];
192
- }
193
- invalidate(): void {}
194
- }
195
-
196
- class ToolBlock {
197
- constructor(
198
- private readonly kind: ToolBlockKind,
199
- private readonly toolName: string,
200
- private readonly lines: string[],
201
- private readonly theme: CompactTheme,
202
- private readonly borderColor?: string,
203
- ) {}
204
-
205
- render(width: number): string[] {
206
- const renderWidth = Math.max(8, width - 1);
207
- const separator = this.theme.fg("borderMuted", "─".repeat(Math.max(8, Math.min(32, renderWidth))));
208
- const block = this.lines.map((line, index) => {
209
- if (this.kind === "call") return this.renderTop(line, renderWidth);
210
- if (this.kind === "full" && index === 0) return this.renderTop(line, renderWidth);
211
- const isLast = index === this.lines.length - 1;
212
- return isLast ? this.renderBottom(line, renderWidth) : this.renderBody(line, renderWidth);
213
- });
214
- return [separator, "", ...block, ""];
215
- }
216
-
217
- invalidate(): void {}
218
-
219
- private color(text: string): string {
220
- return this.theme.fg(this.borderColor ?? toolColor(this.toolName), text);
221
- }
222
-
223
- private fit(text: string, width: number): string {
224
- const clipped = truncateToWidth(text, Math.max(1, width), "");
225
- return `${clipped}${" ".repeat(Math.max(0, width - visibleWidth(clipped)))}`;
226
- }
227
-
228
- private renderTop(content: string, width: number): string {
229
- const prefix = this.color("╭─ ");
230
- const suffix = this.color("╮");
231
- const innerWidth = Math.max(1, width - 4);
232
- const fitted = truncateToWidth(content, innerWidth, "");
233
- const fill = this.color("─".repeat(Math.max(0, innerWidth - visibleWidth(fitted))));
234
- return `${prefix}${fitted}${fill}${suffix}`;
235
- }
236
-
237
- private renderBody(content: string, width: number): string {
238
- const innerWidth = Math.max(1, width - 2);
239
- return `${this.color("│")}${this.fit(content, innerWidth)}${this.color("│")}`;
240
- }
241
-
242
- private renderBottom(content: string, width: number): string {
243
- const prefix = this.color("╰─ ");
244
- const suffix = this.color("╯");
245
- const innerWidth = Math.max(1, width - 4);
246
- const fitted = truncateToWidth(content, innerWidth, "");
247
- const fill = this.color("─".repeat(Math.max(0, innerWidth - visibleWidth(fitted))));
248
- return `${prefix}${fitted}${fill}${suffix}`;
249
- }
250
- }
251
-
252
- function topLine(toolName: string, theme: CompactTheme, label: string): string {
253
- const color = toolColor(toolName);
254
- const title = `${toolIcon(toolName)} ${toolName}`;
255
- return `${theme.fg(color, theme.bold(title))} ${theme.fg("toolOutput", label)}`;
256
- }
257
-
258
- function midLine(_toolName: string, theme: CompactTheme, text: string): string {
259
- return theme.fg("toolOutput", text);
260
- }
261
-
262
- function bottomLine(_toolName: string, _theme: CompactTheme, text = ""): string {
263
- return text.trimEnd();
264
- }
265
-
266
- function toolText(
267
- kind: ToolBlockKind,
268
- toolName: string,
269
- lines: string[],
270
- theme: CompactTheme,
271
- borderColor?: string,
272
- ): ToolBlock {
273
- return new ToolBlock(kind, toolName, lines, theme, borderColor);
274
- }
275
-
276
- function renderCall(_toolName: string, _args: any, _theme: CompactTheme, _context: any) {
277
- return new EmptyBlock();
278
- }
279
-
280
- function renderResult(toolName: string, result: any, options: any, theme: CompactTheme, context: any) {
281
- if (options?.isPartial) {
282
- return toolText(
283
- "full",
284
- toolName,
285
- [
286
- topLine(toolName, theme, callLabel(toolName, context?.args)),
287
- bottomLine(toolName, theme, theme.fg("muted", "running…")),
288
- ],
289
- theme,
290
- "warning",
291
- );
292
- }
293
-
294
- const summary = summaryFor(toolName, result);
295
- const text = textContent(result);
296
- const failed = Boolean(context?.isError || result?.isError);
297
- const statusColor = failed ? "error" : "success";
298
- const statusIcon = failed ? "✗" : "✓";
299
- const expandHint = options?.expanded ? "" : ` ${theme.fg("dim", keyHint("app.tools.expand", "expand"))}`;
300
-
301
- const top = topLine(toolName, theme, callLabel(toolName, context?.args));
302
- const bottom = bottomLine(
303
- toolName,
304
- theme,
305
- `${theme.fg(statusColor, statusIcon)} ${theme.fg("toolOutput", summary)}${expandHint}`,
306
- );
307
- const borderColor = failed ? "error" : "success";
308
-
309
- if (!options?.expanded || !text.trim()) {
310
- return toolText("full", toolName, [top, bottom], theme, borderColor);
311
- }
312
-
313
- const diff = toolName === "edit" && typeof result?.details?.diff === "string" ? result.details.diff : "";
314
- const previewText = diff || text;
315
- const mode = toolName === "bash" ? "tail" : "head";
316
- const lines = previewLines(previewText, mode).map((line) => midLine(toolName, theme, line));
317
- if (lineCount(previewText) > MAX_EXPANDED_LINES) {
318
- const omitted = lineCount(previewText) - MAX_EXPANDED_LINES;
319
- lines.push(midLine(toolName, theme, theme.fg("dim", `… ${omitted} more line(s)`)));
320
- }
321
- lines.unshift(top);
322
- lines.push(bottomLine(toolName, theme, `${theme.fg(statusColor, statusIcon)} ${theme.fg("toolOutput", summary)}`));
323
- return toolText("full", toolName, lines, theme, borderColor);
324
- }
325
-
326
- function padVisible(text: string, width: number): string {
327
- const clipped = truncateToWidth(text, width, "");
328
- return `${clipped}${" ".repeat(Math.max(0, width - visibleWidth(clipped)))}`;
329
- }
330
-
331
- function stripUserZoneMarkers(line: string): string {
332
- return line.replaceAll(OSC133_ZONE_START, "").replaceAll(OSC133_ZONE_END, "").replaceAll(OSC133_ZONE_FINAL, "");
333
- }
334
-
335
- function randomUserMessageMarker(): string {
336
- return USER_MESSAGE_EMOJIS[Math.floor(Math.random() * USER_MESSAGE_EMOJIS.length)] ?? "✨";
337
- }
338
-
339
- function trimVisualBlankLines(lines: string[]): string[] {
340
- let start = 0;
341
- let end = lines.length;
342
- while (start < end && stripUserZoneMarkers(lines[start] ?? "").trim() === "") start++;
343
- while (end > start && stripUserZoneMarkers(lines[end - 1] ?? "").trim() === "") end--;
344
- return lines.slice(start, end);
345
- }
346
-
347
- function frameMessage(
348
- lines: string[],
349
- width: number,
350
- theme: CompactTheme,
351
- markerText: string,
352
- borderColor: string,
353
- markerColor: string,
354
- ): string[] {
355
- if (width < 6) return lines;
356
- const innerWidth = Math.max(1, width - 2);
357
- const border = (text: string) => theme.fg(borderColor, text);
358
- const marker = theme.fg(markerColor, markerText);
359
- const topFill = Math.max(0, innerWidth - visibleWidth(marker) - 2);
360
- const top = `${border("╭─")} ${marker}${border("─".repeat(topFill))}${border("╮")}`;
361
- const body = trimVisualBlankLines(lines).map(
362
- (line) => `${border("│")}${padVisible(stripUserZoneMarkers(line).trimEnd(), innerWidth)}${border("│")}`,
363
- );
364
- const bottom = `${border("╰")}${border("─".repeat(innerWidth))}${border("╯")}`;
365
- return [`${OSC133_ZONE_START}${top}`, ...body, `${OSC133_ZONE_END}${OSC133_ZONE_FINAL}${bottom}`, ""];
366
- }
367
-
368
- function frameUserMessage(lines: string[], width: number, theme: CompactTheme, markerText: string): string[] {
369
- return frameMessage(lines, width, theme, markerText, "accent", "error");
370
- }
371
-
372
- function randomAssistantMessageMarker(): string {
373
- return ASSISTANT_MESSAGE_EMOJIS[Math.floor(Math.random() * ASSISTANT_MESSAGE_EMOJIS.length)] ?? "🤖";
374
- }
375
-
376
- function _frameAssistantMessage(lines: string[], width: number, theme: CompactTheme): string[] {
377
- const marker = randomAssistantMessageMarker();
378
- return frameMessage(lines, width, theme, marker, "border", "toolTitle");
379
- }
380
-
381
- function installGenericToolRendererPatch(pi: ExtensionAPI): void {
382
- const proto = ToolExecutionComponent?.prototype as any;
383
- if (!proto || proto[TOOL_EXECUTION_PATCH_SYMBOL]) return;
384
- const originalGetCallRenderer = proto.getCallRenderer;
385
- const originalGetResultRenderer = proto.getResultRenderer;
386
- const originalGetRenderShell = proto.getRenderShell;
387
- if (
388
- typeof originalGetCallRenderer !== "function" ||
389
- typeof originalGetResultRenderer !== "function" ||
390
- typeof originalGetRenderShell !== "function"
391
- ) {
392
- return;
393
- }
394
-
395
- proto.getCallRenderer = function compactFallbackCallRenderer(this: any) {
396
- const toolName = typeof this?.toolName === "string" ? this.toolName : "tool";
397
- return (args: any, theme: CompactTheme, context: any) => renderCall(toolName, args, theme, context);
398
- };
399
-
400
- proto.getResultRenderer = function compactFallbackResultRenderer(this: any) {
401
- const toolName = typeof this?.toolName === "string" ? this.toolName : "tool";
402
- return (result: any, options: any, theme: CompactTheme, context: any) =>
403
- renderResult(toolName, result, options, theme, context);
404
- };
405
-
406
- proto.getRenderShell = function compactFallbackRenderShell(this: any) {
407
- return "self";
408
- };
409
-
410
- proto[TOOL_EXECUTION_PATCH_SYMBOL] = { originalGetCallRenderer, originalGetResultRenderer, originalGetRenderShell };
411
- pi.on("session_shutdown", () => {
412
- const state = proto[TOOL_EXECUTION_PATCH_SYMBOL];
413
- if (!state) return;
414
- proto.getCallRenderer = state.originalGetCallRenderer;
415
- proto.getResultRenderer = state.originalGetResultRenderer;
416
- proto.getRenderShell = state.originalGetRenderShell;
417
- delete proto[TOOL_EXECUTION_PATCH_SYMBOL];
418
- });
419
- }
420
-
421
- function installMessageRenderers(pi: ExtensionAPI): void {
422
- const userProto = UserMessageComponent?.prototype as any;
423
- if (userProto && !userProto[USER_MESSAGE_PATCH_SYMBOL] && typeof userProto.render === "function") {
424
- const originalRender = userProto.render as (width: number) => string[];
425
- userProto.render = function compactUserMessageRender(this: any, width: number): string[] {
426
- const box = this?.contentBox;
427
- if (box) {
428
- box.paddingY = 0;
429
- box.setBgFn?.(undefined);
430
- box.invalidate?.();
431
- }
432
- const frameWidth = Math.max(1, width - 1);
433
- if (!this[USER_MESSAGE_MARKER_SYMBOL]) this[USER_MESSAGE_MARKER_SYMBOL] = randomUserMessageMarker();
434
- const rendered = originalRender.call(this, Math.max(1, frameWidth - 2));
435
- return frameUserMessage(
436
- rendered,
437
- frameWidth,
438
- activeTheme ?? fallbackTheme(),
439
- this[USER_MESSAGE_MARKER_SYMBOL],
440
- );
441
- };
442
- userProto[USER_MESSAGE_PATCH_SYMBOL] = { originalRender };
443
- }
444
-
445
- const assistantProto = AssistantMessageComponent?.prototype as any;
446
- if (
447
- assistantProto &&
448
- !assistantProto[ASSISTANT_MESSAGE_PATCH_SYMBOL] &&
449
- typeof assistantProto.render === "function"
450
- ) {
451
- const originalRender = assistantProto.render as (width: number) => string[];
452
- assistantProto.render = function compactAssistantMessageRender(this: any, width: number): string[] {
453
- const rendered = originalRender.call(this, Math.max(1, width - 3));
454
- if (this?.hasToolCalls || rendered.length === 0) return rendered;
455
- const frameWidth = Math.max(1, width - 1);
456
- if (!this[ASSISTANT_MESSAGE_MARKER_SYMBOL])
457
- this[ASSISTANT_MESSAGE_MARKER_SYMBOL] = randomAssistantMessageMarker();
458
- return frameMessage(
459
- rendered,
460
- frameWidth,
461
- activeTheme ?? fallbackTheme(),
462
- this[ASSISTANT_MESSAGE_MARKER_SYMBOL],
463
- "border",
464
- "toolTitle",
465
- );
466
- };
467
- assistantProto[ASSISTANT_MESSAGE_PATCH_SYMBOL] = { originalRender };
468
- }
469
-
470
- pi.on("session_start", (_event, ctx) => {
471
- if (ctx.hasUI) activeTheme = ctx.ui.theme as unknown as CompactTheme;
472
- });
473
- pi.on("session_shutdown", () => {
474
- const userState = userProto?.[USER_MESSAGE_PATCH_SYMBOL];
475
- if (userState) {
476
- userProto.render = userState.originalRender;
477
- delete userProto[USER_MESSAGE_PATCH_SYMBOL];
478
- }
479
- const assistantState = assistantProto?.[ASSISTANT_MESSAGE_PATCH_SYMBOL];
480
- if (assistantState) {
481
- assistantProto.render = assistantState.originalRender;
482
- delete assistantProto[ASSISTANT_MESSAGE_PATCH_SYMBOL];
483
- }
484
- activeTheme = undefined;
485
- });
486
- }
487
-
488
- function fallbackTheme(): CompactTheme {
489
- return {
490
- fg: (_color: any, text: string) => text,
491
- bg: (_color: any, text: string) => text,
492
- bold: (text: string) => text,
493
- };
494
- }
495
-
496
- function registerDelegatingTool(
497
- pi: ExtensionAPI,
498
- name: BuiltinToolName,
499
- createTool: (cwd: string) => BuiltinTool,
500
- ): void {
501
- const cwd = process.cwd();
502
- const original = createTool(cwd);
503
- pi.registerTool({
504
- name,
505
- label: name,
506
- description: original.description,
507
- promptSnippet: (original as any).promptSnippet,
508
- parameters: original.parameters as any,
509
- renderShell: "self",
510
- async execute(id: string, params: unknown, signal?: AbortSignal, onUpdate?: unknown, ctx?: any) {
511
- return createTool(ctx?.cwd ?? cwd).execute(id, params, signal, onUpdate, ctx) as any;
512
- },
513
- renderCall(args: any, theme: CompactTheme, context: any) {
514
- return renderCall(name, args, theme, context);
515
- },
516
- renderResult(result: any, options: any, theme: CompactTheme, context: any) {
517
- return renderResult(name, result, options, theme, context);
518
- },
519
- } as any);
520
- }
11
+ import { installGenericToolRendererPatch, installMessageRenderers, registerDelegatingTool } from "./patches.js";
12
+ import type { BuiltinTool } from "./types.js";
521
13
 
522
14
  export default function compactTools(pi: ExtensionAPI): void {
523
15
  installGenericToolRendererPatch(pi);