pi-one-ui 0.1.1

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