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
@@ -1,21 +1,22 @@
1
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
- import { ToolExecutionComponent } from "@earendil-works/pi-coding-agent";
3
- import { Text, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
4
2
  import type { CompactThinkingController } from "../feature/compact-thinking.ts";
5
- import { installToolGrouping, type ToolGroupingHooks } from "./tool-grouping.ts";
3
+ import { installToolGrouping, type ToolGroupingHooks } from "./tool/grouping.ts";
6
4
  import {
7
5
  installCompactMode,
8
6
  refreshCompactModeComponents,
9
7
  type CompactModeHooks,
8
+ type CompactThinkingQuery,
10
9
  } from "./compact-mode.ts";
10
+ import {
11
+ installDefaultMode,
12
+ installToolExpandedBackground,
13
+ type DefaultModeHooks,
14
+ } from "./default-mode.ts";
11
15
  import { isLazyProxyTui } from "../utils/fullscreen-detect.ts";
12
16
  import { showCcstylePanel } from "../config/panel.ts";
13
17
  import {
14
18
  config,
15
- DEFAULT_CONFIG,
16
19
  formatConfigStatus,
17
- getCompactThinkingConfig,
18
- getToolDisplayConfig,
19
20
  normalizeConfig,
20
21
  saveConfig,
21
22
  setConfig,
@@ -24,73 +25,45 @@ import {
24
25
  } from "../config/config.ts";
25
26
  import {
26
27
  installToolMouseInteraction,
27
- isToolCallHovered,
28
28
  resetToolHoverState,
29
29
  scheduleSessionRender,
30
- setHoveredToolGroup,
31
- setHoveredToolIo,
32
30
  teardownToolMouseInteraction,
33
- toolMouseTui,
34
31
  TOOL_MOUSE_DISABLE,
35
- } from "./mouse-interaction.ts";
36
- import {
37
- clearAllAnimations,
38
- countLines,
39
- hasExpandableDetail,
40
- headTruncateToWidth,
41
- insetComponent,
42
- isToolExpanded,
43
- middleTruncateToWidth,
44
- oneLine,
45
- outputLineCount,
46
- pendingIcon,
47
- renderCollapsedToolResultToWidth,
48
- renderExpandedToolResult,
49
- resolveToolVisualState,
50
- scheduleAnimation,
51
- settledIcon,
52
- setToolVisualState,
53
- textFromResult,
54
- toolIconColor,
55
- toolViewportWidth,
56
- } from "./tool-result.ts";
57
- import { showMoreHintText } from "./show-more-hint.ts";
58
- import type { CompactThinkingQuery } from "./compact-mode.ts";
59
- import {
60
- installWriteOverride,
61
- renderRichToolResult,
62
- WriteExecutionMetadataStore,
63
- } from "./tool-diff/index.ts";
32
+ } from "./mouse/interaction.ts";
33
+ import { getToolMouseTui } from "./mouse/scroll.ts";
34
+ import { setHoveredToolGroup, setHoveredToolIo } from "./mouse/hover.ts";
35
+ import { clearAllAnimations } from "./tool/result.ts";
36
+ import { installWriteOverride, WriteExecutionMetadataStore } from "./tool/diff/index.ts";
64
37
  import {
65
- getMessageDisplayTheme,
66
38
  installMessageDisplayRendering,
67
39
  refreshMessageDisplays,
68
40
  setMessageDisplayTheme,
69
41
  } from "./message-display.ts";
70
42
 
71
43
  /**
72
- * Claude Code Style for pi — 装配入口(拆分后位于 ccstyle/index.ts)。
44
+ * Claude Code Style for pi — 装配入口。
73
45
  *
74
- * Tool rendering (summaries, rich edit/write diffs, expand/collapse) is applied
75
- * via prototype-level patches.
46
+ * mode=on → default-mode(工具卡样式 + 展开背景)
47
+ * mode=compact → compact-mode(消息折叠摘要)
48
+ * 共用 → tool-grouping / message-display / mouse / write override
76
49
  */
77
- // Bright green for success icon (truecolor ANSI escape)
78
- const BRIGHT_GREEN = "\x1b[38;2;80;220;100m";
79
- const ANSI_FG_RESET = "\x1b[39m";
50
+
51
+ const GLOBAL_COMPACTION_RENDER_PATCH = Symbol.for("pi.ccstyle.compaction-render-patch");
52
+
53
+ let compactModeHooks: CompactModeHooks | undefined;
80
54
 
81
55
  function refreshCurrentTranscript(ctx?: any, toolGrouping?: ToolGroupingHooks): void {
82
- toolGrouping?.refresh(toolMouseTui);
83
- refreshMessageDisplays(toolMouseTui);
84
- refreshCompactModeComponents(toolMouseTui);
56
+ const tui = getToolMouseTui();
57
+ toolGrouping?.refresh(tui);
58
+ refreshMessageDisplays(tui);
59
+ refreshCompactModeComponents(tui);
85
60
  compactModeHooks?.refresh();
86
- toolMouseTui?.requestRender?.(true);
61
+ tui?.requestRender?.(true);
87
62
  ctx?.ui?.requestRender?.(true);
88
63
  }
89
64
 
90
- let compactModeHooks: CompactModeHooks | undefined;
91
-
92
65
  function syncCompactMode(ctx: any): void {
93
- refreshCompactModeComponents(toolMouseTui);
66
+ refreshCompactModeComponents(getToolMouseTui());
94
67
  compactModeHooks?.sync(ctx);
95
68
  }
96
69
 
@@ -98,708 +71,30 @@ function applyStyleMode(mode: CompactStyleMode, ctx: any, toolGrouping?: ToolGro
98
71
  config.mode = mode;
99
72
  saveConfig();
100
73
  if (mode === "off") {
101
- // Native rendering mode: drop hover/click state and fully disable mouse
102
- // reporting so the terminal restores its default scrollback wheel scrolling.
74
+ // Native rendering:清 hover/click,关闭鼠标上报以恢复终端默认滚轮。
103
75
  resetToolHoverState();
104
76
  setHoveredToolGroup(null);
105
77
  setHoveredToolIo(null, null);
106
78
  // 惰性 Proxy(regular/fullscreen)不持有 reporting:regular 保终端回滚,
107
79
  // fullscreen 归官方所有,均不能在此关闭。
108
- if (toolMouseTui && !isLazyProxyTui(toolMouseTui)) {
109
- toolMouseTui.terminal?.write?.(TOOL_MOUSE_DISABLE);
80
+ const tui = getToolMouseTui();
81
+ if (tui && !isLazyProxyTui(tui)) {
82
+ tui.terminal?.write?.(TOOL_MOUSE_DISABLE);
110
83
  }
111
84
  } else if (mode === "compact") {
112
85
  // 切入 compact:先收集当前 transcript,再同步全局展开状态和补丁所有权。
113
86
  syncCompactMode(ctx);
114
87
  }
88
+ // 立即重塑一次;再延迟一帧(与 session_start 同款),等面板/custom
89
+ // 卸下、主 transcript 重新挂载后再扫树,避免必须 /reload。
115
90
  refreshCurrentTranscript(ctx, toolGrouping);
91
+ scheduleSessionRender(() => {
92
+ if (mode === "compact") syncCompactMode(ctx);
93
+ refreshCurrentTranscript(ctx, toolGrouping);
94
+ });
116
95
  ctx.ui.notify(`Claude Code style: ${mode}`, "info");
117
96
  }
118
97
 
119
- function renderDefault(tool: any, slot: "renderCall" | "renderResult", args: any[], fallback = "") {
120
- try {
121
- if (typeof tool?.[slot] === "function") return tool[slot](...args);
122
- } catch {
123
- // Fall through to raw fallback.
124
- }
125
- return new Text(fallback, 0, 0);
126
- }
127
-
128
- function singleLine(text: string) {
129
- return {
130
- render: (width: number) => [truncateToWidth(text, width, "…")],
131
- invalidate() {},
132
- };
133
- }
134
-
135
- function humanizeToolLabel(label: string): string {
136
- return label
137
- .replace(/([a-z0-9])([A-Z])/g, "$1 $2")
138
- .replace(/[_-]+/g, " ")
139
- .replace(/\b\w/g, (char) => char.toUpperCase());
140
- }
141
-
142
- function singleToolCallSummary(
143
- toolName: string,
144
- label: string,
145
- args: any,
146
- ): { main: string; detail: string } {
147
- const title = label === toolName ? humanizeToolLabel(label) : label;
148
- if (!args || typeof args !== "object") return { main: title, detail: "" };
149
- const name = toolName.toLowerCase();
150
- const value = (fallback: string, ...keys: string[]) => {
151
- const found = keys.map((key) => args[key]).find((item) => typeof item === "string" && item);
152
- // 与多 tool 摘要对齐:从头截断,上限 96 字符(tool-grouping oneLine 默认值)。
153
- return `${title} ${oneLine(found || fallback, 96)}`;
154
- };
155
- if (AGENT_FAMILY_TOOL_NAMES.has(toolName) && args.agent_id) {
156
- return { main: `${title} ${oneLine(args.agent_id, 96)}`, detail: "" };
157
- }
158
- // Agents still uses the ccstyle wrapper; Agent keeps its dedicated renderer.
159
- if (name === "agents") {
160
- return {
161
- main: value("launch agents", "description", "prompt"),
162
- detail: "",
163
- };
164
- }
165
- if (name === "skill") return { main: value("run skill", "name"), detail: "" };
166
- if (name === "enterplanmode" || name === "enter_plan_mode") {
167
- return { main: `${title} enable read-only planning`, detail: "" };
168
- }
169
- if (name === "exitplanmode" || name === "exit_plan_mode") {
170
- return { main: `${title} present plan`, detail: "" };
171
- }
172
- if (name === "taskcreate") return { main: value("create task", "subject"), detail: "" };
173
- if (name === "tasklist") return { main: `${title} task list`, detail: "" };
174
- if (name === "taskget" || name === "taskupdate") {
175
- return { main: value("task", "taskId", "task_id"), detail: "" };
176
- }
177
- if (name === "taskoutput" || name === "taskstop") {
178
- return { main: value("background task", "task_id", "taskId"), detail: "" };
179
- }
180
- if (name === "taskexecute") {
181
- const ids = Array.isArray(args.task_ids)
182
- ? args.task_ids
183
- : Array.isArray(args.taskIds)
184
- ? args.taskIds
185
- : [];
186
- const summary = ids.length
187
- ? `${ids[0]}${ids.length > 1 ? ` (+${ids.length - 1} tasks)` : ""}`
188
- : "start tasks";
189
- return { main: `${title} ${summary}`, detail: "" };
190
- }
191
- if (toolName === "read") {
192
- const details = [
193
- args.offset !== undefined ? `offset=${args.offset}` : "",
194
- args.limit !== undefined ? `limit=${args.limit}` : "",
195
- ].filter(Boolean);
196
- return {
197
- main: `${title}${args.path ? ` ${oneLine(args.path, 96)}` : ""}`,
198
- detail: details.length ? ` (${details.join(", ")})` : "",
199
- };
200
- }
201
- const preferred =
202
- args.path ??
203
- args.file_path ??
204
- args.command ??
205
- args.query ??
206
- args.question ??
207
- args.pattern ??
208
- args.url ??
209
- args.name ??
210
- args.tool_use_id ??
211
- args.toolCallId ??
212
- args.id ??
213
- args.message;
214
- return {
215
- main:
216
- preferred !== undefined && preferred !== null && typeof preferred !== "object"
217
- ? `${title} ${oneLine(preferred, 96)}`
218
- : title,
219
- detail: "",
220
- };
221
- }
222
-
223
- export function shouldRenderRichDiff(
224
- mode: CompactStyleMode,
225
- toolName: string,
226
- isError: boolean,
227
- ): boolean {
228
- return mode === "on" && !isError && (toolName === "edit" || toolName === "write");
229
- }
230
-
231
- type ParsedTask = { id: string; status: string; subject: string };
232
-
233
- function parseTaskList(text: string): ParsedTask[] {
234
- return text
235
- .split("\n")
236
- .map((line) => line.match(/^#(\d+) \[([^\]]+)] (.+)$/))
237
- .filter((match): match is RegExpMatchArray => Boolean(match))
238
- .map((match) => ({ id: match[1]!, status: match[2]!, subject: match[3]! }));
239
- }
240
-
241
- function taskListSummary(tasks: ParsedTask[]): string {
242
- const counts = { pending: 0, in_progress: 0, completed: 0 };
243
- for (const task of tasks) {
244
- if (task.status in counts) counts[task.status as keyof typeof counts]++;
245
- }
246
- return [
247
- `${tasks.length} tasks`,
248
- counts.in_progress ? `${counts.in_progress} in progress` : "",
249
- counts.pending ? `${counts.pending} pending` : "",
250
- counts.completed ? `${counts.completed} completed` : "",
251
- ]
252
- .filter(Boolean)
253
- .join(" • ");
254
- }
255
-
256
- function renderExpandedTaskResult(
257
- toolName: string,
258
- text: string,
259
- theme: any,
260
- isError: boolean,
261
- ): any | undefined {
262
- if (isError) return undefined;
263
- if (toolName === "TaskList") {
264
- const tasks = parseTaskList(text);
265
- if (!tasks.length) return undefined;
266
- const limit = Math.max(1, config.expandedPreviewMaxLines);
267
- const rows = tasks.slice(0, limit).map((task) => {
268
- const color =
269
- task.status === "completed"
270
- ? "success"
271
- : task.status === "in_progress"
272
- ? "warning"
273
- : "muted";
274
- return ` ${theme.fg("accent", `#${task.id}`)} ${theme.fg(color, task.status)} ${theme.fg("dim", task.subject)}`;
275
- });
276
- if (tasks.length > rows.length)
277
- rows.push(theme.fg("muted", ` … ${tasks.length - rows.length} more tasks`));
278
- return new Text(` ↳ ${theme.fg("muted", taskListSummary(tasks))}\n${rows.join("\n")}`, 0, 0);
279
- }
280
- const line = text.trim();
281
- if (!line || line.includes("\n")) return undefined;
282
- let formatted: string | undefined;
283
- let match: RegExpMatchArray | null;
284
- if (
285
- toolName === "TaskCreate" &&
286
- (match = line.match(/^Task #(\d+) created successfully: (.+)$/))
287
- ) {
288
- formatted = `${theme.fg("success", "Created task")} ${theme.fg("accent", `#${match[1]}`)} ${theme.fg("muted", match[2])}`;
289
- } else if (toolName === "TaskUpdate" && (match = line.match(/^Updated task #(\d+) (.+)$/))) {
290
- formatted = `${theme.fg("success", "Updated task")} ${theme.fg("accent", `#${match[1]}`)} ${theme.fg("muted", match[2])}`;
291
- } else if (toolName === "TaskExecute") {
292
- formatted = `${theme.fg("success", "Started")} ${theme.fg("muted", line)}`;
293
- } else if (toolName === "TaskStop") {
294
- formatted = `${theme.fg("success", "Stopped")} ${theme.fg("muted", line)}`;
295
- }
296
- return formatted ? new Text(` ↳ ${formatted}`, 0, 0) : undefined;
297
- }
298
-
299
- /** Wrap an arbitrary tool definition with ccstyle call/result rendering. */
300
- function createCcstyleTool(
301
- originalTool: any,
302
- writeExecutionMetadata: WriteExecutionMetadataStore,
303
- ): any {
304
- const toolName = originalTool.name;
305
- const label = isMcpToolDefinition(originalTool, toolName)
306
- ? humanizeMcpToolName(toolName)
307
- : originalTool.label || toolName;
308
-
309
- return {
310
- ...originalTool,
311
- renderShell: "self",
312
- renderCall(args: any, theme: any, context: any) {
313
- if (config.mode !== "on") {
314
- return renderDefault(originalTool, "renderCall", [args, theme, context], String(toolName));
315
- }
316
-
317
- const visualState = resolveToolVisualState(context);
318
- const isPending =
319
- visualState === "pending" ||
320
- (!visualState && (context?.isPartial || context?.executionStarted));
321
- if (isPending) scheduleAnimation(context);
322
- const rawIcon = isPending ? pendingIcon(toolName) : settledIcon(toolName, visualState);
323
- const icon =
324
- visualState === "success"
325
- ? `${BRIGHT_GREEN}${rawIcon}${ANSI_FG_RESET}`
326
- : theme.fg(toolIconColor(context), rawIcon);
327
- const summary = singleToolCallSummary(toolName, label, args);
328
- let cachedWidth: number | undefined;
329
- let cachedLine: string | undefined;
330
- return {
331
- render(width: number) {
332
- if (cachedLine !== undefined && cachedWidth === width) return [cachedLine];
333
- const viewportWidth = toolViewportWidth(width);
334
- const callWidth = Math.max(0, viewportWidth - visibleWidth(icon) - 2);
335
- const mainWidth = Math.max(0, callWidth - visibleWidth(summary.detail));
336
- cachedWidth = width;
337
- // 纯文本先截断再着色(省略号不带 ANSI,避免 reset 破坏卡片背景);从头截断,与多 tool 一致
338
- cachedLine = ` ${icon} ${theme.fg("toolTitle", headTruncateToWidth(summary.main, mainWidth))}${theme.fg("dim", summary.detail)}`;
339
- return [truncateToWidth(cachedLine, viewportWidth, "")];
340
- },
341
- invalidate() {},
342
- };
343
- },
344
- renderResult(result: any, options: any, theme: any, context: any) {
345
- if (config.mode !== "on") {
346
- return renderDefault(
347
- originalTool,
348
- "renderResult",
349
- [result, options, theme, context],
350
- textFromResult(result),
351
- );
352
- }
353
-
354
- if (options?.isPartial) {
355
- return new Text(theme.fg("muted", " ↳ Pending…"), 0, 0);
356
- }
357
-
358
- const isError = options?.isError || context?.isError;
359
- setToolVisualState(context, isError ? "error" : "success");
360
- const expanded = isToolExpanded(options, context);
361
- const toolCallId = context?.toolCallId;
362
- if (shouldRenderRichDiff(config.mode, toolName, Boolean(isError))) {
363
- // Pass getter so Diff indicator / wrap / limits update on the next paint
364
- // without recreating the tool result component.
365
- const richResult = renderRichToolResult(
366
- toolName,
367
- result,
368
- {
369
- ...options,
370
- expanded,
371
- // 全局共享状态避免 /reload 后旧 result renderer 闭包失联。
372
- isHovered: () => isToolCallHovered(toolCallId),
373
- },
374
- theme,
375
- context,
376
- writeExecutionMetadata,
377
- getToolDisplayConfig,
378
- );
379
- if (richResult) return insetComponent(richResult);
380
- }
381
-
382
- const text = textFromResult(result, expanded);
383
- const args = context?.args;
384
- if (expanded) {
385
- const taskResult = renderExpandedTaskResult(toolName, text, theme, Boolean(isError));
386
- if (taskResult) return taskResult;
387
- }
388
- const tasks = !isError && toolName === "TaskList" ? parseTaskList(text) : [];
389
- const outputLines = outputLineCount(result) || countLines(text);
390
- const lineWord = outputLines === 1 ? "line" : "lines";
391
- const action = toolName === "read" ? "loaded" : "returned";
392
- const rendered = tasks.length
393
- ? taskListSummary(tasks)
394
- : isError
395
- ? text
396
- ? oneLine(text)
397
- : "Failed"
398
- : outputLines
399
- ? `${outputLines} ${lineWord} ${action}`
400
- : "Done";
401
- const expandable = !expanded && (tasks.length > 0 || hasExpandableDetail(text, args));
402
- const hintText = showMoreHintText();
403
- const hintPrefix = expandable ? theme.fg("dim", " • ") : "";
404
- const hint = expandable ? hintPrefix + theme.fg("dim", hintText) : "";
405
- const hoveredHint = expandable ? hintPrefix + theme.fg("text", hintText) : "";
406
- if (expanded) {
407
- return renderExpandedToolResult(
408
- text || "",
409
- theme,
410
- Boolean(isError),
411
- context?.lastComponent,
412
- args,
413
- context,
414
- );
415
- }
416
- if (context?.state) context.state.ccstyleIoView = undefined;
417
- let cachedWidth: number | undefined;
418
- let cachedLine: string | undefined;
419
- let cachedHoveredLine: string | undefined;
420
- return {
421
- render(width: number) {
422
- if (cachedLine === undefined || cachedWidth !== width) {
423
- cachedWidth = width;
424
- cachedLine = theme.fg(
425
- isError ? "error" : "muted",
426
- renderCollapsedToolResultToWidth(rendered, hint, width),
427
- );
428
- cachedHoveredLine = theme.fg(
429
- isError ? "error" : "muted",
430
- renderCollapsedToolResultToWidth(rendered, hoveredHint, width),
431
- );
432
- }
433
- return [isToolCallHovered(toolCallId) ? cachedHoveredLine! : cachedLine];
434
- },
435
- invalidate() {},
436
- };
437
- },
438
- };
439
- }
440
-
441
- /**
442
- * Apart from the write override used to capture pre-write content, renderers are
443
- * applied through ToolExecutionComponent. Patch its lookup once so tools use the
444
- * same ccstyle fallback shell by default. Tools named in excludeRenderers keep
445
- * their original renderer.
446
- */
447
- const GLOBAL_TOOL_RENDER_PATCH = Symbol.for("pi.ccstyle.global-tool-render-patch");
448
- const COMPONENT_TOOL_RENDER_MODE = Symbol.for("pi.ccstyle.component-tool-render-mode");
449
- const COMPONENT_TOOL_SELF_SHELL_MODE = Symbol.for("pi.ccstyle.component-tool-self-shell-mode");
450
- const AGENT_FAMILY_TOOL_NAMES = new Set([
451
- "Agent",
452
- "Agents",
453
- "get_subagent_result",
454
- "steer_subagent",
455
- ]);
456
- // pi-subagents 等扩展为 Agent 提供专用渲染器(displayName/运行统计),ccstyle 必须保留,不能 wrap。
457
- const DEDICATED_RENDERER_TOOLS = new Set(["Agent"]);
458
-
459
- type ToolRenderMethods = {
460
- hasRendererDefinition: (...args: any[]) => boolean;
461
- getRenderShell: (...args: any[]) => "default" | "self";
462
- getCallRenderer: (...args: any[]) => any;
463
- getResultRenderer: (...args: any[]) => any;
464
- };
465
-
466
- type GlobalToolRenderPatch = {
467
- version: 2;
468
- prototype: any;
469
- owner: object;
470
- active: boolean;
471
- enabled: () => boolean;
472
- mode: () => CompactStyleMode;
473
- wrap: (tool: any) => any;
474
- byDefinition: WeakMap<object, any>;
475
- byName: Map<string, any>;
476
- downstream: ToolRenderMethods;
477
- installed: ToolRenderMethods;
478
- // Keep the legacy aliases so an older extension build can read this Symbol.
479
- originalHasRendererDefinition: ToolRenderMethods["hasRendererDefinition"];
480
- originalGetRenderShell: ToolRenderMethods["getRenderShell"];
481
- originalGetCallRenderer: ToolRenderMethods["getCallRenderer"];
482
- originalGetResultRenderer: ToolRenderMethods["getResultRenderer"];
483
- };
484
-
485
- export function isMcpToolDefinition(definition: any, toolName: string): boolean {
486
- const label = typeof definition?.label === "string" ? definition.label.trim() : "";
487
- if (/^MCP(?::|$)/i.test(label)) return true;
488
- if (toolName === "mcp" || /^mcp[_:-]|[_:-]mcp[_:-]/i.test(toolName)) return true;
489
- if (label) return false;
490
- const description = typeof definition?.description === "string" ? definition.description : "";
491
- return /\bModel Context Protocol\b/i.test(description);
492
- }
493
-
494
- export function humanizeMcpToolName(toolName: string): string {
495
- const words = toolName
496
- .replace(/^mcp(?:[_:-]+)+/i, "")
497
- .split(/[_:-]+/)
498
- .filter(Boolean);
499
- return words.length
500
- ? words.map((word) => word[0]!.toUpperCase() + word.slice(1)).join(" ")
501
- : "MCP";
502
- }
503
-
504
- /** Return true when this tool must keep its original renderer. */
505
- export function preservesOriginalRenderer(
506
- extensionDefinition: any,
507
- toolName: string,
508
- builtInToolDefinition?: any,
509
- excludeRenderers: readonly string[] = config.excludeRenderers,
510
- ): boolean {
511
- if (!excludeRenderers.includes(toolName)) return false;
512
- return [extensionDefinition, builtInToolDefinition].some(
513
- (definition) =>
514
- definition?.renderShell === "self" ||
515
- typeof definition?.renderCall === "function" ||
516
- typeof definition?.renderResult === "function",
517
- );
518
- }
519
-
520
- function syncToolShell(component: any, shell: "default" | "self"): void {
521
- const target = shell === "self" ? component.selfRenderContainer : component.contentBox;
522
- if (!target || !Array.isArray(component.children)) return;
523
- const candidates = new Set(
524
- [component.contentText, component.contentBox, component.selfRenderContainer].filter(Boolean),
525
- );
526
- const indexes = component.children
527
- .map((child: any, index: number) => (candidates.has(child) ? index : -1))
528
- .filter((index: number) => index >= 0);
529
- const targetIndex = indexes[0];
530
- // During construction getRenderShell() runs immediately before Pi mounts the
531
- // selected shell. Do not mount it here or the constructor will add it twice.
532
- if (targetIndex === undefined) return;
533
- component.children[targetIndex] = target;
534
- for (const index of indexes.sort((left: number, right: number) => right - left)) {
535
- if (index !== targetIndex) component.children.splice(index, 1);
536
- }
537
- }
538
-
539
- function shouldGloballyStyleTool(component: any, patch: GlobalToolRenderPatch): boolean {
540
- const extensionDefinition = component.toolDefinition;
541
- const builtInDefinition = component.builtInToolDefinition;
542
- const definition = extensionDefinition ?? builtInDefinition;
543
- const toolName = String(component.toolName || definition?.name || "");
544
- const useCcstyle =
545
- patch.mode() === "on" &&
546
- !DEDICATED_RENDERER_TOOLS.has(toolName) &&
547
- !preservesOriginalRenderer(extensionDefinition, toolName, builtInDefinition);
548
- component[COMPONENT_TOOL_RENDER_MODE] = useCcstyle;
549
- return useCcstyle;
550
- }
551
-
552
- function shouldUseSelfShell(component: any, _patch: GlobalToolRenderPatch): boolean {
553
- component[COMPONENT_TOOL_SELF_SHELL_MODE] = false;
554
- return false;
555
- }
556
-
557
- function getGloballyStyledTool(component: any, patch: GlobalToolRenderPatch): any {
558
- const definition = component.toolDefinition ?? component.builtInToolDefinition;
559
- if (definition && typeof definition === "object") {
560
- let wrapped = patch.byDefinition.get(definition);
561
- if (!wrapped) {
562
- wrapped = patch.wrap(definition);
563
- patch.byDefinition.set(definition, wrapped);
564
- }
565
- return wrapped;
566
- }
567
-
568
- const name = String(component.toolName || "tool");
569
- let wrapped = patch.byName.get(name);
570
- if (!wrapped) {
571
- wrapped = patch.wrap({ name, label: name });
572
- patch.byName.set(name, wrapped);
573
- }
574
- return wrapped;
575
- }
576
-
577
- function prototypeToolRenderMethods(prototype: any): ToolRenderMethods {
578
- return {
579
- hasRendererDefinition: prototype.hasRendererDefinition,
580
- getRenderShell: prototype.getRenderShell,
581
- getCallRenderer: prototype.getCallRenderer,
582
- getResultRenderer: prototype.getResultRenderer,
583
- };
584
- }
585
-
586
- function isOwnershipAwarePatch(value: any): value is GlobalToolRenderPatch {
587
- if (!value || value.version !== 2 || !value.installed || !value.downstream) return false;
588
- return ["hasRendererDefinition", "getRenderShell", "getCallRenderer", "getResultRenderer"].every(
589
- (name) =>
590
- typeof value.installed[name] === "function" && typeof value.downstream[name] === "function",
591
- );
592
- }
593
-
594
- function isLegacyInstalledWrapper(method: unknown, downstreamField: string): boolean {
595
- if (typeof method !== "function") return false;
596
- try {
597
- const source = Function.prototype.toString.call(method);
598
- return (
599
- source.includes(downstreamField) &&
600
- (source.includes("shouldGloballyStyleTool") ||
601
- source.includes("shouldUseSelfShell") ||
602
- source.includes("getGloballyStyledTool"))
603
- );
604
- } catch {
605
- return false;
606
- }
607
- }
608
-
609
- function downstreamForGlobalToolInstall(prototype: any, previous: any): ToolRenderMethods {
610
- const current = prototypeToolRenderMethods(prototype);
611
- if (!previous || previous.prototype !== prototype) return current;
612
- if (isOwnershipAwarePatch(previous)) {
613
- return {
614
- hasRendererDefinition:
615
- current.hasRendererDefinition === previous.installed.hasRendererDefinition
616
- ? previous.downstream.hasRendererDefinition
617
- : current.hasRendererDefinition,
618
- getRenderShell:
619
- current.getRenderShell === previous.installed.getRenderShell
620
- ? previous.downstream.getRenderShell
621
- : current.getRenderShell,
622
- getCallRenderer:
623
- current.getCallRenderer === previous.installed.getCallRenderer
624
- ? previous.downstream.getCallRenderer
625
- : current.getCallRenderer,
626
- getResultRenderer:
627
- current.getResultRenderer === previous.installed.getResultRenderer
628
- ? previous.downstream.getResultRenderer
629
- : current.getResultRenderer,
630
- };
631
- }
632
-
633
- // Pre-v2 Symbol state did not retain wrapper references. Recognize its known
634
- // wrappers when possible; otherwise preserve the current method as external.
635
- const legacyDownstream = (method: Function, field: string): Function => {
636
- const saved = previous[field];
637
- return typeof saved === "function" && isLegacyInstalledWrapper(method, field) ? saved : method;
638
- };
639
- return {
640
- hasRendererDefinition: legacyDownstream(
641
- current.hasRendererDefinition,
642
- "originalHasRendererDefinition",
643
- ) as ToolRenderMethods["hasRendererDefinition"],
644
- getRenderShell: legacyDownstream(
645
- current.getRenderShell,
646
- "originalGetRenderShell",
647
- ) as ToolRenderMethods["getRenderShell"],
648
- getCallRenderer: legacyDownstream(
649
- current.getCallRenderer,
650
- "originalGetCallRenderer",
651
- ) as ToolRenderMethods["getCallRenderer"],
652
- getResultRenderer: legacyDownstream(
653
- current.getResultRenderer,
654
- "originalGetResultRenderer",
655
- ) as ToolRenderMethods["getResultRenderer"],
656
- };
657
- }
658
-
659
- function disconnectGlobalToolRenderPatch(patch: any): void {
660
- if (!patch || typeof patch !== "object") return;
661
- patch.active = false;
662
- patch.enabled = () => false;
663
- patch.mode = () => "off";
664
- patch.wrap = (tool: any) => tool;
665
- patch.byDefinition = new WeakMap();
666
- if (patch.byName && typeof patch.byName.clear === "function") patch.byName.clear();
667
- else patch.byName = new Map();
668
- }
669
-
670
- function installGlobalToolRendering(
671
- writeExecutionMetadata: WriteExecutionMetadataStore,
672
- ): GlobalToolRenderPatch {
673
- const prototype = (ToolExecutionComponent as any).prototype;
674
- const host = globalThis as any;
675
- const previous = host[GLOBAL_TOOL_RENDER_PATCH];
676
- const downstream = downstreamForGlobalToolInstall(prototype, previous);
677
- // Any wrapper retained by an external owner must become a callback-free
678
- // pass-through before the new installation is linked above it.
679
- disconnectGlobalToolRenderPatch(previous);
680
-
681
- const patch: GlobalToolRenderPatch = {
682
- version: 2,
683
- prototype,
684
- owner: {},
685
- active: true,
686
- enabled: () => config.mode === "on",
687
- mode: () => config.mode,
688
- wrap: (tool: any) => createCcstyleTool(tool, writeExecutionMetadata),
689
- byDefinition: new WeakMap(),
690
- byName: new Map(),
691
- downstream,
692
- installed: undefined as any,
693
- originalHasRendererDefinition: downstream.hasRendererDefinition,
694
- originalGetRenderShell: downstream.getRenderShell,
695
- originalGetCallRenderer: downstream.getCallRenderer,
696
- originalGetResultRenderer: downstream.getResultRenderer,
697
- };
698
-
699
- patch.installed = {
700
- hasRendererDefinition: function (this: any, ...args: any[]) {
701
- if (patch.active && shouldGloballyStyleTool(this, patch)) return true;
702
- return patch.downstream.hasRendererDefinition.apply(this, args);
703
- },
704
- getRenderShell: function (this: any, ...args: any[]) {
705
- if (!patch.active) return patch.downstream.getRenderShell.apply(this, args);
706
- const useSelfShell = shouldUseSelfShell(this, patch);
707
- const useCcstyle = shouldGloballyStyleTool(this, patch);
708
- const shell =
709
- useSelfShell || (useCcstyle && !this.expanded)
710
- ? "self"
711
- : useCcstyle
712
- ? "default"
713
- : patch.downstream.getRenderShell.apply(this, args);
714
- syncToolShell(this, shell);
715
- return shell;
716
- },
717
- getCallRenderer: function (this: any, ...args: any[]) {
718
- if (patch.active && shouldGloballyStyleTool(this, patch)) {
719
- return getGloballyStyledTool(this, patch).renderCall;
720
- }
721
- return patch.downstream.getCallRenderer.apply(this, args);
722
- },
723
- getResultRenderer: function (this: any, ...args: any[]) {
724
- if (patch.active && shouldGloballyStyleTool(this, patch)) {
725
- return getGloballyStyledTool(this, patch).renderResult;
726
- }
727
- return patch.downstream.getResultRenderer.apply(this, args);
728
- },
729
- };
730
-
731
- prototype.hasRendererDefinition = patch.installed.hasRendererDefinition;
732
- prototype.getRenderShell = patch.installed.getRenderShell;
733
- prototype.getCallRenderer = patch.installed.getCallRenderer;
734
- prototype.getResultRenderer = patch.installed.getResultRenderer;
735
- host[GLOBAL_TOOL_RENDER_PATCH] = patch;
736
- return patch;
737
- }
738
-
739
- function deactivateGlobalToolRendering(patch: GlobalToolRenderPatch): void {
740
- if (!patch.active) return;
741
- disconnectGlobalToolRenderPatch(patch);
742
- const prototype = patch.prototype;
743
- if (prototype.hasRendererDefinition === patch.installed.hasRendererDefinition) {
744
- prototype.hasRendererDefinition = patch.downstream.hasRendererDefinition;
745
- }
746
- if (prototype.getRenderShell === patch.installed.getRenderShell) {
747
- prototype.getRenderShell = patch.downstream.getRenderShell;
748
- }
749
- if (prototype.getCallRenderer === patch.installed.getCallRenderer) {
750
- prototype.getCallRenderer = patch.downstream.getCallRenderer;
751
- }
752
- if (prototype.getResultRenderer === patch.installed.getResultRenderer) {
753
- prototype.getResultRenderer = patch.downstream.getResultRenderer;
754
- }
755
- }
756
-
757
- const GLOBAL_COMPACTION_RENDER_PATCH = Symbol.for("pi.ccstyle.compaction-render-patch");
758
-
759
- const TOOL_EXPANDED_BACKGROUND_PATCH = Symbol.for("pi.ccstyle.tool-expanded-background-patch");
760
-
761
- type ToolExpandedBackgroundPatch = {
762
- active: boolean;
763
- prototype: any;
764
- installed: (...args: any[]) => void;
765
- original: (...args: any[]) => void;
766
- dispose: () => void;
767
- };
768
-
769
- /** 展开面板背景统一为 user message 背景色;折叠行保持原生状态色(由 original 重设)。 */
770
- function installToolExpandedBackground(): () => void {
771
- const host = globalThis as any;
772
- const previous = host[TOOL_EXPANDED_BACKGROUND_PATCH] as ToolExpandedBackgroundPatch | undefined;
773
- if (previous) previous.dispose();
774
- const prototype = ToolExecutionComponent.prototype as unknown as { updateDisplay: () => void };
775
- const original = prototype.updateDisplay;
776
- const patch: ToolExpandedBackgroundPatch = {
777
- active: true,
778
- prototype,
779
- installed: function (this: any) {
780
- original.call(this);
781
- if (!patch.active || config.mode !== "on" || !this.expanded) return;
782
- const theme = getMessageDisplayTheme();
783
- if (!theme?.bg) return;
784
- const box = this.contentBox;
785
- if (box?.setBgFn) box.setBgFn((text: string) => theme.bg("userMessageBg", text));
786
- },
787
- original,
788
- dispose: () => {
789
- patch.active = false;
790
- if (prototype.updateDisplay === patch.installed) {
791
- prototype.updateDisplay = original;
792
- }
793
- if (host[TOOL_EXPANDED_BACKGROUND_PATCH] === patch) {
794
- delete host[TOOL_EXPANDED_BACKGROUND_PATCH];
795
- }
796
- },
797
- };
798
- prototype.updateDisplay = patch.installed;
799
- host[TOOL_EXPANDED_BACKGROUND_PATCH] = patch;
800
- return patch.dispose;
801
- }
802
-
803
98
  type LegacyCompactionRenderPatch = {
804
99
  enabled?: () => boolean;
805
100
  };
@@ -817,25 +112,26 @@ export default function (
817
112
  configOverride?: Partial<Config>,
818
113
  compactThinking?: CompactThinkingController,
819
114
  ) {
820
- // The optional override keeps integration tests independent from the user's global config.
115
+ // 可选 override:集成测试不依赖用户全局配置。
821
116
  if (configOverride) setConfig(normalizeConfig({ ...config, ...configOverride }));
822
117
  const writeExecutionMetadata = new WriteExecutionMetadataStore();
823
118
  const mouseOwner = {};
824
119
  let installation:
825
120
  | {
826
- globalToolRendering: GlobalToolRenderPatch;
121
+ defaultMode: DefaultModeHooks;
827
122
  toolGrouping: ToolGroupingHooks;
828
123
  compactMode: CompactModeHooks;
829
124
  disposeMessageDisplay: () => void;
830
125
  disposeToolExpandedBackground: () => void;
831
126
  }
832
127
  | undefined;
128
+
833
129
  const ensureTuiInstallation = (ctx: any) => {
834
130
  if (ctx?.mode !== "tui" || !ctx?.hasUI) return undefined;
835
131
  // 渲染层(工具样式/分组)是原型与组件级 patch,fullscreen 官方布局
836
132
  // 同样渲染这些组件,因此两种模式都安装。
837
133
  if (installation) return installation;
838
- const globalToolRendering = installGlobalToolRendering(writeExecutionMetadata);
134
+ const defaultMode = installDefaultMode(writeExecutionMetadata);
839
135
  const toolGrouping = installToolGrouping(() => config.mode === "on");
840
136
  const compactMode = installCompactMode({
841
137
  query: compactThinking as CompactThinkingQuery | undefined,
@@ -843,10 +139,11 @@ export default function (
843
139
  });
844
140
  compactModeHooks = compactMode;
845
141
  const disposeMessageDisplay = installMessageDisplayRendering();
142
+ // 展开背景必须在 compact-mode 之后装,shutdown 时先于 compact 释放。
846
143
  const disposeToolExpandedBackground = installToolExpandedBackground();
847
144
  deactivateLegacyCompactionRendering();
848
145
  installation = {
849
- globalToolRendering,
146
+ defaultMode,
850
147
  toolGrouping,
851
148
  compactMode,
852
149
  disposeMessageDisplay,
@@ -910,8 +207,7 @@ export default function (
910
207
 
911
208
  pi.on("tool_execution_end", async (event) => {
912
209
  if (config.mode !== "compact") return;
913
- // Agent 工具在 tool_execution_end 收尾思考:延迟到所有监听器之后刷新,
914
- // 让 compact-thinking 先写入最终时长,再重绘摘要行。
210
+ // Agent 等工具收尾后延迟刷新,让 compact-thinking 先落最终态。
915
211
  const toolCallId: string | undefined = event?.toolCallId;
916
212
  setTimeout(() => {
917
213
  compactModeHooks?.refreshToolCallMessage(toolCallId);
@@ -936,7 +232,7 @@ export default function (
936
232
  // compact-thinking 的 session_start 处理在本 handler 之后执行(在其之上再装
937
233
  // 一层 updateContent);延迟再同步一次,保证 compact 补丁最终位于外层。
938
234
  setTimeout(() => syncCompactMode(ctx), 0);
939
- scheduleSessionRender(() => hooks.toolGrouping.refresh(toolMouseTui));
235
+ scheduleSessionRender(() => hooks.toolGrouping.refresh(getToolMouseTui()));
940
236
  });
941
237
 
942
238
  pi.on("session_compact", async (event, ctx) => {
@@ -950,7 +246,7 @@ export default function (
950
246
  syncCompactMode(ctx);
951
247
  scheduleSessionRender(() => {
952
248
  syncCompactMode(ctx);
953
- hooks.toolGrouping.refresh(toolMouseTui);
249
+ hooks.toolGrouping.refresh(getToolMouseTui());
954
250
  });
955
251
  });
956
252
 
@@ -961,23 +257,18 @@ export default function (
961
257
  scheduleSessionRender(() => syncCompactMode(ctx));
962
258
  });
963
259
 
964
- pi.on("tool_execution_start", async (event, ctx) => {
260
+ pi.on("tool_execution_start", async (_event, ctx) => {
965
261
  installation?.toolGrouping.setTheme(ctx.ui.theme);
966
262
  });
967
263
 
968
- pi.on("session_shutdown", async (event, ctx) => {
264
+ pi.on("session_shutdown", async () => {
969
265
  writeExecutionMetadata.clear();
970
266
  // 鼠标交互独立于渲染层:fullscreen 下 installation 为 undefined,
971
267
  // 但 onTerminalInput 监听与 handleViewportInput 包装仍需释放。
972
268
  teardownToolMouseInteraction(mouseOwner);
973
269
  const current = installation;
974
- if (
975
- !current ||
976
- (globalThis as any)[GLOBAL_TOOL_RENDER_PATCH] !== current.globalToolRendering ||
977
- !current.globalToolRendering.active
978
- )
979
- return;
980
- deactivateGlobalToolRendering(current.globalToolRendering);
270
+ if (!current || !current.defaultMode.isOwner()) return;
271
+ current.defaultMode.shutdown();
981
272
  current.toolGrouping.shutdown();
982
273
  current.disposeToolExpandedBackground();
983
274
  current.compactMode.shutdown();
@@ -989,26 +280,18 @@ export default function (
989
280
  });
990
281
  }
991
282
 
992
- // ---- 对外导出:保持与拆分前 claude-code-style.ts 完全一致的符号集合 ----
283
+ // ---- 对外导出:入口/测试实际消费的符号 ----
284
+ export { getCompactThinkingConfig } from "../config/config.ts";
993
285
  export {
994
- DEFAULT_CONFIG,
995
- formatConfigStatus,
996
- getCompactThinkingConfig,
997
- getToolDisplayConfig,
998
- normalizeConfig,
999
- } from "../config/config.ts";
1000
- export type { CompactStyleMode, Config } from "../config/config.ts";
286
+ humanizeMcpToolName,
287
+ isMcpToolDefinition,
288
+ preservesOriginalRenderer,
289
+ shouldRenderRichDiff,
290
+ } from "./default-mode.ts";
1001
291
  export {
1002
292
  ExpandedToolIoView,
1003
293
  ExpandedToolResultText,
1004
- formatExpandHint,
1005
294
  formatToolInputArgs,
1006
- headTruncateToWidth,
1007
- middleTruncateToWidth,
1008
- outputLineCount,
1009
- renderCollapsedToolResult,
1010
- renderCollapsedToolResultToWidth,
1011
295
  SHOW_MORE_LABEL,
1012
- } from "./tool-result.ts";
1013
- export type { ToolIoSection } from "./tool-result.ts";
1014
- export { installToolMouseInteraction } from "./mouse-interaction.ts";
296
+ } from "./tool/result.ts";
297
+ export { installToolMouseInteraction } from "./mouse/interaction.ts";