@herbertgao/pi-extensions 2026.8.7 → 2026.8.8

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 (99) hide show
  1. package/README.md +7 -5
  2. package/node_modules/@herbertgao/pi-cc-extensions/README.en.md +1 -1
  3. package/node_modules/@herbertgao/pi-cc-extensions/README.md +1 -1
  4. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/compact-thinking.ts +63 -15
  5. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/message-display.ts +78 -16
  6. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/hover.ts +13 -0
  7. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/interaction.ts +17 -6
  8. package/node_modules/@herbertgao/pi-cc-extensions/package.json +3 -3
  9. package/node_modules/@juicesharp/rpiv-ask-user-question/README.md +4 -4
  10. package/node_modules/@juicesharp/rpiv-ask-user-question/ask-user-question.ts +154 -96
  11. package/node_modules/@juicesharp/rpiv-ask-user-question/docs/hosts.md +3 -0
  12. package/node_modules/@juicesharp/rpiv-ask-user-question/docs/keyboard.md +10 -2
  13. package/node_modules/@juicesharp/rpiv-ask-user-question/docs/localization.md +7 -5
  14. package/node_modules/@juicesharp/rpiv-ask-user-question/docs/tool-schema.md +11 -5
  15. package/node_modules/@juicesharp/rpiv-ask-user-question/locales/en.json +3 -0
  16. package/node_modules/@juicesharp/rpiv-ask-user-question/package.json +2 -2
  17. package/node_modules/@juicesharp/rpiv-ask-user-question/state/key-router.ts +137 -96
  18. package/node_modules/@juicesharp/rpiv-ask-user-question/state/questionnaire-session.ts +44 -32
  19. package/node_modules/@juicesharp/rpiv-ask-user-question/state/selectors/projections.ts +22 -15
  20. package/node_modules/@juicesharp/rpiv-ask-user-question/state/state-reducer.ts +11 -1
  21. package/node_modules/@juicesharp/rpiv-ask-user-question/tool/response-envelope.ts +11 -0
  22. package/node_modules/@juicesharp/rpiv-ask-user-question/tool/types.ts +11 -0
  23. package/node_modules/@juicesharp/rpiv-ask-user-question/view/components/multi-select-view.ts +31 -17
  24. package/node_modules/@juicesharp/rpiv-ask-user-question/view/components/preview/preview-block-renderer.ts +15 -6
  25. package/node_modules/@juicesharp/rpiv-ask-user-question/view/components/wrapping-select.ts +15 -6
  26. package/node_modules/@juicesharp/rpiv-ask-user-question/view/dialog-builder.ts +56 -34
  27. package/node_modules/@juicesharp/rpiv-ask-user-question/view/tab-content-strategy.ts +50 -4
  28. package/node_modules/@narumitw/pi-btw/package.json +2 -2
  29. package/node_modules/@pi-plugins/fast-mode/dist/index.mjs +44 -1856
  30. package/node_modules/@pi-plugins/fast-mode/dist/index.mjs.map +1 -1
  31. package/node_modules/@pi-plugins/fast-mode/package.json +4 -6
  32. package/node_modules/pi-lens/CHANGELOG.md +90 -0
  33. package/node_modules/pi-lens/README.md +106 -0
  34. package/node_modules/pi-lens/dist/clients/bash-file-access.js +97 -6
  35. package/node_modules/pi-lens/dist/clients/blocker-past-eof.js +16 -6
  36. package/node_modules/pi-lens/dist/clients/bounded-telemetry.js +14 -0
  37. package/node_modules/pi-lens/dist/clients/cache-observability.js +353 -76
  38. package/node_modules/pi-lens/dist/clients/degradation-ledger.js +22 -1
  39. package/node_modules/pi-lens/dist/clients/demoted-finding-render.js +155 -0
  40. package/node_modules/pi-lens/dist/clients/deps/pi-tui.js +8 -2
  41. package/node_modules/pi-lens/dist/clients/deps/typebox.js +8 -3
  42. package/node_modules/pi-lens/dist/clients/dispatch/runners/detekt.js +9 -6
  43. package/node_modules/pi-lens/dist/clients/dispatch/runners/hadolint.js +9 -7
  44. package/node_modules/pi-lens/dist/clients/dispatch/runners/htmlhint.js +8 -6
  45. package/node_modules/pi-lens/dist/clients/dispatch/runners/markdownlint.js +8 -10
  46. package/node_modules/pi-lens/dist/clients/dispatch/runners/mypy.js +10 -10
  47. package/node_modules/pi-lens/dist/clients/dispatch/runners/oxlint.js +20 -5
  48. package/node_modules/pi-lens/dist/clients/dispatch/runners/phpstan.js +44 -11
  49. package/node_modules/pi-lens/dist/clients/dispatch/runners/spellcheck.js +10 -15
  50. package/node_modules/pi-lens/dist/clients/dispatch/runners/sqlfluff.js +19 -11
  51. package/node_modules/pi-lens/dist/clients/dispatch/runners/stylelint.js +35 -13
  52. package/node_modules/pi-lens/dist/clients/dispatch/runners/swiftlint.js +8 -8
  53. package/node_modules/pi-lens/dist/clients/dispatch/runners/taplo.js +110 -23
  54. package/node_modules/pi-lens/dist/clients/dispatch/runners/tflint.js +12 -7
  55. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/tool-failure.js +117 -1
  56. package/node_modules/pi-lens/dist/clients/dispatch/runners/vale.js +16 -14
  57. package/node_modules/pi-lens/dist/clients/dispatch/runners/yamllint.js +8 -9
  58. package/node_modules/pi-lens/dist/clients/finding-delivery-gate.js +22 -0
  59. package/node_modules/pi-lens/dist/clients/jscpd-client.js +5 -2
  60. package/node_modules/pi-lens/dist/clients/language-profile.js +7 -2
  61. package/node_modules/pi-lens/dist/clients/lsp/cascade-tier.js +27 -2
  62. package/node_modules/pi-lens/dist/clients/lsp/client.js +144 -5
  63. package/node_modules/pi-lens/dist/clients/lsp/index.js +151 -6
  64. package/node_modules/pi-lens/dist/clients/module-report.js +14 -1
  65. package/node_modules/pi-lens/dist/clients/ndjson-logger.js +114 -23
  66. package/node_modules/pi-lens/dist/clients/project-diagnostics/scanner.js +18 -9
  67. package/node_modules/pi-lens/dist/clients/project-report.js +62 -27
  68. package/node_modules/pi-lens/dist/clients/read-guard-logger.js +7 -2
  69. package/node_modules/pi-lens/dist/clients/read-guard.js +88 -19
  70. package/node_modules/pi-lens/dist/clients/review-graph/builder.js +194 -31
  71. package/node_modules/pi-lens/dist/clients/review-graph/revision-drift.js +21 -0
  72. package/node_modules/pi-lens/dist/clients/review-graph/workspace-modules.js +6 -3
  73. package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +44 -0
  74. package/node_modules/pi-lens/dist/clients/runtime-session.js +41 -0
  75. package/node_modules/pi-lens/dist/clients/runtime-turn.js +47 -2
  76. package/node_modules/pi-lens/dist/clients/session-event-guard.js +184 -0
  77. package/node_modules/pi-lens/dist/clients/source-filter.js +6 -2
  78. package/node_modules/pi-lens/dist/clients/startup-scan.js +5 -2
  79. package/node_modules/pi-lens/dist/clients/tree-sitter-logger.js +1 -0
  80. package/node_modules/pi-lens/dist/index.js +1224 -393
  81. package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +10 -3
  82. package/node_modules/pi-lens/package.json +18 -5
  83. package/node_modules/pi-lens/scripts/install-selftest.mjs +99 -11
  84. package/node_modules/pi-lens/scripts/lib/host-provided-deps.mjs +75 -0
  85. package/node_modules/pi-lens/scripts/lib/warm-loader-cache.mjs +285 -0
  86. package/node_modules/pi-lens/scripts/warm-loader-cache.mjs +233 -0
  87. package/node_modules/pi-web-access/CHANGELOG.md +14 -0
  88. package/node_modules/pi-web-access/README.md +6 -5
  89. package/node_modules/pi-web-access/chrome-cookies.ts +86 -15
  90. package/node_modules/pi-web-access/data-uri-sanitize.ts +406 -0
  91. package/node_modules/pi-web-access/extract.ts +12 -2
  92. package/node_modules/pi-web-access/firecrawl.ts +18 -2
  93. package/node_modules/pi-web-access/github-extract.ts +65 -36
  94. package/node_modules/pi-web-access/index.ts +19 -4
  95. package/node_modules/pi-web-access/openai-search.ts +17 -6
  96. package/node_modules/pi-web-access/package.json +1 -1
  97. package/node_modules/pi-web-access/pdf-extract.ts +18 -5
  98. package/node_modules/pi-web-access/ssrf-protection.ts +12 -2
  99. package/package.json +9 -9
package/README.md CHANGED
@@ -11,18 +11,20 @@ pi install npm:@herbertgao/pi-extensions
11
11
  The package bundles 11 active `@herbertgao/*` child packages, including `@herbertgao/pi-cc-extensions`, plus the following upstream packages under their original names:
12
12
 
13
13
  - `@dietrichgebert/ponytail@4.9.0`
14
- - `@juicesharp/rpiv-ask-user-question@2.6.4`
15
- - `@narumitw/pi-btw@0.55.0`
16
- - `@pi-plugins/fast-mode@0.1.9`
14
+ - `@juicesharp/rpiv-ask-user-question@2.7.0`
15
+ - `@narumitw/pi-btw@0.55.1`
16
+ - `@pi-plugins/fast-mode@0.1.10`
17
17
  - `pi-mcp-adapter@2.27.0`
18
18
  - `pi-footer@0.5.1`
19
- - `pi-lens@4.1.0`
20
- - `pi-web-access@0.24.0`
19
+ - `pi-lens@4.1.1`
20
+ - `pi-web-access@0.24.1`
21
21
  - `remote-pi@0.7.0`
22
22
  - `@czottmann/pi-automode@1.11.0`
23
23
 
24
24
  Pi loads their extensions and skills through `node_modules/` paths inside one package root. The upstream companions are pinned and bundled, not forked or renamed.
25
25
 
26
+ `pi-lens@4.1.1` improves diagnostics, cache, and LSP observability; prevents session-switch crashes; restores oxlint warnings; speeds large ignored-tree scans; and retries failed log writes once.
27
+
26
28
  If `pi-footer` was installed separately before upgrading to an aggregate release that includes it, remove the standalone source shown by `pi list` so only the bundled copy loads. For the pinned standalone install used while preparing this integration:
27
29
 
28
30
  ```bash
@@ -101,5 +101,5 @@ bun run typecheck
101
101
 
102
102
  ## Credits
103
103
 
104
- - Derived from [`minuque/pi-cc-extensions`](https://github.com/minuque/pi-cc-extensions) under the MIT license.
104
+ - Derived from [`minuque/pi-cc-extensions` v0.8.64](https://github.com/minuque/pi-cc-extensions/releases/tag/v0.8.64) (`4709081`) under the MIT license.
105
105
  - Rich diffs are adapted from [`MasuRii/pi-tool-display`](https://github.com/MasuRii/pi-tool-display) (MIT). See [`extensions/renderer/tool/diff/ATTRIBUTION.md`](./extensions/renderer/tool/diff/ATTRIBUTION.md).
@@ -101,5 +101,5 @@ bun run typecheck
101
101
 
102
102
  ## 致谢
103
103
 
104
- - 基于 MIT 许可的 [`minuque/pi-cc-extensions`](https://github.com/minuque/pi-cc-extensions) 衍生维护。
104
+ - 基于 MIT 许可的 [`minuque/pi-cc-extensions` v0.8.64](https://github.com/minuque/pi-cc-extensions/releases/tag/v0.8.64)(`4709081`)衍生维护。
105
105
  - Rich diff 改编自 [`MasuRii/pi-tool-display`](https://github.com/MasuRii/pi-tool-display)(MIT);详见 [`extensions/renderer/tool/diff/ATTRIBUTION.md`](./extensions/renderer/tool/diff/ATTRIBUTION.md)。
@@ -196,12 +196,60 @@ function cachePreview(key: string, entry: PreviewCacheEntry): void {
196
196
 
197
197
  export function clearThinkingPreviewCache(): void {
198
198
  previewCache.clear();
199
+ expandedWrapCache.clear();
200
+ }
201
+
202
+ type ExpandedWrapEntry = { text: string; lines: string[] };
203
+ const expandedWrapCache = new Map<string, ExpandedWrapEntry>();
204
+ export const THINKING_EXPANDED_WRAP_CACHE_MAX = 64;
205
+
206
+ export function thinkingRunKey(messageTimestamp: number, runStartIndex: number): string {
207
+ return `${messageTimestamp}:${runStartIndex}`;
208
+ }
209
+
210
+ function expandedWrapKey(messageTimestamp: number, runStartIndex: number, width: number): string {
211
+ return `${thinkingRunKey(messageTimestamp, runStartIndex)}:${width}`;
212
+ }
213
+
214
+ function wrapExpandedThinking(
215
+ messageTimestamp: number,
216
+ runStartIndex: number,
217
+ text: string,
218
+ width: number,
219
+ ): string[] {
220
+ const key = expandedWrapKey(messageTimestamp, runStartIndex, width);
221
+ const hit = expandedWrapCache.get(key);
222
+ if (hit && hit.text === text) {
223
+ expandedWrapCache.delete(key);
224
+ expandedWrapCache.set(key, hit);
225
+ return hit.lines;
226
+ }
227
+ const lines = wrapTextWithAnsi(text.replace(/\t/g, " "), Math.max(1, width));
228
+ expandedWrapCache.delete(key);
229
+ while (expandedWrapCache.size >= THINKING_EXPANDED_WRAP_CACHE_MAX) {
230
+ const oldest = expandedWrapCache.keys().next().value;
231
+ if (oldest === undefined) break;
232
+ expandedWrapCache.delete(oldest);
233
+ }
234
+ expandedWrapCache.set(key, { text, lines });
235
+ return lines;
236
+ }
237
+
238
+ function evictExpandedWraps(messageTimestamp: number, runStartIndex: number): void {
239
+ const prefix = `${thinkingRunKey(messageTimestamp, runStartIndex)}:`;
240
+ for (const key of expandedWrapCache.keys()) {
241
+ if (key.startsWith(prefix)) expandedWrapCache.delete(key);
242
+ }
199
243
  }
200
244
 
201
245
  export function thinkingPreviewCacheSize(): number {
202
246
  return previewCache.size;
203
247
  }
204
248
 
249
+ export function thinkingExpandedWrapCacheSize(): number {
250
+ return expandedWrapCache.size;
251
+ }
252
+
205
253
  /** 按可见宽度估算折行数,不走 Text 全量 wrap。无换行长段也能计到隐藏行。 */
206
254
  function countWrappedLines(text: string, contentWidth: number): number {
207
255
  if (!text) return 0;
@@ -274,19 +322,19 @@ function hiddenPreviewHint(
274
322
  return undefined;
275
323
  }
276
324
 
277
- const expandedThinking = new Set<number>();
325
+ const expandedThinking = new Set<string>();
278
326
 
279
327
  /** 折叠预览 + 展开全文。fullscreen 点击 hint 展开、双击整块收起,对齐工具卡。 */
280
328
  export class ThinkingPreviewBlock implements Component {
281
329
  private heading: string;
282
330
  private text: string;
283
331
  private padding: number;
284
- private messageTimestamp: number;
332
+ readonly messageTimestamp: number;
333
+ readonly runStartIndex: number;
285
334
  private style: (text: string) => string;
286
335
  private theme: Theme | undefined;
287
336
  private _expanded: boolean;
288
337
  private hintHovered = false;
289
- private expandedBody: { width: number; lines: string[] } | undefined;
290
338
  private collapsedMemo:
291
339
  | {
292
340
  width: number;
@@ -304,14 +352,16 @@ export class ThinkingPreviewBlock implements Component {
304
352
  messageTimestamp: number,
305
353
  style: (text: string) => string,
306
354
  theme?: Theme,
355
+ runStartIndex = 0,
307
356
  ) {
308
357
  this.heading = heading;
309
358
  this.text = text;
310
359
  this.padding = padding;
311
360
  this.messageTimestamp = messageTimestamp;
361
+ this.runStartIndex = runStartIndex;
312
362
  this.style = style;
313
363
  this.theme = theme;
314
- this._expanded = expandedThinking.has(messageTimestamp);
364
+ this._expanded = expandedThinking.has(thinkingRunKey(messageTimestamp, runStartIndex));
315
365
  }
316
366
 
317
367
  private paint(color: string, text: string): string {
@@ -327,10 +377,11 @@ export class ThinkingPreviewBlock implements Component {
327
377
  setExpanded(expanded: boolean): void {
328
378
  if (this._expanded !== expanded) this.collapsedMemo = undefined;
329
379
  this._expanded = expanded;
330
- if (expanded) expandedThinking.add(this.messageTimestamp);
380
+ const key = thinkingRunKey(this.messageTimestamp, this.runStartIndex);
381
+ if (expanded) expandedThinking.add(key);
331
382
  else {
332
- expandedThinking.delete(this.messageTimestamp);
333
- this.expandedBody = undefined;
383
+ expandedThinking.delete(key);
384
+ evictExpandedWraps(this.messageTimestamp, this.runStartIndex);
334
385
  }
335
386
  }
336
387
 
@@ -364,13 +415,10 @@ export class ThinkingPreviewBlock implements Component {
364
415
  return { lines: [], hiddenLines: 0 };
365
416
  }
366
417
  if (this._expanded) {
367
- if (this.expandedBody?.width !== width) {
368
- this.expandedBody = {
369
- width,
370
- lines: wrapTextWithAnsi(this.text.replace(/\t/g, " "), Math.max(1, width)),
371
- };
372
- }
373
- return { lines: this.expandedBody.lines, hiddenLines: 0 };
418
+ return {
419
+ lines: wrapExpandedThinking(this.messageTimestamp, this.runStartIndex, this.text, width),
420
+ hiddenLines: 0,
421
+ };
374
422
  }
375
423
  const preview = layoutThinkingPreview(this.text, width, padding);
376
424
  return { lines: preview.visible, hiddenLines: preview.hiddenLines };
@@ -426,7 +474,6 @@ export class ThinkingPreviewBlock implements Component {
426
474
 
427
475
  invalidate() {
428
476
  this.collapsedMemo = undefined;
429
- this.expandedBody = undefined;
430
477
  }
431
478
  }
432
479
 
@@ -710,6 +757,7 @@ function compactThinking(pi: ExtensionAPI) {
710
757
  message.timestamp,
711
758
  thinkingStyle,
712
759
  activeTheme,
760
+ runStartIndex,
713
761
  ),
714
762
  );
715
763
 
@@ -17,6 +17,9 @@ import { showMoreHintText } from "./show-more-hint.ts";
17
17
  */
18
18
 
19
19
  const MESSAGE_DISPLAY_PATCH = Symbol.for("pi.ccstyle.message-display-patch");
20
+ const MESSAGE_DISPLAY_ORIGINAL_BG = Symbol.for("pi.ccstyle.message-display-original-bg");
21
+ const MESSAGE_DISPLAY_ORIGINAL_PADDING = Symbol.for("pi.ccstyle.message-display-original-padding");
22
+ const MESSAGE_DISPLAY_ORIGINAL_SAVED = Symbol.for("pi.ccstyle.message-display-original-saved");
20
23
 
21
24
  // 与 renderer/index.ts renderCall 的成功勾一致:亮绿 ✓(truecolor ANSI)。
22
25
  const BRIGHT_GREEN = "\x1b[38;2;80;220;100m";
@@ -55,21 +58,54 @@ const BRANCH_KIND: DisplayKind = {
55
58
  body: (component) => String(component.message?.summary ?? ""),
56
59
  };
57
60
 
58
- function renderCcstyle(component: any, kind: DisplayKind): void {
61
+ function ensureHintHover(component: any): void {
62
+ if (typeof component.setHintHovered === "function") return;
63
+ component.setHintHovered = function (this: any, hovered: boolean) {
64
+ if (this.hintHovered === hovered) return;
65
+ this.hintHovered = hovered;
66
+ this.invalidate?.();
67
+ };
68
+ }
69
+
70
+ function rememberMessageDisplay(component: any): void {
71
+ if (component[MESSAGE_DISPLAY_ORIGINAL_SAVED]) return;
72
+ component[MESSAGE_DISPLAY_ORIGINAL_BG] = component.bgFn;
73
+ component[MESSAGE_DISPLAY_ORIGINAL_PADDING] = [component.paddingX, component.paddingY];
74
+ component[MESSAGE_DISPLAY_ORIGINAL_SAVED] = true;
75
+ }
76
+
77
+ function restoreMessageDisplay(component: any): void {
78
+ if (!component[MESSAGE_DISPLAY_ORIGINAL_SAVED]) return;
79
+ const [paddingX, paddingY] = component[MESSAGE_DISPLAY_ORIGINAL_PADDING];
80
+ component.paddingX = paddingX;
81
+ component.paddingY = paddingY;
82
+ component.setBgFn?.(component[MESSAGE_DISPLAY_ORIGINAL_BG]);
83
+ delete component[MESSAGE_DISPLAY_ORIGINAL_BG];
84
+ delete component[MESSAGE_DISPLAY_ORIGINAL_PADDING];
85
+ delete component[MESSAGE_DISPLAY_ORIGINAL_SAVED];
86
+ }
87
+
88
+ function renderCcstyle(component: any, kind: DisplayKind): boolean {
59
89
  const theme = displayTheme;
60
- if (!theme) return; // 主题未就绪时保留原生渲染
90
+ if (!theme) return false; // 主题未就绪时回退原生渲染
91
+ rememberMessageDisplay(component);
61
92
  if (component.bgFn) component.setBgFn?.(undefined); // 与工具调用一致,去掉灰底
62
- if (component.paddingY !== 0) {
63
- component._ccstyleOriginalPaddingY = component.paddingY;
64
- component.paddingY = 0; // 工具组件 paddingY=0;原生 Box 默认 1,会上下各留一个空行
65
- }
93
+ component.paddingY = 0; // 工具组件 paddingY=0;原生 Box 默认 1,会上下各留一个空行
66
94
  component.clear();
95
+ ensureHintHover(component);
67
96
  const icon = `${BRIGHT_GREEN}✓${ANSI_FG_RESET}`; // 已完成消息,等同工具成功态
68
97
  const title = theme.fg("toolTitle", kind.title(component));
69
98
  if (!component.expanded) {
70
- const hint = theme.fg("dim", ` • ${showMoreHintText()}`);
99
+ const hovered = component.hintHovered === true;
100
+ const hint = `${theme.fg("dim", " • ")}${theme.fg(hovered ? "text" : "dim", showMoreHintText())}`;
71
101
  component.addChild(new Text(`${icon} ${title}${hint}`, 0, 0));
72
- return;
102
+ return true;
103
+ }
104
+ // 展开卡与 tool 一致:userMessageBg + 上下左右 1 格
105
+ component.paddingX = 1;
106
+ component.paddingY = 1;
107
+ if (typeof theme.bg === "function" && typeof component.setBgFn === "function") {
108
+ component.setBgFn((text: string) => theme.bg("userMessageBg", text));
73
109
  }
74
110
  component.addChild(new Text(`${icon} ${title}`, 0, 0));
75
111
  component.addChild(new Spacer(1));
@@ -78,6 +114,7 @@ function renderCcstyle(component: any, kind: DisplayKind): void {
78
114
  color: (text: string) => theme.fg("customMessageText", text),
79
115
  }),
80
116
  );
117
+ return true;
81
118
  }
82
119
 
83
120
  type PatchEntry = {
@@ -91,28 +128,42 @@ export function installMessageDisplayRendering(): () => void {
91
128
  const host = globalThis as any;
92
129
  const previous = host[MESSAGE_DISPLAY_PATCH];
93
130
  if (previous) previous.dispose();
94
- const patch: { active: boolean; entries: PatchEntry[]; dispose: () => void } = {
131
+ const patch: {
132
+ active: boolean;
133
+ entries: PatchEntry[];
134
+ components: Set<WeakRef<any>>;
135
+ tracked: WeakSet<object>;
136
+ dispose: () => void;
137
+ } = {
95
138
  active: true,
96
139
  entries: [],
140
+ components: new Set(),
141
+ tracked: new WeakSet(),
97
142
  dispose: () => {},
98
143
  };
144
+ const track = (component: object): void => {
145
+ for (const ref of patch.components) {
146
+ if (!ref.deref()) patch.components.delete(ref);
147
+ }
148
+ if (patch.tracked.has(component)) return;
149
+ patch.tracked.add(component);
150
+ patch.components.add(new WeakRef(component));
151
+ };
99
152
  const installOne = (ComponentClass: any, kind: DisplayKind): void => {
100
153
  const prototype = ComponentClass.prototype;
101
154
  const original = prototype.updateDisplay;
102
155
  const installed = function (this: any) {
103
156
  if (patch.active && config.mode !== "off") {
104
157
  try {
105
- renderCcstyle(this, kind);
106
- return;
158
+ if (renderCcstyle(this, kind)) {
159
+ track(this);
160
+ return;
161
+ }
107
162
  } catch {
108
163
  // 渲染失败回退原生
109
164
  }
110
165
  }
111
- // 回退原生前恢复 paddingY,避免原生渲染丢失上下内边距
112
- if (this._ccstyleOriginalPaddingY !== undefined) {
113
- this.paddingY = this._ccstyleOriginalPaddingY;
114
- delete this._ccstyleOriginalPaddingY;
115
- }
166
+ restoreMessageDisplay(this);
116
167
  original.call(this);
117
168
  };
118
169
  prototype.updateDisplay = installed;
@@ -128,6 +179,17 @@ export function installMessageDisplayRendering(): () => void {
128
179
  entry.prototype.updateDisplay = entry.original;
129
180
  }
130
181
  }
182
+ for (const ref of patch.components) {
183
+ const component = ref.deref();
184
+ if (!component) continue;
185
+ try {
186
+ restoreMessageDisplay(component);
187
+ component.updateDisplay();
188
+ } catch {
189
+ // 单个陈旧组件不应阻断其余原型与组件恢复
190
+ }
191
+ }
192
+ patch.components.clear();
131
193
  if (host[MESSAGE_DISPLAY_PATCH] === patch) delete host[MESSAGE_DISPLAY_PATCH];
132
194
  };
133
195
  host[MESSAGE_DISPLAY_PATCH] = patch;
@@ -54,6 +54,16 @@ export function setHoveredThinking(block: ThinkingPreviewBlock | null): boolean
54
54
  return true;
55
55
  }
56
56
 
57
+ let hoveredMessageDisplay: any = null;
58
+
59
+ export function setHoveredMessageDisplay(component: any): boolean {
60
+ if (component === hoveredMessageDisplay) return false;
61
+ hoveredMessageDisplay?.setHintHovered?.(false);
62
+ hoveredMessageDisplay = component;
63
+ component?.setHintHovered?.(true);
64
+ return true;
65
+ }
66
+
57
67
  let hoveredToolIoView: ExpandedToolIoView | null = null;
58
68
  let hoveredToolIoSection: ToolIoSection | null = null;
59
69
 
@@ -110,6 +120,7 @@ export type FullscreenHoverTarget =
110
120
  | { kind: "button" }
111
121
  | { kind: "group"; component: ToolGroupComponent }
112
122
  | { kind: "thinking"; component: ThinkingPreviewBlock }
123
+ | { kind: "message"; component: any }
113
124
  | { kind: "assistant"; component: any }
114
125
  | {
115
126
  kind: "tool";
@@ -133,6 +144,8 @@ export function applyFullscreenHover(tui: any, target: FullscreenHoverTarget | n
133
144
  if (setHoveredToolGroup(nextGroup)) changed = true;
134
145
  const nextThinking = target?.kind === "thinking" ? target.component : null;
135
146
  if (setHoveredThinking(nextThinking)) changed = true;
147
+ const nextMessage = target?.kind === "message" ? target.component : null;
148
+ if (setHoveredMessageDisplay(nextMessage)) changed = true;
136
149
  const nextAssistant = target?.kind === "assistant" ? target.component : null;
137
150
  if (setHoveredCompactAssistant(nextAssistant)) changed = true;
138
151
  const nextView = target?.kind === "tool" ? target.view : null;
@@ -1,4 +1,4 @@
1
- import { ThinkingPreviewBlock } from "../../feature/compact-thinking.ts";
1
+ import { ThinkingPreviewBlock, thinkingRunKey } from "../../feature/compact-thinking.ts";
2
2
  import { hasActiveTextPreview, showTextPreview } from "../../feature/context.ts";
3
3
  import { ToolGroupComponent } from "../tool/grouping.ts";
4
4
  import { isCompactAssistantComponent, setHoveredCompactAssistant } from "../compact-mode.ts";
@@ -58,6 +58,7 @@ import {
58
58
  setHoveredToolCallId,
59
59
  setHoveredToolGroup,
60
60
  setHoveredThinking,
61
+ setHoveredMessageDisplay,
61
62
  setHoveredToolIo,
62
63
  type FullscreenHoverTarget,
63
64
  } from "./hover.ts";
@@ -155,13 +156,20 @@ function updateToolSummaryHover(tui: any, packet: SgrMousePacket): void {
155
156
  }
156
157
 
157
158
  const EXPAND_PANEL_DOUBLE_CLICK_MS = 400;
158
- let lastExpandPanelClick: { card: any; at: number } | null = null;
159
+ let lastExpandPanelClick: { id: unknown; at: number } | null = null;
160
+
161
+ function expandPanelIdentity(card: any): unknown {
162
+ return card instanceof ThinkingPreviewBlock
163
+ ? thinkingRunKey(card.messageTimestamp, card.runStartIndex)
164
+ : card;
165
+ }
159
166
 
160
167
  function isExpandPanelDoubleClick(card: any): boolean {
161
168
  const now = Date.now();
169
+ const id = expandPanelIdentity(card);
162
170
  const prev = lastExpandPanelClick;
163
- lastExpandPanelClick = { card, at: now };
164
- return Boolean(prev && prev.card === card && now - prev.at <= EXPAND_PANEL_DOUBLE_CLICK_MS);
171
+ lastExpandPanelClick = { id, at: now };
172
+ return Boolean(prev && prev.id === id && now - prev.at <= EXPAND_PANEL_DOUBLE_CLICK_MS);
165
173
  }
166
174
 
167
175
  function clearExpandPanelDoubleClick(): void {
@@ -174,6 +182,7 @@ function collapseExpandedCard(tui: any, card: any): boolean {
174
182
  setHoveredToolCallId(null);
175
183
  setHoveredToolGroup(null);
176
184
  setHoveredThinking(null);
185
+ setHoveredMessageDisplay(null);
177
186
  setHoveredToolIo(null, null);
178
187
  setHoveredCompactAssistant(null);
179
188
  card.invalidate?.();
@@ -346,6 +355,7 @@ function handleFullscreenToolClick(tui: any, packet: SgrMousePacket): boolean {
346
355
  setHoveredToolCallId(null);
347
356
  setHoveredToolGroup(null);
348
357
  setHoveredThinking(null);
358
+ setHoveredMessageDisplay(null);
349
359
  setHoveredToolIo(null, null);
350
360
  setHoveredCompactAssistant(null);
351
361
  card.invalidate?.();
@@ -417,8 +427,7 @@ function handleFullscreenToolHover(tui: any, packet: SgrMousePacket): void {
417
427
  // compact 摘要行:折叠时仅提示文字高亮,展开卡整体高亮。
418
428
  if (component.expanded || overHint) target = { kind: "assistant", component };
419
429
  } else if (isMessageDisplayComponent(component)) {
420
- // skill/compaction/branch:折叠只点 hint;展开后双击收起。
421
- if (component.expanded || overHint) target = { kind: "assistant", component };
430
+ if (overHint) target = { kind: "message", component };
422
431
  }
423
432
  }
424
433
  }
@@ -815,6 +824,7 @@ export function teardownToolMouseInteraction(
815
824
  setHoveredToolCallId(null);
816
825
  setHoveredToolGroup(null);
817
826
  setHoveredThinking(null);
827
+ setHoveredMessageDisplay(null);
818
828
  setHoveredToolIo(null, null);
819
829
  setHoveredCompactAssistant(null);
820
830
  clearExpandPanelDoubleClick();
@@ -842,6 +852,7 @@ export function teardownToolMouseInteraction(
842
852
  export function resetToolHoverState(): void {
843
853
  setHoveredToolCallId(null);
844
854
  setHoveredThinking(null);
855
+ setHoveredMessageDisplay(null);
845
856
  setHoveredCompactAssistant(null);
846
857
  setScrollButtonVisible(false);
847
858
  setScrollButtonHovered(false);
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@herbertgao/pi-cc-extensions",
3
- "version": "0.8.56",
3
+ "version": "0.8.57",
4
4
  "license": "MIT",
5
5
  "description": "A Pi productivity suite with Claude Code-style UI, context inspection, and agent/session references",
6
6
  "type": "module",
@@ -69,8 +69,8 @@
69
69
  },
70
70
  "x-upstream": {
71
71
  "package": "pi-cc-extensions",
72
- "version": "0.8.63",
72
+ "version": "0.8.64",
73
73
  "repository": "https://github.com/minuque/pi-cc-extensions",
74
- "commit": "6b7447eb9d82afa9ea26dd7433c59ff56663098e"
74
+ "commit": "4709081a28255b8bdfe610620a2a9ee8951013d6"
75
75
  }
76
76
  }
@@ -25,7 +25,7 @@ Nothing to set up — the tool is live as soon as Pi restarts. Hand the model a
25
25
 
26
26
  > Add caching to the API client.
27
27
 
28
- Rather than picking a strategy on your behalf, the model calls `ask_user_question` and a dialog takes over the bottom of your terminal. Move with `↑`/`↓`, choose with `Enter`, press `n` to attach a note, or land on the `Type something.` row to answer in your own words. While typing, `Shift+Enter` adds a line, `Ctrl+G` opens Pi's configured external editor, and `Ctrl+U` clears the draft; browsing another option and returning keeps what you wrote. `Esc` abandons the questionnaire entirely.
28
+ Rather than picking a strategy on your behalf, the model calls `ask_user_question` and a dialog takes over the bottom of your terminal. Move with `↑`/`↓`, choose with `Enter`, press `n` to attach a note to a question — or a global note to the whole questionnaire from the Submit tab — or land on the `Type something.` row to answer in your own words. While typing, `Shift+Enter` adds a line, `Ctrl+G` opens Pi's configured external editor, and `Ctrl+U` clears the draft; browsing another option and returning keeps what you wrote. `Esc` abandons the questionnaire entirely.
29
29
 
30
30
  When the questionnaire begins waiting in an interactive TTY, it emits one standard terminal BEL (`\x07`). Your terminal configuration determines whether that appears as an audible alert, a visual alert, or nothing; redirected and non-TTY output is untouched.
31
31
 
@@ -33,7 +33,7 @@ When the questionnaire begins waiting in an interactive TTY, it emits one standa
33
33
 
34
34
  When the model asks several things at once, `Tab` moves between them and a Submit tab reviews everything before it goes back:
35
35
 
36
- ![Submit tab of a four-question dialog: a Review your answers list showing Feature Type set to Bug fix and Testing set to Unit tests plus Integration tests, a warning naming Design Tab and Release as still unanswered, and a picker offering Submit answers or Cancel](https://raw.githubusercontent.com/juicesharp/rpiv-mono/main/packages/rpiv-ask-user-question/docs/submit-tab.jpg)
36
+ ![Submit tab of a four-question dialog: a Review your answers list showing Feature Type set to Bug fix and Testing set to Unit tests plus Integration tests, a warning naming Design Tab and Release as still unanswered, a picker offering Submit answers or Cancel, and a dim bottom key-hint row including n to add a note](https://raw.githubusercontent.com/juicesharp/rpiv-mono/main/packages/rpiv-ask-user-question/docs/submit-tab.jpg)
37
37
 
38
38
  ## What you get
39
39
 
@@ -41,9 +41,9 @@ When the model asks several things at once, `Tab` moves between them and a Submi
41
41
  - **You can always answer in your own words** — a `Type something.` row is appended to every question, single- or multi-select, widens to the full pane while you type, keeps its multiline draft visible in that row while you browse, and supports Pi's `Shift+Enter` newline and `Ctrl+G` external-editor flows.
42
42
  - **Compare real artifacts, not just labels** — an option can carry a markdown `preview` (ASCII mockup, code, diagram, config) that renders in a bordered box beside the option list.
43
43
  - **One interruption, not five** — up to four questions arrive in a single tabbed dialog, and the Submit tab lists your answers and names anything still blank before you commit.
44
- - **Notes on any answer** — `n` opens a multiline note editor on any question tab; the note travels back to the model alongside the choice without marking the question answered.
44
+ - **Notes on any answer — or on all of them** — `n` opens a multiline note editor on any question tab, and on the Submit tab it opens one global note for the whole questionnaire. Per-question notes reach the model as `user notes: <text>`, the global note as `global note: <text>`; neither marks a question answered.
45
45
  - **Read the transcript behind the dialog** — `Ctrl+]` collapses the overlay so you can scroll the conversation, then brings it back with your answers intact.
46
- - **Works outside the terminal too** — in RPC and ACP hosts such as the VS Code pendant or Zed the questionnaire walks through the host's native dialogs, and in non-interactive runs the tool is removed from the model's tool list instead of failing every call.
46
+ - **Works outside the terminal too** — in RPC and ACP hosts such as the VS Code pendant or Zed the questionnaire walks through the host's native dialogs (notes are terminal-only and do not carry over), and in non-interactive runs the tool is removed from the model's tool list instead of failing every call.
47
47
 
48
48
  ## Configuration
49
49