pi-cc-extensions 0.7.8 → 0.7.10
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 +13 -2
- package/extensions/claude-code-style.ts +33 -425
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -50,9 +50,9 @@
|
|
|
50
50
|
|
|
51
51
|
`extensions/claude-code-style.ts` 提供:
|
|
52
52
|
|
|
53
|
-
- Claude Code
|
|
53
|
+
- Claude Code 风格的工具调用行与结果摘要(默认覆盖普通工具 renderer)
|
|
54
54
|
- Pi 默认输入栏
|
|
55
|
-
-
|
|
55
|
+
- 通过 `excludeRenderers` 保留指定工具的原生或扩展 renderer
|
|
56
56
|
- 工具结果的折叠与展开
|
|
57
57
|
- 类 claude code 滚动到底部 button
|
|
58
58
|
|
|
@@ -65,6 +65,17 @@
|
|
|
65
65
|
/ccstyle status # 查看状态
|
|
66
66
|
```
|
|
67
67
|
|
|
68
|
+
需要保留指定工具的原生或扩展 renderer 时,编辑 `~/.pi/agent/claude-code-style.json`:
|
|
69
|
+
|
|
70
|
+
```json
|
|
71
|
+
{
|
|
72
|
+
"enabled": true,
|
|
73
|
+
"excludeRenderers": ["edit", "write"]
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
名单使用精确工具名;修改后执行 `/reload`。`Agent` 的专用 renderer 始终保留。
|
|
78
|
+
|
|
68
79
|
### 上下文窗口查看
|
|
69
80
|
|
|
70
81
|
`extensions/context.ts` 注册 `/context`,展示当前上下文窗口的使用分布,并可进一步预览:
|
|
@@ -1,30 +1,20 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import {
|
|
3
|
-
createBashToolDefinition,
|
|
4
|
-
createEditToolDefinition,
|
|
5
|
-
createFindToolDefinition,
|
|
6
|
-
createGrepToolDefinition,
|
|
7
|
-
createLsToolDefinition,
|
|
8
|
-
createReadToolDefinition,
|
|
9
|
-
createWriteToolDefinition,
|
|
10
|
-
generateDiffString,
|
|
11
3
|
keyHint,
|
|
12
4
|
getSettingsListTheme,
|
|
13
|
-
renderDiff,
|
|
14
5
|
ToolExecutionComponent,
|
|
15
6
|
} from "@earendil-works/pi-coding-agent";
|
|
16
7
|
import { Container, SettingsList, Text, matchesKey, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
|
|
17
8
|
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
18
|
-
import { readFile, stat } from "node:fs/promises";
|
|
19
9
|
import { homedir } from "node:os";
|
|
20
|
-
import { join
|
|
10
|
+
import { join } from "node:path";
|
|
21
11
|
|
|
22
12
|
/**
|
|
23
13
|
* Claude Code Style for pi
|
|
24
14
|
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* expand only that tool.
|
|
15
|
+
* Tool calls and results use a compact fallback renderer by default. Renderers
|
|
16
|
+
* listed in excludeRenderers remain untouched. A collapsed tool result can be
|
|
17
|
+
* clicked on its Ctrl+O hint to expand only that tool.
|
|
28
18
|
*
|
|
29
19
|
* Dynamic commands:
|
|
30
20
|
* /ccstyle open interactive settings
|
|
@@ -38,6 +28,7 @@ import { join, resolve } from "node:path";
|
|
|
38
28
|
|
|
39
29
|
type Config = {
|
|
40
30
|
enabled: boolean;
|
|
31
|
+
excludeRenderers: string[];
|
|
41
32
|
};
|
|
42
33
|
|
|
43
34
|
const CONFIG_PATH = join(homedir(), ".pi", "agent", "claude-code-style.json");
|
|
@@ -48,26 +39,24 @@ function loadConfig(): Config {
|
|
|
48
39
|
try {
|
|
49
40
|
if (existsSync(CONFIG_PATH)) {
|
|
50
41
|
const parsed = JSON.parse(readFileSync(CONFIG_PATH, "utf8")) as Partial<Config>;
|
|
42
|
+
const excludeRenderers = Array.isArray(parsed.excludeRenderers)
|
|
43
|
+
? [...new Set(parsed.excludeRenderers.filter((name): name is string => typeof name === "string" && name.length > 0))]
|
|
44
|
+
: [];
|
|
51
45
|
return {
|
|
52
46
|
enabled: parsed.enabled ?? true,
|
|
47
|
+
excludeRenderers,
|
|
53
48
|
};
|
|
54
49
|
}
|
|
55
50
|
} catch {
|
|
56
51
|
// Ignore bad config and fall back to defaults.
|
|
57
52
|
}
|
|
58
|
-
return { enabled: true };
|
|
53
|
+
return { enabled: true, excludeRenderers: [] };
|
|
59
54
|
}
|
|
60
55
|
|
|
61
56
|
function saveConfig() {
|
|
62
57
|
writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
|
|
63
58
|
}
|
|
64
59
|
|
|
65
|
-
function shortenPath(path: string): string {
|
|
66
|
-
const home = homedir();
|
|
67
|
-
if (path.startsWith(home)) return `~${path.slice(home.length)}`;
|
|
68
|
-
return path;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
60
|
function oneLine(value: unknown, max = 72): string {
|
|
72
61
|
const text = String(value ?? "")
|
|
73
62
|
.replace(/\s+/g, " ")
|
|
@@ -106,46 +95,6 @@ function hasExpandableResult(text: string): boolean {
|
|
|
106
95
|
return countLines(text) > 1;
|
|
107
96
|
}
|
|
108
97
|
|
|
109
|
-
function parsePathCount(text: string): number {
|
|
110
|
-
const files = new Set<string>();
|
|
111
|
-
for (const line of text.split("\n")) {
|
|
112
|
-
const match = line.match(/^([^:\n]+):(\d+:)?/);
|
|
113
|
-
if (match?.[1]) files.add(match[1]);
|
|
114
|
-
}
|
|
115
|
-
return files.size;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
function summarizeBash(text: string): string {
|
|
119
|
-
const clean = text.trim();
|
|
120
|
-
if (!clean) return "Done";
|
|
121
|
-
const lower = clean.toLowerCase();
|
|
122
|
-
const lines = clean.split("\n").filter(Boolean);
|
|
123
|
-
const errorLine = lines.find((line) => /\b(error|failed|exception|fatal)\b/i.test(line));
|
|
124
|
-
if (errorLine) return `Failed: ${oneLine(errorLine, 88)}`;
|
|
125
|
-
if (/\b(warning|warn)\b/i.test(lower)) {
|
|
126
|
-
const warnings = lines.filter((line) => /\b(warning|warn)\b/i.test(line)).length;
|
|
127
|
-
return `Completed with ${warnings} warning${warnings === 1 ? "" : "s"}`;
|
|
128
|
-
}
|
|
129
|
-
if (/\b(success|passed|compiled|built|done)\b/i.test(lower)) return oneLine(lines.at(-1) ?? "Done", 88);
|
|
130
|
-
return lines.length > 1 ? `${lines.length} lines output: ${oneLine(lines.at(-1), 72)}` : oneLine(clean, 88);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
function summarizeEdit(text: string): string {
|
|
134
|
-
const clean = text.trim();
|
|
135
|
-
if (!clean) return "Edited";
|
|
136
|
-
const added = (clean.match(/^\+/gm) ?? []).length;
|
|
137
|
-
const removed = (clean.match(/^-/gm) ?? []).length;
|
|
138
|
-
if (added || removed) return `Updated ${added} added, ${removed} removed`;
|
|
139
|
-
return oneLine(clean, 88);
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
function summarizeDiffStats(preview: FileDiffPreview | undefined): string | undefined {
|
|
143
|
-
if (!preview?.diff) return undefined;
|
|
144
|
-
const { added, removed } = preview.stats ?? diffStats(preview.lines ?? preview.diff.split("\n"));
|
|
145
|
-
if (!added && !removed) return undefined;
|
|
146
|
-
return `Added ${added} line${added === 1 ? "" : "s"}, removed ${removed} line${removed === 1 ? "" : "s"}`;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
98
|
function toolIcon(_name: string): string {
|
|
150
99
|
return "●";
|
|
151
100
|
}
|
|
@@ -989,150 +938,6 @@ function scheduleSessionRender(): void {
|
|
|
989
938
|
const BRIGHT_GREEN = "\x1b[38;2;80;220;100m";
|
|
990
939
|
const ANSI_RESET = "\x1b[0m";
|
|
991
940
|
|
|
992
|
-
function fileAction(args: any, verb: string): string {
|
|
993
|
-
const path = shortenPath(args?.path || "...");
|
|
994
|
-
return `${verb}(${path})`;
|
|
995
|
-
}
|
|
996
|
-
|
|
997
|
-
type DiffStats = { added: number; removed: number };
|
|
998
|
-
type FileDiffPreview = {
|
|
999
|
-
diff: string;
|
|
1000
|
-
error?: string;
|
|
1001
|
-
notice?: string;
|
|
1002
|
-
stats?: DiffStats;
|
|
1003
|
-
/** Pre-split outside the render hot path. */
|
|
1004
|
-
lines?: string[];
|
|
1005
|
-
};
|
|
1006
|
-
|
|
1007
|
-
// Diff generation is intentionally bounded: it runs before the mutation, but
|
|
1008
|
-
// should never add unbounded latency or memory pressure to a tool call.
|
|
1009
|
-
const MAX_DIFF_INPUT_BYTES = 2 * 1024 * 1024;
|
|
1010
|
-
const COLLAPSED_DIFF_LINES = 12;
|
|
1011
|
-
const EXPANDED_DIFF_LINES = 40;
|
|
1012
|
-
const diffPreviewByCall = new Map<string, FileDiffPreview | undefined>();
|
|
1013
|
-
|
|
1014
|
-
function diffStats(lines: readonly string[]): DiffStats {
|
|
1015
|
-
let added = 0;
|
|
1016
|
-
let removed = 0;
|
|
1017
|
-
for (const line of lines) {
|
|
1018
|
-
if (/^\+\s*\d+\s/.test(line)) added++;
|
|
1019
|
-
else if (/^-\s*\d+\s/.test(line)) removed++;
|
|
1020
|
-
}
|
|
1021
|
-
return { added, removed };
|
|
1022
|
-
}
|
|
1023
|
-
|
|
1024
|
-
function diffPreview(diff: string): FileDiffPreview {
|
|
1025
|
-
const lines = diff.split("\n");
|
|
1026
|
-
return { diff, lines, stats: diffStats(lines) };
|
|
1027
|
-
}
|
|
1028
|
-
|
|
1029
|
-
function normalizeDiffText(text: string): string {
|
|
1030
|
-
return text.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
1031
|
-
}
|
|
1032
|
-
|
|
1033
|
-
function editEntries(args: any): Array<{ oldText: string; newText: string }> {
|
|
1034
|
-
if (Array.isArray(args?.edits)) {
|
|
1035
|
-
return args.edits.filter((edit: any) =>
|
|
1036
|
-
typeof edit?.oldText === "string" && typeof edit?.newText === "string",
|
|
1037
|
-
);
|
|
1038
|
-
}
|
|
1039
|
-
if (typeof args?.oldText === "string" && typeof args?.newText === "string") {
|
|
1040
|
-
return [{ oldText: args.oldText, newText: args.newText }];
|
|
1041
|
-
}
|
|
1042
|
-
return [];
|
|
1043
|
-
}
|
|
1044
|
-
|
|
1045
|
-
/** Build the preview before mutation, outside the renderer's hot path. */
|
|
1046
|
-
async function buildFileDiffPreview(toolName: string, args: any, cwd: string): Promise<FileDiffPreview | undefined> {
|
|
1047
|
-
const rawPath = typeof args?.path === "string" ? args.path : typeof args?.file_path === "string" ? args.file_path : "";
|
|
1048
|
-
if (!rawPath) return undefined;
|
|
1049
|
-
|
|
1050
|
-
if (toolName === "write" && typeof args?.content === "string" && Buffer.byteLength(args.content, "utf8") > MAX_DIFF_INPUT_BYTES) {
|
|
1051
|
-
return { diff: "", notice: "Diff omitted: new content exceeds 2 MiB" };
|
|
1052
|
-
}
|
|
1053
|
-
|
|
1054
|
-
const absolutePath = resolve(cwd || process.cwd(), rawPath);
|
|
1055
|
-
let before = "";
|
|
1056
|
-
try {
|
|
1057
|
-
const info = await stat(absolutePath);
|
|
1058
|
-
if (info.size > MAX_DIFF_INPUT_BYTES) {
|
|
1059
|
-
return { diff: "", notice: "Diff omitted: existing file exceeds 2 MiB" };
|
|
1060
|
-
}
|
|
1061
|
-
before = await readFile(absolutePath, "utf8");
|
|
1062
|
-
} catch (error: any) {
|
|
1063
|
-
if (error?.code !== "ENOENT") {
|
|
1064
|
-
return { diff: "", error: `Cannot read ${shortenPath(rawPath)}: ${oneLine(error, 72)}` };
|
|
1065
|
-
}
|
|
1066
|
-
if (toolName === "edit") return { diff: "", error: `File not found: ${shortenPath(rawPath)}` };
|
|
1067
|
-
}
|
|
1068
|
-
|
|
1069
|
-
before = normalizeDiffText(before);
|
|
1070
|
-
let after = before;
|
|
1071
|
-
if (toolName === "write") {
|
|
1072
|
-
if (typeof args?.content !== "string") return undefined;
|
|
1073
|
-
after = normalizeDiffText(args.content);
|
|
1074
|
-
} else {
|
|
1075
|
-
const edits = editEntries(args);
|
|
1076
|
-
if (edits.length === 0) return undefined;
|
|
1077
|
-
const replacements: Array<{ start: number; end: number; text: string }> = [];
|
|
1078
|
-
for (const edit of edits) {
|
|
1079
|
-
const oldText = normalizeDiffText(edit.oldText);
|
|
1080
|
-
const start = after.indexOf(oldText);
|
|
1081
|
-
if (start < 0) return { diff: "", error: "Preview unavailable: old text was not found" };
|
|
1082
|
-
if (after.indexOf(oldText, start + 1) >= 0) {
|
|
1083
|
-
return { diff: "", error: "Preview unavailable: old text is not unique" };
|
|
1084
|
-
}
|
|
1085
|
-
replacements.push({ start, end: start + oldText.length, text: normalizeDiffText(edit.newText) });
|
|
1086
|
-
}
|
|
1087
|
-
for (const replacement of replacements.sort((a, b) => b.start - a.start)) {
|
|
1088
|
-
after = after.slice(0, replacement.start) + replacement.text + after.slice(replacement.end);
|
|
1089
|
-
}
|
|
1090
|
-
}
|
|
1091
|
-
|
|
1092
|
-
if (Buffer.byteLength(after, "utf8") > MAX_DIFF_INPUT_BYTES) {
|
|
1093
|
-
return { diff: "", notice: "Diff omitted: resulting content exceeds 2 MiB" };
|
|
1094
|
-
}
|
|
1095
|
-
const diff = generateDiffString(before, after).diff;
|
|
1096
|
-
return diff ? diffPreview(diff) : undefined;
|
|
1097
|
-
}
|
|
1098
|
-
|
|
1099
|
-
function diffLineCount(preview: FileDiffPreview | undefined): number {
|
|
1100
|
-
if (!preview?.diff) return 0;
|
|
1101
|
-
return preview.lines?.length ?? preview.diff.split("\n").length;
|
|
1102
|
-
}
|
|
1103
|
-
|
|
1104
|
-
function hasExpandableDiff(preview: FileDiffPreview | undefined): boolean {
|
|
1105
|
-
return diffLineCount(preview) > COLLAPSED_DIFF_LINES;
|
|
1106
|
-
}
|
|
1107
|
-
|
|
1108
|
-
function renderDiffPreview(preview: FileDiffPreview | undefined, theme: any, expanded: boolean): string {
|
|
1109
|
-
if (!preview) return "";
|
|
1110
|
-
if (preview.error) return theme.fg("error", ` ${preview.error}`);
|
|
1111
|
-
if (preview.notice) return theme.fg("muted", ` ${preview.notice}`);
|
|
1112
|
-
if (!preview.diff.trim()) return "";
|
|
1113
|
-
|
|
1114
|
-
// Bound work before calling pi's renderer. Rendering only the visible raw
|
|
1115
|
-
// lines avoids repeatedly running intra-line diffing over a multi-megabyte diff.
|
|
1116
|
-
const rawLines = preview.lines ?? preview.diff.split("\n");
|
|
1117
|
-
const maxLines = expanded ? EXPANDED_DIFF_LINES : COLLAPSED_DIFF_LINES;
|
|
1118
|
-
const shown = renderDiff(rawLines.slice(0, maxLines).join("\n"))
|
|
1119
|
-
.split("\n")
|
|
1120
|
-
.map((line) => ` ${line}`);
|
|
1121
|
-
if (rawLines.length > maxLines) {
|
|
1122
|
-
shown.push(theme.fg("muted", ` … ${rawLines.length - maxLines} more diff lines`));
|
|
1123
|
-
}
|
|
1124
|
-
return shown.join("\n");
|
|
1125
|
-
}
|
|
1126
|
-
|
|
1127
|
-
function renderFileDiffPreview(_toolName: string, _args: any, theme: any, context: any): string {
|
|
1128
|
-
const state = (context.state ??= {});
|
|
1129
|
-
if (diffPreviewByCall.has(context.toolCallId)) {
|
|
1130
|
-
state.ccstyleDiffPreview = diffPreviewByCall.get(context.toolCallId);
|
|
1131
|
-
diffPreviewByCall.delete(context.toolCallId);
|
|
1132
|
-
}
|
|
1133
|
-
return renderDiffPreview(state.ccstyleDiffPreview, theme, isToolExpanded(undefined, context));
|
|
1134
|
-
}
|
|
1135
|
-
|
|
1136
941
|
function applyEnabledState(enabled: boolean, ctx: any): void {
|
|
1137
942
|
config.enabled = enabled;
|
|
1138
943
|
saveConfig();
|
|
@@ -1187,33 +992,6 @@ function renderDefault(tool: any, slot: "renderCall" | "renderResult", args: any
|
|
|
1187
992
|
return new Text(fallback, 0, 0);
|
|
1188
993
|
}
|
|
1189
994
|
|
|
1190
|
-
function createBuiltInTools(cwd: string) {
|
|
1191
|
-
return {
|
|
1192
|
-
// Keep complete definitions so wrapped tools retain prompt metadata,
|
|
1193
|
-
// argument preparation, execution mode, and native fallback renderers.
|
|
1194
|
-
read: createReadToolDefinition(cwd),
|
|
1195
|
-
bash: createBashToolDefinition(cwd),
|
|
1196
|
-
edit: createEditToolDefinition(cwd),
|
|
1197
|
-
write: createWriteToolDefinition(cwd),
|
|
1198
|
-
find: createFindToolDefinition(cwd),
|
|
1199
|
-
grep: createGrepToolDefinition(cwd),
|
|
1200
|
-
ls: createLsToolDefinition(cwd),
|
|
1201
|
-
};
|
|
1202
|
-
}
|
|
1203
|
-
|
|
1204
|
-
const toolCache = new Map<string, ReturnType<typeof createBuiltInTools>>();
|
|
1205
|
-
function getBuiltInTools(cwd: string) {
|
|
1206
|
-
let tools = toolCache.get(cwd);
|
|
1207
|
-
if (!tools) {
|
|
1208
|
-
tools = createBuiltInTools(cwd);
|
|
1209
|
-
toolCache.set(cwd, tools);
|
|
1210
|
-
}
|
|
1211
|
-
return tools;
|
|
1212
|
-
}
|
|
1213
|
-
|
|
1214
|
-
/** Tools explicitly wrapped with bespoke call/result renderers. Skip these in the generic interceptor. */
|
|
1215
|
-
const WRAPPED_BUILTINS = new Set<string>();
|
|
1216
|
-
|
|
1217
995
|
/**
|
|
1218
996
|
* Generate a descriptive label for an unknown tool from its args.
|
|
1219
997
|
* Uses the tool label + first stringable arg value.
|
|
@@ -1295,12 +1073,10 @@ function createCcstyleTool(originalTool: any): any {
|
|
|
1295
1073
|
}
|
|
1296
1074
|
|
|
1297
1075
|
/**
|
|
1298
|
-
*
|
|
1299
|
-
* pi
|
|
1300
|
-
*
|
|
1301
|
-
*
|
|
1302
|
-
* Only subagent tools keep their specialized renderers. Every other tool,
|
|
1303
|
-
* including MCP and extension tools, uses the same compact ccstyle shell.
|
|
1076
|
+
* This extension does not register tools. ToolExecutionComponent is shared by
|
|
1077
|
+
* the TUI and exported by pi; patch its renderer lookup once so tools use the
|
|
1078
|
+
* same compact fallback shell by default. Tools named in excludeRenderers keep
|
|
1079
|
+
* their original renderer; subagent rendering remains protected.
|
|
1304
1080
|
*/
|
|
1305
1081
|
const GLOBAL_TOOL_RENDER_PATCH = Symbol.for("pi.ccstyle.global-tool-render-patch");
|
|
1306
1082
|
const COMPONENT_TOOL_RENDER_MODE = Symbol.for("pi.ccstyle.component-tool-render-mode");
|
|
@@ -1326,20 +1102,33 @@ function isMcpToolDefinition(definition: any, toolName: string): boolean {
|
|
|
1326
1102
|
return toolName === "mcp" || label === "MCP" || label.startsWith("MCP: ");
|
|
1327
1103
|
}
|
|
1328
1104
|
|
|
1329
|
-
|
|
1330
|
-
|
|
1105
|
+
/** Return true when this tool must keep its original renderer. */
|
|
1106
|
+
export function preservesOriginalRenderer(
|
|
1107
|
+
extensionDefinition: any,
|
|
1108
|
+
toolName: string,
|
|
1109
|
+
builtInToolDefinition?: any,
|
|
1110
|
+
excludeRenderers: readonly string[] = config.excludeRenderers,
|
|
1111
|
+
): boolean {
|
|
1112
|
+
if (SUBAGENT_TOOL_NAMES.has(toolName)) return true;
|
|
1113
|
+
if (!excludeRenderers.includes(toolName)) return false;
|
|
1114
|
+
return [extensionDefinition, builtInToolDefinition].some((definition) =>
|
|
1115
|
+
definition?.renderShell === "self"
|
|
1116
|
+
|| typeof definition?.renderCall === "function"
|
|
1117
|
+
|| typeof definition?.renderResult === "function",
|
|
1118
|
+
);
|
|
1331
1119
|
}
|
|
1332
1120
|
|
|
1333
1121
|
function shouldGloballyStyleTool(component: any, patch: GlobalToolRenderPatch): boolean {
|
|
1334
1122
|
const selectedMode = component[COMPONENT_TOOL_RENDER_MODE];
|
|
1335
1123
|
if (typeof selectedMode === "boolean") return selectedMode;
|
|
1336
1124
|
|
|
1337
|
-
const
|
|
1125
|
+
const extensionDefinition = component.toolDefinition;
|
|
1126
|
+
const builtInDefinition = component.builtInToolDefinition;
|
|
1127
|
+
const definition = extensionDefinition ?? builtInDefinition;
|
|
1338
1128
|
const toolName = String(component.toolName || definition?.name || "");
|
|
1339
1129
|
const useCcstyle =
|
|
1340
1130
|
patch.enabled() &&
|
|
1341
|
-
!preservesOriginalRenderer(
|
|
1342
|
-
definition?.renderShell !== "self";
|
|
1131
|
+
!preservesOriginalRenderer(extensionDefinition, toolName, builtInDefinition);
|
|
1343
1132
|
// ToolExecutionComponent chooses its child shell in the constructor. Keep that
|
|
1344
1133
|
// choice stable for this row so toggling ccstyle cannot switch containers later.
|
|
1345
1134
|
component[COMPONENT_TOOL_RENDER_MODE] = useCcstyle;
|
|
@@ -1354,7 +1143,7 @@ function shouldUseSelfShell(component: any, patch: GlobalToolRenderPatch): boole
|
|
|
1354
1143
|
const toolName = String(component.toolName || definition?.name || "");
|
|
1355
1144
|
const useSelfShell =
|
|
1356
1145
|
patch.enabled() &&
|
|
1357
|
-
|
|
1146
|
+
SUBAGENT_TOOL_NAMES.has(toolName) &&
|
|
1358
1147
|
definition?.renderShell !== "self";
|
|
1359
1148
|
component[COMPONENT_TOOL_SELF_SHELL_MODE] = useSelfShell;
|
|
1360
1149
|
return useSelfShell;
|
|
@@ -1449,111 +1238,6 @@ function deactivateLegacyCompactionRendering() {
|
|
|
1449
1238
|
if (patch) patch.enabled = () => false;
|
|
1450
1239
|
}
|
|
1451
1240
|
|
|
1452
|
-
function registerWrappedTool(pi: ExtensionAPI, name: keyof ReturnType<typeof createBuiltInTools>, renderer: {
|
|
1453
|
-
call: (args: any, theme: any) => string;
|
|
1454
|
-
result: (result: any, options: any, theme: any, context: any) => string;
|
|
1455
|
-
diff?: (args: any, theme: any, context: any) => string;
|
|
1456
|
-
/** Use pi's built-in call/result renderers, including native diff panels. */
|
|
1457
|
-
defaultRenderer?: boolean;
|
|
1458
|
-
defaultResult?: boolean;
|
|
1459
|
-
diffAfterResult?: boolean;
|
|
1460
|
-
}) {
|
|
1461
|
-
WRAPPED_BUILTINS.add(name);
|
|
1462
|
-
const initial = getBuiltInTools(process.cwd())[name] as any;
|
|
1463
|
-
|
|
1464
|
-
pi.registerTool({
|
|
1465
|
-
// Spread the full definition instead of copying a subset. In particular,
|
|
1466
|
-
// edit requires prepareArguments and tools may declare executionMode.
|
|
1467
|
-
...initial,
|
|
1468
|
-
name,
|
|
1469
|
-
label: initial.label ?? name,
|
|
1470
|
-
renderShell: "self",
|
|
1471
|
-
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
1472
|
-
const tool = (getBuiltInTools(ctx.cwd)[name] as any);
|
|
1473
|
-
if (config.enabled && (name === "edit" || name === "write")) {
|
|
1474
|
-
// Await the old-file snapshot before mutation to avoid races, while
|
|
1475
|
-
// keeping all filesystem and diff work out of the TUI renderer.
|
|
1476
|
-
diffPreviewByCall.set(toolCallId, await buildFileDiffPreview(name, params, ctx.cwd));
|
|
1477
|
-
}
|
|
1478
|
-
let result: any;
|
|
1479
|
-
try {
|
|
1480
|
-
result = await tool.execute(toolCallId, params, signal, onUpdate, ctx);
|
|
1481
|
-
} catch (error) {
|
|
1482
|
-
diffPreviewByCall.delete(toolCallId);
|
|
1483
|
-
throw error;
|
|
1484
|
-
}
|
|
1485
|
-
if (name === "edit" && typeof result?.details?.diff === "string") {
|
|
1486
|
-
const actualDiff = result.details.diff as string;
|
|
1487
|
-
diffPreviewByCall.set(toolCallId,
|
|
1488
|
-
Buffer.byteLength(actualDiff, "utf8") <= MAX_DIFF_INPUT_BYTES
|
|
1489
|
-
? diffPreview(actualDiff)
|
|
1490
|
-
: { diff: "", notice: "Diff omitted: result exceeds 2 MiB" },
|
|
1491
|
-
);
|
|
1492
|
-
}
|
|
1493
|
-
return result;
|
|
1494
|
-
},
|
|
1495
|
-
renderCall(args, theme, context) {
|
|
1496
|
-
const tool = (getBuiltInTools(context.cwd)[name] as any);
|
|
1497
|
-
if (!config.enabled || renderer.defaultRenderer) {
|
|
1498
|
-
return renderDefault(tool, "renderCall", [args, theme, context], String(name));
|
|
1499
|
-
}
|
|
1500
|
-
|
|
1501
|
-
const toolName = String(name);
|
|
1502
|
-
const visualState = resolveToolVisualState(context);
|
|
1503
|
-
const isPending = visualState === "pending" || (!visualState && (context?.isPartial || context?.executionStarted));
|
|
1504
|
-
if (isPending) scheduleAnimation(context);
|
|
1505
|
-
const rawIcon = isPending ? pendingIcon(toolName) : settledIcon(toolName, visualState);
|
|
1506
|
-
const icon = visualState === "success"
|
|
1507
|
-
? `${BRIGHT_GREEN}${rawIcon}${ANSI_RESET}`
|
|
1508
|
-
: theme.fg(toolIconColor(context), rawIcon);
|
|
1509
|
-
const title = `${icon} ${theme.fg("toolTitle", renderer.call(args, theme))}`;
|
|
1510
|
-
// Prime the per-tool snapshot, but keep the call row stable while arguments
|
|
1511
|
-
// stream in. The diff is rendered only after the result is finalized below.
|
|
1512
|
-
if (renderer.diffAfterResult) renderer.diff?.(args, theme, context);
|
|
1513
|
-
return new Text(title, 0, 0);
|
|
1514
|
-
},
|
|
1515
|
-
renderResult(result, options, theme, context) {
|
|
1516
|
-
const tool = (getBuiltInTools(context.cwd)[name] as any);
|
|
1517
|
-
if (!config.enabled || renderer.defaultResult) {
|
|
1518
|
-
diffPreviewByCall.delete(context.toolCallId);
|
|
1519
|
-
return renderDefault(tool, "renderResult", [result, options, theme, context], textFromResult(result));
|
|
1520
|
-
}
|
|
1521
|
-
|
|
1522
|
-
if (options?.isPartial) {
|
|
1523
|
-
return new Text(theme.fg("muted", " ⎿ Pending…"), 0, 0);
|
|
1524
|
-
}
|
|
1525
|
-
|
|
1526
|
-
const isError = options?.isError || context?.isError;
|
|
1527
|
-
if (isError) {
|
|
1528
|
-
diffPreviewByCall.delete(context.toolCallId);
|
|
1529
|
-
if (context.state) context.state.ccstyleDiffPreview = undefined;
|
|
1530
|
-
}
|
|
1531
|
-
setToolVisualState(context, isError ? "error" : "success");
|
|
1532
|
-
const expanded = isToolExpanded(options, context);
|
|
1533
|
-
// Hydrate the cached preview before building the summary so edit uses
|
|
1534
|
-
// the authoritative result.details.diff rather than its estimate.
|
|
1535
|
-
const diff = !isError && renderer.diffAfterResult
|
|
1536
|
-
? renderer.diff?.(context.args, theme, context)
|
|
1537
|
-
: "";
|
|
1538
|
-
const rendered = renderer.result(result, { ...options, expanded }, theme, context);
|
|
1539
|
-
if (!rendered) return new Text("", 0, 0);
|
|
1540
|
-
const fullText = textFromResult(result);
|
|
1541
|
-
const preview = context?.state?.ccstyleDiffPreview as FileDiffPreview | undefined;
|
|
1542
|
-
const hint = !expanded && (hasExpandableResult(fullText) || hasExpandableDiff(preview))
|
|
1543
|
-
? expandHint(theme)
|
|
1544
|
-
: "";
|
|
1545
|
-
const body = `${rendered}${diff ? `\n${diff}` : ""}`;
|
|
1546
|
-
if (expanded) return renderExpandedToolResult(body, theme, isError, context?.lastComponent);
|
|
1547
|
-
const output = ` ⎿ ${rendered}${hint}${diff ? `\n${diff}` : ""}`;
|
|
1548
|
-
return new Text(
|
|
1549
|
-
theme.fg(isError ? "error" : "muted", output),
|
|
1550
|
-
0,
|
|
1551
|
-
0,
|
|
1552
|
-
);
|
|
1553
|
-
},
|
|
1554
|
-
});
|
|
1555
|
-
}
|
|
1556
|
-
|
|
1557
1241
|
function notePendingScrollMessage(role: unknown): void {
|
|
1558
1242
|
if (!toolMouseTui || !isFixedEditorTui(toolMouseTui) || !scrollButtonVisible) return;
|
|
1559
1243
|
if (role === "assistant") {
|
|
@@ -1608,7 +1292,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
1608
1292
|
});
|
|
1609
1293
|
|
|
1610
1294
|
pi.on("session_start", async (_event, ctx) => {
|
|
1611
|
-
diffPreviewByCall.clear();
|
|
1612
1295
|
pendingScrollMessages = 0;
|
|
1613
1296
|
assistantMessageActive = false;
|
|
1614
1297
|
ctx.ui.setStatus("ccstyle", undefined);
|
|
@@ -1633,81 +1316,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
1633
1316
|
deactivateGlobalToolRendering();
|
|
1634
1317
|
deactivateLegacyCompactionRendering();
|
|
1635
1318
|
clearAllAnimations();
|
|
1636
|
-
diffPreviewByCall.clear();
|
|
1637
|
-
toolCache.clear();
|
|
1638
|
-
});
|
|
1639
|
-
|
|
1640
|
-
registerWrappedTool(pi, "read", {
|
|
1641
|
-
call: (args) => fileAction(args, "Read"),
|
|
1642
|
-
result: (result, { expanded }) => {
|
|
1643
|
-
const text = textFromResult(result);
|
|
1644
|
-
if (!expanded) return `${countLines(text)} lines read`;
|
|
1645
|
-
return text;
|
|
1646
|
-
},
|
|
1647
|
-
});
|
|
1648
|
-
|
|
1649
|
-
registerWrappedTool(pi, "bash", {
|
|
1650
|
-
call: (args) => `Bash(${oneLine(args?.command, 86) || "..."})`,
|
|
1651
|
-
result: (result, { expanded }) => {
|
|
1652
|
-
const text = textFromResult(result).trim();
|
|
1653
|
-
if (!expanded) return summarizeBash(text);
|
|
1654
|
-
return text;
|
|
1655
|
-
},
|
|
1656
|
-
});
|
|
1657
|
-
|
|
1658
|
-
registerWrappedTool(pi, "edit", {
|
|
1659
|
-
call: (args) => fileAction(args, "Edit"),
|
|
1660
|
-
diff: (args, theme, context) => renderFileDiffPreview("edit", args, theme, context),
|
|
1661
|
-
diffAfterResult: true,
|
|
1662
|
-
result: (result, { expanded }, _theme, context) => {
|
|
1663
|
-
const text = textFromResult(result).trim();
|
|
1664
|
-
return summarizeDiffStats(context?.state?.ccstyleDiffPreview) ?? summarizeEdit(text);
|
|
1665
|
-
},
|
|
1666
|
-
});
|
|
1667
|
-
|
|
1668
|
-
registerWrappedTool(pi, "write", {
|
|
1669
|
-
call: (args) => {
|
|
1670
|
-
const path = shortenPath(args?.path || "...");
|
|
1671
|
-
const lines = args?.content ? countLines(args.content) : 0;
|
|
1672
|
-
return `Write(${path}${lines ? `, ${lines} lines` : ""})`;
|
|
1673
|
-
},
|
|
1674
|
-
diff: (args, theme, context) => renderFileDiffPreview("write", args, theme, context),
|
|
1675
|
-
diffAfterResult: true,
|
|
1676
|
-
result: (result, { expanded }, _theme, context) => {
|
|
1677
|
-
const text = textFromResult(result).trim();
|
|
1678
|
-
return summarizeDiffStats(context?.state?.ccstyleDiffPreview) ?? (text ? oneLine(text, 96) : "Written");
|
|
1679
|
-
},
|
|
1680
|
-
});
|
|
1681
|
-
|
|
1682
|
-
registerWrappedTool(pi, "find", {
|
|
1683
|
-
call: (args) => `Find(${args?.pattern || "..."}${args?.path ? ` in ${shortenPath(args.path)}` : ""})`,
|
|
1684
|
-
result: (result, { expanded }) => {
|
|
1685
|
-
const text = textFromResult(result).trim();
|
|
1686
|
-
if (!expanded) return `${countLines(text)} files`;
|
|
1687
|
-
return text;
|
|
1688
|
-
},
|
|
1689
|
-
});
|
|
1690
|
-
|
|
1691
|
-
registerWrappedTool(pi, "grep", {
|
|
1692
|
-
call: (args) => `Grep(${args?.pattern ? `/${oneLine(args.pattern, 48)}/` : "..."})`,
|
|
1693
|
-
result: (result, { expanded }) => {
|
|
1694
|
-
const text = textFromResult(result).trim();
|
|
1695
|
-
if (!expanded) {
|
|
1696
|
-
const matches = countLines(text);
|
|
1697
|
-
const files = parsePathCount(text);
|
|
1698
|
-
return files ? `Found ${matches} matches in ${files} files` : `${matches} matches`;
|
|
1699
|
-
}
|
|
1700
|
-
return text;
|
|
1701
|
-
},
|
|
1702
|
-
});
|
|
1703
|
-
|
|
1704
|
-
registerWrappedTool(pi, "ls", {
|
|
1705
|
-
call: (args) => `List(${shortenPath(args?.path || ".")})`,
|
|
1706
|
-
result: (result, { expanded }) => {
|
|
1707
|
-
const text = textFromResult(result).trim();
|
|
1708
|
-
if (!expanded) return `${countLines(text)} entries`;
|
|
1709
|
-
return text;
|
|
1710
|
-
},
|
|
1711
1319
|
});
|
|
1712
1320
|
|
|
1713
1321
|
}
|