pi-cc-extensions 1.0.9 → 1.0.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -122,6 +122,7 @@ export const THINKING_PREVIEW_LINES_VALUES = ["0", "1", "3", "5", "10"];
122
122
  export const THINKING_ANIMATION_INTERVAL_VALUES = ["40", "60", "90", "120", "180"];
123
123
  /** Tools commonly toggled in excludeRenderers via the settings panel. */
124
124
  export const EXCLUDE_RENDERER_CANDIDATES = [
125
+ "agent",
125
126
  "bash",
126
127
  "powershell",
127
128
  "read",
@@ -82,8 +82,8 @@ function modeSettingDescription(mode: CompactStyleMode): string {
82
82
 
83
83
  function excludeRenderersDescription(names: readonly string[]): string {
84
84
  return names.length === 0
85
- ? "No tools excluded. Agent always keeps its dedicated renderer. Enter to toggle common tools."
86
- : `Native renderer for: ${names.join(", ")}. Agent is always native. Enter to toggle.`;
85
+ ? "No tools excluded. Enter to toggle common tools."
86
+ : `Native renderer for: ${names.join(", ")}. Enter to toggle.`;
87
87
  }
88
88
 
89
89
  function customFooterDescription(enabled: boolean): string {
@@ -274,10 +274,7 @@ function buildExcludeRenderersSubmenu(
274
274
  const items = candidates.map((name) => ({
275
275
  id: name,
276
276
  label: name,
277
- description:
278
- name === "Agent"
279
- ? "Agent always uses its dedicated renderer and cannot be forced through ccstyle."
280
- : `Use Pi native renderer for ${name} instead of Claude Code styling.`,
277
+ description: `Use Pi native renderer for ${name} instead of Claude Code styling.`,
281
278
  currentValue: config.excludeRenderers.includes(name) ? "exclude" : "style",
282
279
  values: ["style", "exclude"],
283
280
  }));
@@ -20,10 +20,7 @@ import { createCcstyleTool } from "./tool/ccstyle-tool.ts";
20
20
  import type { WriteExecutionMetadataStore } from "./tool/diff/index.ts";
21
21
  export { shouldRenderRichDiff } from "./tool/ccstyle-tool.ts";
22
22
 
23
- // pi-subagents 等扩展为 Agent 提供专用渲染器,ccstyle 必须保留。
24
- const DEDICATED_RENDERER_TOOLS = new Set(["Agent"]);
25
-
26
- /** 排除名单内且自带 renderer 的工具保留原渲染。 */
23
+ /** 排除名单内且自带 renderer 的工具保留原渲染;excludeRenderers 里的工具也交给原生渲染器。 */
27
24
  export function preservesOriginalRenderer(
28
25
  extensionDefinition: any,
29
26
  toolName: string,
@@ -117,7 +114,6 @@ function shouldGloballyStyleTool(component: any, patch: GlobalToolRenderPatch):
117
114
  // compact 也复用同一套 ccstyle call/result,避免折叠态工具卡回落到 Pi 原生样式。
118
115
  const useCcstyle =
119
116
  patch.mode() !== "off" &&
120
- !DEDICATED_RENDERER_TOOLS.has(toolName) &&
121
117
  !preservesOriginalRenderer(extensionDefinition, toolName, builtInDefinition);
122
118
  component[COMPONENT_TOOL_RENDER_MODE] = useCcstyle;
123
119
  return useCcstyle;
@@ -49,13 +49,17 @@ import {
49
49
  refreshMessageDisplays,
50
50
  setMessageDisplayTheme,
51
51
  } from "./tool/message-display.ts";
52
+ import {
53
+ installSubagentNotificationRenderer,
54
+ refreshSubagentNotifications,
55
+ } from "./tool/subagent-notification.ts";
52
56
 
53
57
  /**
54
58
  * Claude Code Style for pi — 装配入口。
55
59
  *
56
60
  * mode=on → default-mode(工具卡样式 + 展开背景)
57
61
  * mode=compact → compact-mode(消息折叠摘要)
58
- * 共用 → tool-grouping / message-display / mouse / write override
62
+ * 共用 → tool-grouping / message-display / subagent-notification / mouse / write override
59
63
  */
60
64
 
61
65
  let compactModeHooks: CompactModeHooks | undefined;
@@ -64,6 +68,7 @@ function refreshCurrentTranscript(ctx?: any, toolGrouping?: ToolGroupingHooks):
64
68
  const tui = getToolMouseTui();
65
69
  toolGrouping?.refresh(tui);
66
70
  refreshMessageDisplays(tui);
71
+ refreshSubagentNotifications(tui);
67
72
  refreshCompactModeComponents(tui);
68
73
  compactModeHooks?.refresh();
69
74
  tui?.requestRender?.(true);
@@ -117,6 +122,7 @@ export default function (
117
122
  toolGrouping: ToolGroupingHooks;
118
123
  compactMode: CompactModeHooks;
119
124
  disposeMessageDisplay: () => void;
125
+ disposeSubagentNotificationRenderer: () => void;
120
126
  disposeToolExpandedBackground: () => void;
121
127
  }
122
128
  | undefined;
@@ -138,6 +144,8 @@ export default function (
138
144
  });
139
145
  compactModeHooks = compactMode;
140
146
  const disposeMessageDisplay = installMessageDisplayRendering();
147
+ // pi-subagents 的结束通知:换查找结果而不注册,加载先后都归本插件,off 还给原生。
148
+ const disposeSubagentNotificationRenderer = installSubagentNotificationRenderer();
141
149
  // 展开背景必须在 compact-mode 之后装,shutdown 时先于 compact 释放。
142
150
  const disposeToolExpandedBackground = installToolExpandedBackground();
143
151
  installation = {
@@ -145,6 +153,7 @@ export default function (
145
153
  toolGrouping,
146
154
  compactMode,
147
155
  disposeMessageDisplay,
156
+ disposeSubagentNotificationRenderer,
148
157
  disposeToolExpandedBackground,
149
158
  };
150
159
  return installation;
@@ -253,7 +262,11 @@ export default function (
253
262
  // 历史回放发生在 session_start 之后,延迟到回放结束再收尾最后一个回合,
254
263
  // 否则它没有后续消息,摘要行会停在 Running。
255
264
  setTimeout(() => compactModeHooks?.settleReplayedRound(), 0);
256
- scheduleSessionRender(() => hooks.toolGrouping.refresh(getToolMouseTui()));
265
+ scheduleSessionRender(() => {
266
+ hooks.toolGrouping.refresh(getToolMouseTui());
267
+ // /reload、/resume 在 session_start 之前就重建了 transcript:已挂载的通知卡换上本插件的渲染器。
268
+ refreshSubagentNotifications(getToolMouseTui());
269
+ });
257
270
  });
258
271
 
259
272
  pi.on("session_compact", async (event, ctx) => {
@@ -307,6 +320,7 @@ export default function (
307
320
  current.compactMode.shutdown();
308
321
  compactModeHooks = undefined;
309
322
  current.disposeMessageDisplay();
323
+ current.disposeSubagentNotificationRenderer();
310
324
  disposeStickyPromptPatches();
311
325
  clearAllAnimations();
312
326
  installation = undefined;
@@ -102,7 +102,10 @@ function deCircled(markdown: string): string {
102
102
  // 跨行链接规整
103
103
  // ============================================================================
104
104
 
105
- const MULTILINE_LINK_RE = /\[([^\]\n]*(?:\n[^\]\n]*)+)\](?=\()/g;
105
+ // \x1b 排除:Pi 的带颜色 Mermaid 输出把每行包成含 ANSI SGR(\x1b[...m)的 code span,
106
+ // 转义序列里的 `[` 不能当成链接起点,否则匹配会从第一个转义跨行直到后面真正的 `](`,
107
+ // 把图和之后的段落全压成一行(#84)。
108
+ const MULTILINE_LINK_RE = /(?<!\x1b)\[([^\]\n\x1b]*(?:\n[^\]\n\x1b]*)+)\](?=\()/g;
106
109
 
107
110
  /** 防止 Markdown 跨行链接把行尾填充空格纳入 OSC 8 点击区。 */
108
111
  function normalizeMultilineLinks(markdown: string): string {
@@ -12,7 +12,6 @@ import {
12
12
  markCompactRoundToolExpanded,
13
13
  setHoveredCompactAssistant,
14
14
  } from "../compact-mode.ts";
15
- import { isMessageDisplayComponent } from "../tool/message-display.ts";
16
15
  import { isLazyProxyTui } from "../../utils/fullscreen-detect.ts";
17
16
  import { setToolTuiFullscreen, isCollapseHintLine } from "../tool/names.ts";
18
17
  import {
@@ -43,6 +42,7 @@ import {
43
42
  collapsedHintHitbox,
44
43
  fullscreenContentWidth,
45
44
  fullscreenLeafAt,
45
+ isMessageCard,
46
46
  isScrollbarColumnAt,
47
47
  } from "./layout.ts";
48
48
  import {
@@ -122,7 +122,7 @@ let sessionRenderTimer: ReturnType<typeof setTimeout> | null = null;
122
122
  let latestInteractionFrame: InteractionFrame = { regions: [] };
123
123
 
124
124
  /** Summary markers used by Pi and ccstyle; unlike the trailing hint, these survive truncation. */
125
- const COLLAPSED_TOOL_SUMMARY = /^\s*(?:↳|└|⎿|●|✓|✗|…)/;
125
+ const COLLAPSED_TOOL_SUMMARY = /^\s*(?:↳|└|⎿|●|✓|✗|…|\.\.\.)/;
126
126
 
127
127
  function interactionRegionAt(packet: SgrMousePacket): InteractionRegion | null {
128
128
  const matches = latestInteractionFrame.regions.filter(
@@ -347,28 +347,40 @@ function releaseFullscreenToolMouseMotion(tui?: any): void {
347
347
  const FULLSCREEN_VIEWPORT_PATCH = Symbol("ccstyle.fullscreen-viewport-patch");
348
348
 
349
349
  /**
350
- * diff 结果组件自带 remainder 行:声明存在时只有那一行是展开入口(正文里的同名字样不算);
351
- * 未声明的组件继续用文本规则。
350
+ * 折叠卡上哪些行声明为展开入口。resultRendererComponent 自带 isCollapsedHintLine
351
+ * 的(diff/ask/codemode)按它声明的判,行号原样转发;不带的原生卡(grep/find/ls/
352
+ * read/bash/fallback)按原生展开提示的收尾特征判——pi 给结果区套整卡 MouseRegion,
353
+ * 正文行(含路径文本)也会被官方 click 展开,这里把入口收到提示那几行。
354
+ * localRow 是组件 render 内的行号(frame marker / 布局 localRow),供声明按行判。
352
355
  */
353
- function isCollapsedHintRow(component: any, finalLine: string): boolean {
356
+ function isCollapsedHintRow(component: any, finalLine: string, localRow?: number): boolean {
354
357
  const result = component?.resultRendererComponent;
355
- return typeof result?.isCollapsedHintLine === "function"
356
- ? result.isCollapsedHintLine(finalLine)
357
- : true;
358
+ if (typeof result?.isCollapsedHintLine === "function") {
359
+ return result.isCollapsedHintLine(finalLine, localRow);
360
+ }
361
+ return isNativeExpandHintLine(finalLine);
362
+ }
363
+
364
+ /** 原生折叠卡展开提示的收尾特征:`(... N more lines, ctrl+o to expand)` 或行尾的 `to show more`。 */
365
+ function isNativeExpandHintLine(line: string): boolean {
366
+ return /(\([^()]*\/(?: click| to expand)\)|(?:[\w/+]+\s+)?to (?:expand|show more)\)?)\s*$/.test(
367
+ stripTerminalSequencesPreservingLayout(line),
368
+ );
358
369
  }
359
370
 
360
371
  /**
361
372
  * pi 0.87 给工具结果区套了 MouseRegion:整卡左键 click 都会 setExpanded。
362
- * 命中该区域时官方会跳过文本选区,所以可以安全吞掉;单行摘要卡没有声明
363
- * remainder 行,继续走官方整行行为。
373
+ * 声明了展开入口的组件(diff remainder 等)与原生卡(提示在预览末行)只放行入口
374
+ * 行,其余位置直接吞掉 click,避免官方把正文当展开入口;无提示的单行摘要卡保持
375
+ * 官方整行行为。
364
376
  */
365
377
  function blocksOfficialCardToggle(component: any, event: any): boolean {
366
378
  if (event?.type !== "click" || event.button !== "left" || component?.expanded) return false;
367
- const result = component?.resultRendererComponent;
368
- if (typeof result?.isCollapsedHintLine !== "function") return false;
369
379
  const lines = component.render?.(Math.max(1, Math.floor(Number(event.width) || 0)));
370
- const line = Array.isArray(lines) ? lines[event.y] : undefined;
371
- return typeof line === "string" && !result.isCollapsedHintLine(line);
380
+ const row = event.y;
381
+ const line = Array.isArray(lines) ? lines[row] : undefined;
382
+ if (typeof line !== "string") return false;
383
+ return !isCollapsedHintRow(component, line, row);
372
384
  }
373
385
 
374
386
  /**
@@ -410,7 +422,7 @@ function handleFullscreenToolClick(tui: any, packet: SgrMousePacket): boolean {
410
422
  const isGroup = component instanceof ToolGroupComponent;
411
423
  const isAssistant = isCompactAssistantComponent(component);
412
424
  const isThinking = component instanceof ThinkingPreviewBlock;
413
- const isMessage = isMessageDisplayComponent(component);
425
+ const isMessage = isMessageCard(component);
414
426
  if (!isTool && !isGroup && !isAssistant && !isThinking && !isMessage) return false;
415
427
  if (!component.expanded) {
416
428
  // collapsed 仅按钮文本可展开,不能把同一行正文/留白变成点击区。
@@ -419,7 +431,7 @@ function handleFullscreenToolClick(tui: any, packet: SgrMousePacket): boolean {
419
431
  const onHint = Boolean(
420
432
  hint &&
421
433
  (isThinking || (packet.col >= hint.startCol && packet.col <= hint.endCol)) &&
422
- isCollapsedHintRow(component, line),
434
+ isCollapsedHintRow(component, line, hit.localRow),
423
435
  );
424
436
  if (!onHint) {
425
437
  // 面板内非提示区(工具卡标题/摘要行、thinking 预览正文):单击收起整块面板。
@@ -573,7 +585,7 @@ function handleFullscreenToolHover(tui: any, packet: SgrMousePacket): void {
573
585
  } else if (isCompactAssistantComponent(component)) {
574
586
  // compact 摘要行:折叠时仅提示文字高亮,展开卡整体高亮。
575
587
  if (component.expanded || overHint) target = { kind: "assistant", component };
576
- } else if (isMessageDisplayComponent(component)) {
588
+ } else if (isMessageCard(component)) {
577
589
  if (overHint) target = { kind: "message", component };
578
590
  }
579
591
  }
@@ -758,7 +770,7 @@ function buildInteractionFrame(
758
770
  if (
759
771
  box &&
760
772
  COLLAPSED_TOOL_SUMMARY.test(stripTerminalSequences(line)) &&
761
- isCollapsedHintRow(component, line)
773
+ isCollapsedHintRow(component, line, placement.componentRow)
762
774
  ) {
763
775
  regions.push({ kind: "collapsed-hint", row: finalRow, ...box, component });
764
776
  }
@@ -3,6 +3,7 @@ import { ThinkingPreviewBlock } from "../../feature/compact-thinking.ts";
3
3
  import { isCompactAssistantComponent } from "../compact-mode.ts";
4
4
  import { ToolGroupComponent } from "../tool/grouping.ts";
5
5
  import { isMessageDisplayComponent } from "../tool/message-display.ts";
6
+ import { isSubagentNotificationView } from "../tool/subagent-notification.ts";
6
7
  import { isToolExecutionComponent, stripTerminalSequencesPreservingLayout } from "./packets.ts";
7
8
  import { getScrollButtonWidget } from "./scroll.ts";
8
9
 
@@ -15,11 +16,19 @@ export type ComponentRowHit = {
15
16
  owner?: any;
16
17
  };
17
18
 
19
+ /**
20
+ * 消息类卡片:skill/压缩/分支摘要三种消息组件,以及单独出现的 subagent 通知行。
21
+ * 点击、悬停与 single-expand 规则相同。
22
+ */
23
+ export function isMessageCard(component: any): boolean {
24
+ return isMessageDisplayComponent(component) || isSubagentNotificationView(component);
25
+ }
26
+
18
27
  /** 行内 [click to show more] / [↑ Collapse] 提示的命中列区间(1-based,含两端)。 */
19
28
  export function collapsedHintHitbox(line: string): { startCol: number; endCol: number } | null {
20
29
  const plain = stripTerminalSequencesPreservingLayout(line);
21
30
  const match =
22
- /(\([^()\n]* \/ click\)|click to show more|to show more|↑ Collapse|to collapse)(?=\)?\s*$)/.exec(
31
+ /(\([^()\n]*(?: \/ click|, (?:[\w/+]+\s+)?to (?:expand|show more))\)|click to show more|to show more|↑ Collapse|to collapse)(?=\)?\s*$)/.exec(
23
32
  plain,
24
33
  );
25
34
  if (!match?.[1]) return null;
@@ -140,7 +149,7 @@ export function componentAtLocalRow(
140
149
  if (component instanceof ThinkingPreviewBlock) {
141
150
  return { component, row: localRow };
142
151
  }
143
- if (isMessageDisplayComponent(component)) {
152
+ if (isMessageCard(component)) {
144
153
  return { component, row: localRow };
145
154
  }
146
155
  if (component === getScrollButtonWidget()) {
@@ -182,7 +191,7 @@ export function collectFullscreenToolCards(
182
191
  component instanceof ToolGroupComponent ||
183
192
  isCompactAssistantComponent(component) ||
184
193
  component instanceof ThinkingPreviewBlock ||
185
- isMessageDisplayComponent(component)
194
+ isMessageCard(component)
186
195
  ) {
187
196
  out.push(component);
188
197
  return;
@@ -30,6 +30,7 @@ import {
30
30
  expandedBodyComponent,
31
31
  formatToolDuration,
32
32
  insetComponent,
33
+ renderCollapsedToolResult,
33
34
  renderCollapsedToolResultToWidth,
34
35
  textFromResult,
35
36
  toolViewportWidth,
@@ -46,6 +47,12 @@ import {
46
47
  toolIconColor,
47
48
  } from "./tool-state.ts";
48
49
  import { showMoreHintText } from "./names.ts";
50
+ import { oneLine } from "../../utils/format.ts";
51
+ import {
52
+ isFailedSubagentStatus,
53
+ measuredStats,
54
+ agentStatusLabel,
55
+ } from "./subagent-notification.ts";
49
56
 
50
57
  /** 成功勾:亮绿 truecolor(与 message-display 一致)。 */
51
58
  const BRIGHT_GREEN = "";
@@ -82,6 +89,8 @@ function renderDefault(tool: any, slot: "renderCall" | "renderResult", args: any
82
89
  return new Text(fallback, 0, 0);
83
90
  }
84
91
 
92
+ export const AGENT_TOOL_NAME = "Agent";
93
+
85
94
  type ParsedTask = { id: string; status: string; subject: string };
86
95
 
87
96
  function parseTaskList(text: string): ParsedTask[] {
@@ -185,6 +194,92 @@ function shellDurationText(toolName: string, context: any, isPartial: boolean):
185
194
  return elapsed === undefined ? "" : formatToolDuration(elapsed);
186
195
  }
187
196
 
197
+ /**
198
+ * Agent 的 renderResult:与 pi-subagents 保持同样的分支,但收敛成 ccstyle 的 ↳ 行。
199
+ * running/partial 转交原渲染器(只有它有 spinner frame 和 activity 文案);
200
+ * background/completed/stopped/error 画单行摘要,展开态结果正文交给通用展开视图。
201
+ */
202
+ function renderAgentResult(
203
+ originalTool: any,
204
+ result: any,
205
+ options: any,
206
+ theme: any,
207
+ context: any,
208
+ expanded: boolean,
209
+ isError: boolean,
210
+ ): any | undefined {
211
+ const details = result?.details;
212
+ const text = textFromResult(result);
213
+ // 没拿到子代理状态(pi-subagents 原始路径的兜底)就沿用通用渲染。
214
+ if (!details?.status) return undefined;
215
+
216
+ const isPartial = Boolean(options?.isPartial);
217
+ const status = String(details.status);
218
+
219
+ // running:pi-subagents 的 spinner/activity 组件才能实时动,原样转交。
220
+ if (isPartial || status === "running") {
221
+ return renderDefault(originalTool, "renderResult", [result, options, theme, context], text);
222
+ }
223
+
224
+ const stats = measuredStats(details).join(" · ");
225
+
226
+ const agentId = details.agentId ? `ID: ${details.agentId}` : "";
227
+ const tail = (body: string) => (stats ? `${body} · ${stats}` : body);
228
+
229
+ if (status === "background") {
230
+ if (!expanded) {
231
+ const body = tail(`Running in background${agentId ? ` · ${agentId}` : ""}`);
232
+ return new Text(theme.fg("muted", renderCollapsedToolResult(body, "")), 0, 0);
233
+ }
234
+ const view = renderExpandedToolResult(
235
+ `Running in background${agentId ? ` · ${agentId}` : ""}`,
236
+ theme,
237
+ false,
238
+ context?.lastComponent,
239
+ context?.args,
240
+ context,
241
+ true,
242
+ );
243
+ return view;
244
+ }
245
+
246
+ if (status === "completed" || status === "steered") {
247
+ if (!expanded) {
248
+ const label = status === "steered" ? "Wrapped up (turn limit)" : "Done";
249
+ const body = [label, stats].filter(Boolean).join(" · ");
250
+ return new Text(theme.fg("muted", renderCollapsedToolResult(body, "")), 0, 0);
251
+ }
252
+ const view = renderExpandedToolResult(
253
+ text || "Done",
254
+ theme,
255
+ Boolean(isError),
256
+ context?.lastComponent,
257
+ context?.args,
258
+ context,
259
+ true,
260
+ );
261
+ return view;
262
+ }
263
+
264
+ if (status === "stopped") {
265
+ const body = tail("Stopped");
266
+ return new Text(theme.fg("muted", renderCollapsedToolResult(body, "")), 0, 0);
267
+ }
268
+
269
+ // error / aborted:首行错误文案 + 统计,与通用失败行口径一致。
270
+ const label = agentStatusLabel(status);
271
+ const firstLine = text.split("\n").find((line) => line.trim()) ?? label;
272
+ const body = tail(oneLine(firstLine, 72));
273
+ return new Text(
274
+ theme.fg(
275
+ isFailedSubagentStatus(status) ? "error" : "muted",
276
+ renderCollapsedToolResult(body, ""),
277
+ ),
278
+ 0,
279
+ 0,
280
+ );
281
+ }
282
+
188
283
  /** 用 ccstyle call/result 包装任意工具定义。 */
189
284
  export function createCcstyleTool(
190
285
  originalTool: any,
@@ -315,6 +410,20 @@ export function createCcstyleTool(
315
410
 
316
411
  const expanded = isToolExpanded(options, context);
317
412
  const isError = Boolean(options?.isError || context?.isError);
413
+ // Agent:running 转交原渲染器(spinner/activity 只有它有);其余画 ↳ 摘要行。
414
+ // toolName 大小写宽松匹配:上游改名也不静默失效。
415
+ if (toolName.toLowerCase() === AGENT_TOOL_NAME.toLowerCase()) {
416
+ const agentResult = renderAgentResult(
417
+ originalTool,
418
+ result,
419
+ options,
420
+ theme,
421
+ context,
422
+ expanded,
423
+ isError,
424
+ );
425
+ if (agentResult) return agentResult;
426
+ }
318
427
  // codemode:折叠态自己画子调用树,展开态给全部子调用与去掉脚本头的输出
319
428
  if (toolName === CODEMODE_TOOL_NAME) {
320
429
  if (!options?.isPartial) setToolVisualState(context, isError ? "error" : "success");