pi-cc-extensions 0.8.53 → 0.8.55

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.
Files changed (42) hide show
  1. package/README.en.md +12 -17
  2. package/README.md +18 -17
  3. package/assets/readme/hero.en.svg +101 -0
  4. package/extensions/config/config.ts +25 -17
  5. package/extensions/config/panel.ts +100 -4
  6. package/extensions/{utils/agent-summary.ts → feature/agent-summary/core.ts} +39 -47
  7. package/extensions/feature/{agent-summary.ts → agent-summary/index.ts} +2 -6
  8. package/extensions/feature/compact-thinking.ts +8 -3
  9. package/extensions/feature/{session-reference.ts → reference/index.ts} +1 -1
  10. package/extensions/feature/{agent-autocomplete.ts → reference/subagent.ts} +0 -1
  11. package/extensions/feature/{pi-startup-header.ts → shell/startup-header.ts} +2 -42
  12. package/extensions/feature/{working-message.ts → shell/working-message.ts} +5 -11
  13. package/extensions/index.ts +28 -38
  14. package/extensions/renderer/compact-mode.ts +394 -101
  15. package/extensions/renderer/default-mode.ts +747 -0
  16. package/extensions/renderer/index.ts +58 -775
  17. package/extensions/renderer/{mouse-hover.ts → mouse/hover.ts} +8 -6
  18. package/extensions/renderer/{mouse-interaction.ts → mouse/interaction.ts} +37 -36
  19. package/extensions/renderer/{mouse-layout.ts → mouse/layout.ts} +5 -21
  20. package/extensions/renderer/{mouse-packets.ts → mouse/packets.ts} +1 -1
  21. package/extensions/renderer/{mouse-scroll.ts → mouse/scroll.ts} +49 -36
  22. package/extensions/renderer/{tool-diff → tool/diff}/diff-renderer.ts +10 -45
  23. package/extensions/renderer/{tool-diff → tool/diff}/line-width-safety.ts +1 -1
  24. package/extensions/renderer/tool/diff/write-display-utils.ts +12 -0
  25. package/extensions/renderer/{tool-grouping.ts → tool/grouping.ts} +20 -16
  26. package/extensions/renderer/{tool-result.ts → tool/result.ts} +35 -27
  27. package/extensions/utils/ansi-color.ts +44 -0
  28. package/extensions/utils/format.ts +21 -0
  29. package/extensions/utils/fullscreen-detect.ts +0 -27
  30. package/package.json +4 -2
  31. package/extensions/renderer/tool-diff/write-display-utils.ts +0 -29
  32. /package/extensions/feature/{session-reference-core.ts → reference/session.ts} +0 -0
  33. /package/extensions/feature/{pi-aliases.ts → shell/aliases.ts} +0 -0
  34. /package/extensions/{feature → renderer}/markdown-enhance.ts +0 -0
  35. /package/extensions/renderer/{tool-diff → tool/diff}/ATTRIBUTION.md +0 -0
  36. /package/extensions/renderer/{tool-diff → tool/diff}/ansi-utils.ts +0 -0
  37. /package/extensions/renderer/{tool-diff → tool/diff}/diff-presentation.ts +0 -0
  38. /package/extensions/renderer/{tool-diff → tool/diff}/index.ts +0 -0
  39. /package/extensions/renderer/{tool-diff → tool/diff}/render-utils.ts +0 -0
  40. /package/extensions/renderer/{tool-diff → tool/diff}/shiki-highlight.ts +0 -0
  41. /package/extensions/renderer/{tool-diff → tool/diff}/types.ts +0 -0
  42. /package/extensions/renderer/{tool-diff → tool/diff}/write-execution.ts +0 -0
@@ -0,0 +1,747 @@
1
+ /**
2
+ * default mode(config.mode === "on"):Claude Code 风格工具卡渲染。
3
+ *
4
+ * 经 ToolExecutionComponent 原型补丁接管 call/result;edit/write 走 rich diff。
5
+ * 生命周期对齐 compact-mode:installDefaultMode → hooks.shutdown。
6
+ */
7
+ import { ToolExecutionComponent } from "@earendil-works/pi-coding-agent";
8
+ import { Text, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
9
+ import { config, getToolDisplayConfig, type CompactStyleMode } from "../config/config.ts";
10
+ import { isToolCallHovered } from "./mouse/hover.ts";
11
+ import {
12
+ countLines,
13
+ hasExpandableDetail,
14
+ headTruncateToWidth,
15
+ insetComponent,
16
+ isToolExpanded,
17
+ outputLineCount,
18
+ pendingIcon,
19
+ renderCollapsedToolResultToWidth,
20
+ renderExpandedToolResult,
21
+ resolveToolVisualState,
22
+ scheduleAnimation,
23
+ settledIcon,
24
+ setToolVisualState,
25
+ textFromResult,
26
+ toolIconColor,
27
+ toolViewportWidth,
28
+ } from "./tool/result.ts";
29
+ import { oneLine } from "../utils/format.ts";
30
+ import { showMoreHintText } from "./show-more-hint.ts";
31
+ import { renderRichToolResult, type WriteExecutionMetadataStore } from "./tool/diff/index.ts";
32
+ import { getMessageDisplayTheme } from "./message-display.ts";
33
+
34
+ // 成功勾:亮绿 truecolor(与 message-display 一致)
35
+ const BRIGHT_GREEN = "\x1b[38;2;80;220;100m";
36
+ const ANSI_FG_RESET = "\x1b[39m";
37
+
38
+ const GLOBAL_TOOL_RENDER_PATCH = Symbol.for("pi.ccstyle.global-tool-render-patch");
39
+ const COMPONENT_TOOL_RENDER_MODE = Symbol.for("pi.ccstyle.component-tool-render-mode");
40
+ const COMPONENT_TOOL_SELF_SHELL_MODE = Symbol.for("pi.ccstyle.component-tool-self-shell-mode");
41
+ const TOOL_EXPANDED_BACKGROUND_PATCH = Symbol.for("pi.ccstyle.tool-expanded-background-patch");
42
+
43
+ const AGENT_FAMILY_TOOL_NAMES = new Set([
44
+ "Agent",
45
+ "Agents",
46
+ "get_subagent_result",
47
+ "steer_subagent",
48
+ ]);
49
+ // pi-subagents 等扩展为 Agent 提供专用渲染器,ccstyle 必须保留。
50
+ const DEDICATED_RENDERER_TOOLS = new Set(["Agent"]);
51
+
52
+ export type DefaultModeHooks = {
53
+ /** 本安装是否仍持有全局工具渲染补丁。 */
54
+ isOwner(): boolean;
55
+ shutdown(): void;
56
+ };
57
+
58
+ type ToolRenderMethods = {
59
+ hasRendererDefinition: (...args: any[]) => boolean;
60
+ getRenderShell: (...args: any[]) => "default" | "self";
61
+ getCallRenderer: (...args: any[]) => any;
62
+ getResultRenderer: (...args: any[]) => any;
63
+ };
64
+
65
+ type GlobalToolRenderPatch = {
66
+ version: 2;
67
+ prototype: any;
68
+ owner: object;
69
+ active: boolean;
70
+ enabled: () => boolean;
71
+ mode: () => CompactStyleMode;
72
+ wrap: (tool: any) => any;
73
+ byDefinition: WeakMap<object, any>;
74
+ byName: Map<string, any>;
75
+ downstream: ToolRenderMethods;
76
+ installed: ToolRenderMethods;
77
+ // 兼容旧扩展读取 Symbol 上的 legacy 字段。
78
+ originalHasRendererDefinition: ToolRenderMethods["hasRendererDefinition"];
79
+ originalGetRenderShell: ToolRenderMethods["getRenderShell"];
80
+ originalGetCallRenderer: ToolRenderMethods["getCallRenderer"];
81
+ originalGetResultRenderer: ToolRenderMethods["getResultRenderer"];
82
+ };
83
+
84
+ type ToolExpandedBackgroundPatch = {
85
+ active: boolean;
86
+ prototype: any;
87
+ installed: (...args: any[]) => void;
88
+ original: (...args: any[]) => void;
89
+ dispose: () => void;
90
+ };
91
+
92
+ export function shouldRenderRichDiff(
93
+ mode: CompactStyleMode,
94
+ toolName: string,
95
+ isError: boolean,
96
+ ): boolean {
97
+ return mode === "on" && !isError && (toolName === "edit" || toolName === "write");
98
+ }
99
+
100
+ export function isMcpToolDefinition(definition: any, toolName: string): boolean {
101
+ const label = typeof definition?.label === "string" ? definition.label.trim() : "";
102
+ if (/^MCP(?::|$)/i.test(label)) return true;
103
+ if (toolName === "mcp" || /^mcp[_:-]|[_:-]mcp[_:-]/i.test(toolName)) return true;
104
+ if (label) return false;
105
+ const description = typeof definition?.description === "string" ? definition.description : "";
106
+ return /\bModel Context Protocol\b/i.test(description);
107
+ }
108
+
109
+ export function humanizeMcpToolName(toolName: string): string {
110
+ const words = toolName
111
+ .replace(/^mcp(?:[_:-]+)+/i, "")
112
+ .split(/[_:-]+/)
113
+ .filter(Boolean);
114
+ return words.length
115
+ ? words.map((word) => word[0]!.toUpperCase() + word.slice(1)).join(" ")
116
+ : "MCP";
117
+ }
118
+
119
+ /** 排除名单内且自带 renderer 的工具保留原渲染。 */
120
+ export function preservesOriginalRenderer(
121
+ extensionDefinition: any,
122
+ toolName: string,
123
+ builtInToolDefinition?: any,
124
+ excludeRenderers: readonly string[] = config.excludeRenderers,
125
+ ): boolean {
126
+ if (!excludeRenderers.includes(toolName)) return false;
127
+ return [extensionDefinition, builtInToolDefinition].some(
128
+ (definition) =>
129
+ definition?.renderShell === "self" ||
130
+ typeof definition?.renderCall === "function" ||
131
+ typeof definition?.renderResult === "function",
132
+ );
133
+ }
134
+
135
+ function renderDefault(tool: any, slot: "renderCall" | "renderResult", args: any[], fallback = "") {
136
+ try {
137
+ if (typeof tool?.[slot] === "function") return tool[slot](...args);
138
+ } catch {
139
+ // Fall through to raw fallback.
140
+ }
141
+ return new Text(fallback, 0, 0);
142
+ }
143
+
144
+ function humanizeToolLabel(label: string): string {
145
+ return label
146
+ .replace(/([a-z0-9])([A-Z])/g, "$1 $2")
147
+ .replace(/[_-]+/g, " ")
148
+ .replace(/\b\w/g, (char) => char.toUpperCase());
149
+ }
150
+
151
+ function singleToolCallSummary(
152
+ toolName: string,
153
+ label: string,
154
+ args: any,
155
+ ): { main: string; detail: string } {
156
+ const title = label === toolName ? humanizeToolLabel(label) : label;
157
+ if (!args || typeof args !== "object") return { main: title, detail: "" };
158
+ const name = toolName.toLowerCase();
159
+ const value = (fallback: string, ...keys: string[]) => {
160
+ const found = keys.map((key) => args[key]).find((item) => typeof item === "string" && item);
161
+ // 与多 tool 摘要对齐:从头截断,上限 96 字符(tool-grouping oneLine 默认值)。
162
+ return `${title} ${oneLine(found || fallback, 96)}`;
163
+ };
164
+ if (AGENT_FAMILY_TOOL_NAMES.has(toolName) && args.agent_id) {
165
+ return { main: `${title} ${oneLine(args.agent_id, 96)}`, detail: "" };
166
+ }
167
+ // Agents 走 ccstyle wrapper;Agent 保留专用渲染器。
168
+ if (name === "agents") {
169
+ return {
170
+ main: value("launch agents", "description", "prompt"),
171
+ detail: "",
172
+ };
173
+ }
174
+ if (name === "skill") return { main: value("run skill", "name"), detail: "" };
175
+ if (name === "enterplanmode" || name === "enter_plan_mode") {
176
+ return { main: `${title} enable read-only planning`, detail: "" };
177
+ }
178
+ if (name === "exitplanmode" || name === "exit_plan_mode") {
179
+ return { main: `${title} present plan`, detail: "" };
180
+ }
181
+ if (name === "taskcreate") return { main: value("create task", "subject"), detail: "" };
182
+ if (name === "tasklist") return { main: `${title} task list`, detail: "" };
183
+ if (name === "taskget" || name === "taskupdate") {
184
+ return { main: value("task", "taskId", "task_id"), detail: "" };
185
+ }
186
+ if (name === "taskoutput" || name === "taskstop") {
187
+ return { main: value("background task", "task_id", "taskId"), detail: "" };
188
+ }
189
+ if (name === "taskexecute") {
190
+ const ids = Array.isArray(args.task_ids)
191
+ ? args.task_ids
192
+ : Array.isArray(args.taskIds)
193
+ ? args.taskIds
194
+ : [];
195
+ const summary = ids.length
196
+ ? `${ids[0]}${ids.length > 1 ? ` (+${ids.length - 1} tasks)` : ""}`
197
+ : "start tasks";
198
+ return { main: `${title} ${summary}`, detail: "" };
199
+ }
200
+ if (toolName === "read") {
201
+ const details = [
202
+ args.offset !== undefined ? `offset=${args.offset}` : "",
203
+ args.limit !== undefined ? `limit=${args.limit}` : "",
204
+ ].filter(Boolean);
205
+ return {
206
+ main: `${title}${args.path ? ` ${oneLine(args.path, 96)}` : ""}`,
207
+ detail: details.length ? ` (${details.join(", ")})` : "",
208
+ };
209
+ }
210
+ const preferred =
211
+ args.path ??
212
+ args.file_path ??
213
+ args.command ??
214
+ args.query ??
215
+ args.question ??
216
+ args.pattern ??
217
+ args.url ??
218
+ args.name ??
219
+ args.tool_use_id ??
220
+ args.toolCallId ??
221
+ args.id ??
222
+ args.message;
223
+ return {
224
+ main:
225
+ preferred !== undefined && preferred !== null && typeof preferred !== "object"
226
+ ? `${title} ${oneLine(preferred, 96)}`
227
+ : title,
228
+ detail: "",
229
+ };
230
+ }
231
+
232
+ type ParsedTask = { id: string; status: string; subject: string };
233
+
234
+ function parseTaskList(text: string): ParsedTask[] {
235
+ return text
236
+ .split("\n")
237
+ .map((line) => line.match(/^#(\d+) \[([^\]]+)] (.+)$/))
238
+ .filter((match): match is RegExpMatchArray => Boolean(match))
239
+ .map((match) => ({ id: match[1]!, status: match[2]!, subject: match[3]! }));
240
+ }
241
+
242
+ function taskListSummary(tasks: ParsedTask[]): string {
243
+ const counts = { pending: 0, in_progress: 0, completed: 0 };
244
+ for (const task of tasks) {
245
+ if (task.status in counts) counts[task.status as keyof typeof counts]++;
246
+ }
247
+ return [
248
+ `${tasks.length} tasks`,
249
+ counts.in_progress ? `${counts.in_progress} in progress` : "",
250
+ counts.pending ? `${counts.pending} pending` : "",
251
+ counts.completed ? `${counts.completed} completed` : "",
252
+ ]
253
+ .filter(Boolean)
254
+ .join(" • ");
255
+ }
256
+
257
+ function renderExpandedTaskResult(
258
+ toolName: string,
259
+ text: string,
260
+ theme: any,
261
+ isError: boolean,
262
+ ): any | undefined {
263
+ if (isError) return undefined;
264
+ if (toolName === "TaskList") {
265
+ const tasks = parseTaskList(text);
266
+ if (!tasks.length) return undefined;
267
+ const limit = Math.max(1, config.expandedPreviewMaxLines);
268
+ const rows = tasks.slice(0, limit).map((task) => {
269
+ const color =
270
+ task.status === "completed"
271
+ ? "success"
272
+ : task.status === "in_progress"
273
+ ? "warning"
274
+ : "muted";
275
+ return `${theme.fg("accent", `#${task.id}`)} ${theme.fg(color, task.status)} ${theme.fg("dim", task.subject)}`;
276
+ });
277
+ if (tasks.length > rows.length)
278
+ rows.push(theme.fg("muted", `… ${tasks.length - rows.length} more tasks`));
279
+ // 贴左:外层展开卡片 Box(1,1) 提供 1 格 padding
280
+ return new Text(
281
+ `↳ ${theme.fg("muted", taskListSummary(tasks))}\n${rows.map((r) => ` ${r}`).join("\n")}`,
282
+ 0,
283
+ 0,
284
+ );
285
+ }
286
+ const line = text.trim();
287
+ if (!line || line.includes("\n")) return undefined;
288
+ let formatted: string | undefined;
289
+ let match: RegExpMatchArray | null;
290
+ if (
291
+ toolName === "TaskCreate" &&
292
+ (match = line.match(/^Task #(\d+) created successfully: (.+)$/))
293
+ ) {
294
+ formatted = `${theme.fg("success", "Created task")} ${theme.fg("accent", `#${match[1]}`)} ${theme.fg("muted", match[2])}`;
295
+ } else if (toolName === "TaskUpdate" && (match = line.match(/^Updated task #(\d+) (.+)$/))) {
296
+ formatted = `${theme.fg("success", "Updated task")} ${theme.fg("accent", `#${match[1]}`)} ${theme.fg("muted", match[2])}`;
297
+ } else if (toolName === "TaskExecute") {
298
+ formatted = `${theme.fg("success", "Started")} ${theme.fg("muted", line)}`;
299
+ } else if (toolName === "TaskStop") {
300
+ formatted = `${theme.fg("success", "Stopped")} ${theme.fg("muted", line)}`;
301
+ }
302
+ return formatted ? new Text(`↳ ${formatted}`, 0, 0) : undefined;
303
+ }
304
+
305
+ /** 用 ccstyle call/result 包装任意工具定义。 */
306
+ function createCcstyleTool(
307
+ originalTool: any,
308
+ writeExecutionMetadata: WriteExecutionMetadataStore,
309
+ ): any {
310
+ const toolName = originalTool.name;
311
+ const label = isMcpToolDefinition(originalTool, toolName)
312
+ ? humanizeMcpToolName(toolName)
313
+ : originalTool.label || toolName;
314
+
315
+ return {
316
+ ...originalTool,
317
+ renderShell: "self",
318
+ renderCall(args: any, theme: any, context: any) {
319
+ if (config.mode !== "on") {
320
+ return renderDefault(originalTool, "renderCall", [args, theme, context], String(toolName));
321
+ }
322
+
323
+ const visualState = resolveToolVisualState(context);
324
+ const isPending =
325
+ visualState === "pending" ||
326
+ (!visualState && (context?.isPartial || context?.executionStarted));
327
+ if (isPending) scheduleAnimation(context);
328
+ const rawIcon = isPending ? pendingIcon(toolName) : settledIcon(toolName, visualState);
329
+ const icon =
330
+ visualState === "success"
331
+ ? `${BRIGHT_GREEN}${rawIcon}${ANSI_FG_RESET}`
332
+ : theme.fg(toolIconColor(context), rawIcon);
333
+ const summary = singleToolCallSummary(toolName, label, args);
334
+ let cachedWidth: number | undefined;
335
+ let cachedLine: string | undefined;
336
+ const expanded = Boolean(context?.expanded);
337
+ return {
338
+ render(width: number) {
339
+ if (cachedLine !== undefined && cachedWidth === width) return [cachedLine];
340
+ const viewportWidth = toolViewportWidth(width);
341
+ // 展开态贴左(外层 Box 已 pad 1);折叠 self-shell 保留 1 格前导空格
342
+ const lead = expanded ? "" : " ";
343
+ const callWidth = Math.max(
344
+ 0,
345
+ viewportWidth - visibleWidth(icon) - 1 - (expanded ? 0 : 1),
346
+ );
347
+ const mainWidth = Math.max(0, callWidth - visibleWidth(summary.detail));
348
+ cachedWidth = width;
349
+ // 纯文本先截断再着色(省略号不带 ANSI);从头截断,与多 tool 一致
350
+ cachedLine = `${lead}${icon} ${theme.fg("toolTitle", headTruncateToWidth(summary.main, mainWidth))}${theme.fg("dim", summary.detail)}`;
351
+ return [truncateToWidth(cachedLine, viewportWidth, "")];
352
+ },
353
+ invalidate() {},
354
+ };
355
+ },
356
+ renderResult(result: any, options: any, theme: any, context: any) {
357
+ if (config.mode !== "on") {
358
+ return renderDefault(
359
+ originalTool,
360
+ "renderResult",
361
+ [result, options, theme, context],
362
+ textFromResult(result),
363
+ );
364
+ }
365
+
366
+ const expanded = isToolExpanded(options, context);
367
+ if (options?.isPartial) {
368
+ // 展开态贴左(Box 提供 1 格 pad);折叠态保持 3 格对齐标题
369
+ const pending = expanded ? "↳ Pending…" : " ↳ Pending…";
370
+ return new Text(theme.fg("muted", pending), 0, 0);
371
+ }
372
+
373
+ const isError = options?.isError || context?.isError;
374
+ setToolVisualState(context, isError ? "error" : "success");
375
+ const toolCallId = context?.toolCallId;
376
+ if (shouldRenderRichDiff(config.mode, toolName, Boolean(isError))) {
377
+ // getter 保证 Diff indicator / wrap / limits 下次绘制即更新
378
+ const richResult = renderRichToolResult(
379
+ toolName,
380
+ result,
381
+ {
382
+ ...options,
383
+ expanded,
384
+ // 全局共享状态避免 /reload 后旧 result renderer 闭包失联
385
+ isHovered: () => isToolCallHovered(toolCallId),
386
+ },
387
+ theme,
388
+ context,
389
+ writeExecutionMetadata,
390
+ getToolDisplayConfig,
391
+ );
392
+ // 展开态由 Box(1,1) 提供内边距;折叠态 inset 一级缩进
393
+ if (richResult) return expanded ? richResult : insetComponent(richResult);
394
+ }
395
+
396
+ const text = textFromResult(result, expanded);
397
+ const args = context?.args;
398
+ if (expanded) {
399
+ const taskResult = renderExpandedTaskResult(toolName, text, theme, Boolean(isError));
400
+ if (taskResult) return taskResult;
401
+ }
402
+ const tasks = !isError && toolName === "TaskList" ? parseTaskList(text) : [];
403
+ const outputLines = outputLineCount(result) || countLines(text);
404
+ const lineWord = outputLines === 1 ? "line" : "lines";
405
+ const action = toolName === "read" ? "loaded" : "returned";
406
+ const rendered = tasks.length
407
+ ? taskListSummary(tasks)
408
+ : isError
409
+ ? text
410
+ ? // 72 与原 result.ts 默认一致,保持错误摘要截断宽度。
411
+ oneLine(text, 72)
412
+ : "Failed"
413
+ : outputLines
414
+ ? `${outputLines} ${lineWord} ${action}`
415
+ : "Done";
416
+ const expandable = !expanded && (tasks.length > 0 || hasExpandableDetail(text, args));
417
+ const hintText = showMoreHintText();
418
+ const hintPrefix = expandable ? theme.fg("dim", " • ") : "";
419
+ const hint = expandable ? hintPrefix + theme.fg("dim", hintText) : "";
420
+ const hoveredHint = expandable ? hintPrefix + theme.fg("text", hintText) : "";
421
+ if (expanded) {
422
+ return renderExpandedToolResult(
423
+ text || "",
424
+ theme,
425
+ Boolean(isError),
426
+ context?.lastComponent,
427
+ args,
428
+ context,
429
+ true, // mode=on:贴左,由外层 Box(1,1) 提供 1 格 padding
430
+ );
431
+ }
432
+ if (context?.state) context.state.ccstyleIoView = undefined;
433
+ let cachedWidth: number | undefined;
434
+ let cachedLine: string | undefined;
435
+ let cachedHoveredLine: string | undefined;
436
+ return {
437
+ render(width: number) {
438
+ if (cachedLine === undefined || cachedWidth !== width) {
439
+ cachedWidth = width;
440
+ cachedLine = theme.fg(
441
+ isError ? "error" : "muted",
442
+ renderCollapsedToolResultToWidth(rendered, hint, width),
443
+ );
444
+ cachedHoveredLine = theme.fg(
445
+ isError ? "error" : "muted",
446
+ renderCollapsedToolResultToWidth(rendered, hoveredHint, width),
447
+ );
448
+ }
449
+ return [isToolCallHovered(toolCallId) ? cachedHoveredLine! : cachedLine];
450
+ },
451
+ invalidate() {},
452
+ };
453
+ },
454
+ };
455
+ }
456
+
457
+ function syncToolShell(component: any, shell: "default" | "self"): void {
458
+ const target = shell === "self" ? component.selfRenderContainer : component.contentBox;
459
+ if (!target || !Array.isArray(component.children)) return;
460
+ const candidates = new Set(
461
+ [component.contentText, component.contentBox, component.selfRenderContainer].filter(Boolean),
462
+ );
463
+ const indexes = component.children
464
+ .map((child: any, index: number) => (candidates.has(child) ? index : -1))
465
+ .filter((index: number) => index >= 0);
466
+ const targetIndex = indexes[0];
467
+ // 构造期 getRenderShell 先于 Pi 挂载 shell;此处勿挂载,否则构造器会二次添加。
468
+ if (targetIndex === undefined) return;
469
+ component.children[targetIndex] = target;
470
+ for (const index of indexes.sort((left: number, right: number) => right - left)) {
471
+ if (index !== targetIndex) component.children.splice(index, 1);
472
+ }
473
+ }
474
+
475
+ function shouldGloballyStyleTool(component: any, patch: GlobalToolRenderPatch): boolean {
476
+ const extensionDefinition = component.toolDefinition;
477
+ const builtInDefinition = component.builtInToolDefinition;
478
+ const definition = extensionDefinition ?? builtInDefinition;
479
+ const toolName = String(component.toolName || definition?.name || "");
480
+ const useCcstyle =
481
+ patch.mode() === "on" &&
482
+ !DEDICATED_RENDERER_TOOLS.has(toolName) &&
483
+ !preservesOriginalRenderer(extensionDefinition, toolName, builtInDefinition);
484
+ component[COMPONENT_TOOL_RENDER_MODE] = useCcstyle;
485
+ return useCcstyle;
486
+ }
487
+
488
+ function shouldUseSelfShell(component: any, _patch: GlobalToolRenderPatch): boolean {
489
+ component[COMPONENT_TOOL_SELF_SHELL_MODE] = false;
490
+ return false;
491
+ }
492
+
493
+ function getGloballyStyledTool(component: any, patch: GlobalToolRenderPatch): any {
494
+ const definition = component.toolDefinition ?? component.builtInToolDefinition;
495
+ if (definition && typeof definition === "object") {
496
+ let wrapped = patch.byDefinition.get(definition);
497
+ if (!wrapped) {
498
+ wrapped = patch.wrap(definition);
499
+ patch.byDefinition.set(definition, wrapped);
500
+ }
501
+ return wrapped;
502
+ }
503
+
504
+ const name = String(component.toolName || "tool");
505
+ let wrapped = patch.byName.get(name);
506
+ if (!wrapped) {
507
+ wrapped = patch.wrap({ name, label: name });
508
+ patch.byName.set(name, wrapped);
509
+ }
510
+ return wrapped;
511
+ }
512
+
513
+ function prototypeToolRenderMethods(prototype: any): ToolRenderMethods {
514
+ return {
515
+ hasRendererDefinition: prototype.hasRendererDefinition,
516
+ getRenderShell: prototype.getRenderShell,
517
+ getCallRenderer: prototype.getCallRenderer,
518
+ getResultRenderer: prototype.getResultRenderer,
519
+ };
520
+ }
521
+
522
+ function isOwnershipAwarePatch(value: any): value is GlobalToolRenderPatch {
523
+ if (!value || value.version !== 2 || !value.installed || !value.downstream) return false;
524
+ return ["hasRendererDefinition", "getRenderShell", "getCallRenderer", "getResultRenderer"].every(
525
+ (name) =>
526
+ typeof value.installed[name] === "function" && typeof value.downstream[name] === "function",
527
+ );
528
+ }
529
+
530
+ function isLegacyInstalledWrapper(method: unknown, downstreamField: string): boolean {
531
+ if (typeof method !== "function") return false;
532
+ try {
533
+ const source = Function.prototype.toString.call(method);
534
+ return (
535
+ source.includes(downstreamField) &&
536
+ (source.includes("shouldGloballyStyleTool") ||
537
+ source.includes("shouldUseSelfShell") ||
538
+ source.includes("getGloballyStyledTool"))
539
+ );
540
+ } catch {
541
+ return false;
542
+ }
543
+ }
544
+
545
+ function downstreamForGlobalToolInstall(prototype: any, previous: any): ToolRenderMethods {
546
+ const current = prototypeToolRenderMethods(prototype);
547
+ if (!previous || previous.prototype !== prototype) return current;
548
+ if (isOwnershipAwarePatch(previous)) {
549
+ return {
550
+ hasRendererDefinition:
551
+ current.hasRendererDefinition === previous.installed.hasRendererDefinition
552
+ ? previous.downstream.hasRendererDefinition
553
+ : current.hasRendererDefinition,
554
+ getRenderShell:
555
+ current.getRenderShell === previous.installed.getRenderShell
556
+ ? previous.downstream.getRenderShell
557
+ : current.getRenderShell,
558
+ getCallRenderer:
559
+ current.getCallRenderer === previous.installed.getCallRenderer
560
+ ? previous.downstream.getCallRenderer
561
+ : current.getCallRenderer,
562
+ getResultRenderer:
563
+ current.getResultRenderer === previous.installed.getResultRenderer
564
+ ? previous.downstream.getResultRenderer
565
+ : current.getResultRenderer,
566
+ };
567
+ }
568
+
569
+ // pre-v2 Symbol 无 wrapper 引用;能识别则回退,否则保留当前方法为 external。
570
+ const legacyDownstream = (method: Function, field: string): Function => {
571
+ const saved = previous[field];
572
+ return typeof saved === "function" && isLegacyInstalledWrapper(method, field) ? saved : method;
573
+ };
574
+ return {
575
+ hasRendererDefinition: legacyDownstream(
576
+ current.hasRendererDefinition,
577
+ "originalHasRendererDefinition",
578
+ ) as ToolRenderMethods["hasRendererDefinition"],
579
+ getRenderShell: legacyDownstream(
580
+ current.getRenderShell,
581
+ "originalGetRenderShell",
582
+ ) as ToolRenderMethods["getRenderShell"],
583
+ getCallRenderer: legacyDownstream(
584
+ current.getCallRenderer,
585
+ "originalGetCallRenderer",
586
+ ) as ToolRenderMethods["getCallRenderer"],
587
+ getResultRenderer: legacyDownstream(
588
+ current.getResultRenderer,
589
+ "originalGetResultRenderer",
590
+ ) as ToolRenderMethods["getResultRenderer"],
591
+ };
592
+ }
593
+
594
+ function disconnectGlobalToolRenderPatch(patch: any): void {
595
+ if (!patch || typeof patch !== "object") return;
596
+ patch.active = false;
597
+ patch.enabled = () => false;
598
+ patch.mode = () => "off";
599
+ patch.wrap = (tool: any) => tool;
600
+ patch.byDefinition = new WeakMap();
601
+ if (patch.byName && typeof patch.byName.clear === "function") patch.byName.clear();
602
+ else patch.byName = new Map();
603
+ }
604
+
605
+ function installGlobalToolRendering(
606
+ writeExecutionMetadata: WriteExecutionMetadataStore,
607
+ ): GlobalToolRenderPatch {
608
+ const prototype = (ToolExecutionComponent as any).prototype;
609
+ const host = globalThis as any;
610
+ const previous = host[GLOBAL_TOOL_RENDER_PATCH];
611
+ const downstream = downstreamForGlobalToolInstall(prototype, previous);
612
+ // 外部仍持有的旧 wrapper 先变为无回调 pass-through,再挂新安装。
613
+ disconnectGlobalToolRenderPatch(previous);
614
+
615
+ const patch: GlobalToolRenderPatch = {
616
+ version: 2,
617
+ prototype,
618
+ owner: {},
619
+ active: true,
620
+ enabled: () => config.mode === "on",
621
+ mode: () => config.mode,
622
+ wrap: (tool: any) => createCcstyleTool(tool, writeExecutionMetadata),
623
+ byDefinition: new WeakMap(),
624
+ byName: new Map(),
625
+ downstream,
626
+ installed: undefined as any,
627
+ originalHasRendererDefinition: downstream.hasRendererDefinition,
628
+ originalGetRenderShell: downstream.getRenderShell,
629
+ originalGetCallRenderer: downstream.getCallRenderer,
630
+ originalGetResultRenderer: downstream.getResultRenderer,
631
+ };
632
+
633
+ patch.installed = {
634
+ hasRendererDefinition: function (this: any, ...args: any[]) {
635
+ if (patch.active && shouldGloballyStyleTool(this, patch)) return true;
636
+ return patch.downstream.hasRendererDefinition.apply(this, args);
637
+ },
638
+ getRenderShell: function (this: any, ...args: any[]) {
639
+ if (!patch.active) return patch.downstream.getRenderShell.apply(this, args);
640
+ const useSelfShell = shouldUseSelfShell(this, patch);
641
+ const useCcstyle = shouldGloballyStyleTool(this, patch);
642
+ const shell =
643
+ useSelfShell || (useCcstyle && !this.expanded)
644
+ ? "self"
645
+ : useCcstyle
646
+ ? "default"
647
+ : patch.downstream.getRenderShell.apply(this, args);
648
+ syncToolShell(this, shell);
649
+ return shell;
650
+ },
651
+ getCallRenderer: function (this: any, ...args: any[]) {
652
+ if (patch.active && shouldGloballyStyleTool(this, patch)) {
653
+ return getGloballyStyledTool(this, patch).renderCall;
654
+ }
655
+ return patch.downstream.getCallRenderer.apply(this, args);
656
+ },
657
+ getResultRenderer: function (this: any, ...args: any[]) {
658
+ if (patch.active && shouldGloballyStyleTool(this, patch)) {
659
+ return getGloballyStyledTool(this, patch).renderResult;
660
+ }
661
+ return patch.downstream.getResultRenderer.apply(this, args);
662
+ },
663
+ };
664
+
665
+ prototype.hasRendererDefinition = patch.installed.hasRendererDefinition;
666
+ prototype.getRenderShell = patch.installed.getRenderShell;
667
+ prototype.getCallRenderer = patch.installed.getCallRenderer;
668
+ prototype.getResultRenderer = patch.installed.getResultRenderer;
669
+ host[GLOBAL_TOOL_RENDER_PATCH] = patch;
670
+ return patch;
671
+ }
672
+
673
+ function deactivateGlobalToolRendering(patch: GlobalToolRenderPatch): void {
674
+ if (!patch.active) return;
675
+ disconnectGlobalToolRenderPatch(patch);
676
+ const prototype = patch.prototype;
677
+ if (prototype.hasRendererDefinition === patch.installed.hasRendererDefinition) {
678
+ prototype.hasRendererDefinition = patch.downstream.hasRendererDefinition;
679
+ }
680
+ if (prototype.getRenderShell === patch.installed.getRenderShell) {
681
+ prototype.getRenderShell = patch.downstream.getRenderShell;
682
+ }
683
+ if (prototype.getCallRenderer === patch.installed.getCallRenderer) {
684
+ prototype.getCallRenderer = patch.downstream.getCallRenderer;
685
+ }
686
+ if (prototype.getResultRenderer === patch.installed.getResultRenderer) {
687
+ prototype.getResultRenderer = patch.downstream.getResultRenderer;
688
+ }
689
+ }
690
+
691
+ /** 展开面板背景统一为 user message 背景色;折叠行保持原生状态色。
692
+ * 必须在 compact-mode 之后安装,shutdown 时先于 compact-mode 释放。 */
693
+ export function installToolExpandedBackground(): () => void {
694
+ const host = globalThis as any;
695
+ const previous = host[TOOL_EXPANDED_BACKGROUND_PATCH] as ToolExpandedBackgroundPatch | undefined;
696
+ if (previous) previous.dispose();
697
+ const prototype = ToolExecutionComponent.prototype as unknown as { updateDisplay: () => void };
698
+ const original = prototype.updateDisplay;
699
+ const patch: ToolExpandedBackgroundPatch = {
700
+ active: true,
701
+ prototype,
702
+ installed: function (this: any) {
703
+ original.call(this);
704
+ if (!patch.active || config.mode !== "on" || !this.expanded) return;
705
+ const theme = getMessageDisplayTheme();
706
+ if (!theme?.bg) return;
707
+ const box = this.contentBox;
708
+ // 展开最外层卡片:上下左右内间距 1 格
709
+ if (box) {
710
+ box.paddingX = 1;
711
+ box.paddingY = 1;
712
+ if (box.setBgFn) box.setBgFn((text: string) => theme.bg("userMessageBg", text));
713
+ }
714
+ },
715
+ original,
716
+ dispose: () => {
717
+ patch.active = false;
718
+ if (prototype.updateDisplay === patch.installed) {
719
+ prototype.updateDisplay = original;
720
+ }
721
+ if (host[TOOL_EXPANDED_BACKGROUND_PATCH] === patch) {
722
+ delete host[TOOL_EXPANDED_BACKGROUND_PATCH];
723
+ }
724
+ },
725
+ };
726
+ prototype.updateDisplay = patch.installed;
727
+ host[TOOL_EXPANDED_BACKGROUND_PATCH] = patch;
728
+ return patch.dispose;
729
+ }
730
+
731
+ /** 安装 default mode 全局工具渲染补丁(不含展开背景,见 installToolExpandedBackground)。 */
732
+ export function installDefaultMode(
733
+ writeExecutionMetadata: WriteExecutionMetadataStore,
734
+ ): DefaultModeHooks {
735
+ const globalToolRendering = installGlobalToolRendering(writeExecutionMetadata);
736
+ return {
737
+ isOwner() {
738
+ return (
739
+ (globalThis as any)[GLOBAL_TOOL_RENDER_PATCH] === globalToolRendering &&
740
+ globalToolRendering.active
741
+ );
742
+ },
743
+ shutdown() {
744
+ deactivateGlobalToolRendering(globalToolRendering);
745
+ },
746
+ };
747
+ }