@herbertgao/pi-extensions 2026.8.7 → 2026.8.9

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 (161) hide show
  1. package/README.md +8 -6
  2. package/node_modules/@czottmann/pi-automode/CHANGELOG.md +24 -0
  3. package/node_modules/@czottmann/pi-automode/README.md +78 -112
  4. package/node_modules/@czottmann/pi-automode/docs/GLOSSARY.md +18 -14
  5. package/node_modules/@czottmann/pi-automode/docs/adr/ADR-001-permission-precedence-and-trust-boundaries.md +44 -0
  6. package/node_modules/@czottmann/pi-automode/docs/adr/INDEX.md +5 -0
  7. package/node_modules/@czottmann/pi-automode/docs/automode-classifier-flow.md +198 -104
  8. package/node_modules/@czottmann/pi-automode/docs/configuration.md +155 -0
  9. package/node_modules/@czottmann/pi-automode/docs/defaults.md +55 -14
  10. package/node_modules/@czottmann/pi-automode/docs/diagnostics.md +90 -0
  11. package/node_modules/@czottmann/pi-automode/docs/observability-logging.md +61 -26
  12. package/node_modules/@czottmann/pi-automode/examples/automode.local.json +5 -0
  13. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/classifier.ts +172 -18
  14. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/config.ts +127 -30
  15. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/constants.ts +3 -0
  16. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/extension.ts +273 -31
  17. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/log.ts +60 -5
  18. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/paths.ts +92 -8
  19. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/permissions.ts +203 -13
  20. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/state.ts +1 -0
  21. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/types.ts +11 -0
  22. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/utils.ts +9 -1
  23. package/node_modules/@czottmann/pi-automode/package.json +9 -2
  24. package/node_modules/@czottmann/pi-automode/skills/automode-diagnostics/SKILL.md +63 -0
  25. package/node_modules/@herbertgao/pi-cc-extensions/README.en.md +3 -3
  26. package/node_modules/@herbertgao/pi-cc-extensions/README.md +3 -3
  27. package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/panel.ts +8 -3
  28. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/compact-thinking.ts +63 -15
  29. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/context.ts +56 -29
  30. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/message-display.ts +78 -16
  31. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/hover.ts +13 -0
  32. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/interaction.ts +30 -17
  33. package/node_modules/@herbertgao/pi-cc-extensions/package.json +3 -3
  34. package/node_modules/@herbertgao/pi-handoff/CHANGELOG.md +29 -0
  35. package/node_modules/@herbertgao/pi-handoff/README.md +15 -18
  36. package/node_modules/@herbertgao/pi-handoff/package.json +11 -4
  37. package/node_modules/@herbertgao/pi-handoff/src/complete-text.ts +33 -0
  38. package/node_modules/@herbertgao/pi-handoff/src/index.ts +127 -232
  39. package/node_modules/@herbertgao/pi-handoff/src/session-query.ts +15 -30
  40. package/node_modules/@herbertgao/pi-mermaid-open/CHANGELOG.md +14 -0
  41. package/node_modules/@herbertgao/pi-mermaid-open/README.md +21 -4
  42. package/node_modules/@herbertgao/pi-mermaid-open/herdr-plugin/herdr-plugin.toml +11 -0
  43. package/node_modules/@herbertgao/pi-mermaid-open/herdr-plugin/viewer.mjs +305 -0
  44. package/node_modules/@herbertgao/pi-mermaid-open/package.json +15 -4
  45. package/node_modules/@herbertgao/pi-mermaid-open/src/index.ts +351 -101
  46. package/node_modules/@herbertgao/pi-preferred-thinking/CHANGELOG.md +12 -0
  47. package/node_modules/@herbertgao/pi-preferred-thinking/README.md +30 -14
  48. package/node_modules/@herbertgao/pi-preferred-thinking/package.json +7 -4
  49. package/node_modules/@herbertgao/pi-preferred-thinking/src/index.ts +140 -85
  50. package/node_modules/@herbertgao/pi-recap/CHANGELOG.md +12 -0
  51. package/node_modules/@herbertgao/pi-recap/README.md +5 -3
  52. package/node_modules/@herbertgao/pi-recap/package.json +9 -3
  53. package/node_modules/@herbertgao/pi-recap/src/index.ts +3 -7
  54. package/node_modules/@herbertgao/pi-recap/src/model-picker.ts +6 -19
  55. package/node_modules/@herbertgao/pi-recap/src/models.ts +9 -21
  56. package/node_modules/@herbertgao/pi-recap/src/tui.ts +1 -1
  57. package/node_modules/@herbertgao/pi-rename/CHANGELOG.md +49 -0
  58. package/node_modules/@herbertgao/pi-rename/README.md +13 -11
  59. package/node_modules/@herbertgao/pi-rename/package.json +13 -3
  60. package/node_modules/@herbertgao/pi-rename/src/index.ts +135 -59
  61. package/node_modules/@herbertgao/pi-rename/src/models.ts +5 -13
  62. package/node_modules/@herbertgao/pi-rename/src/naming.ts +2 -6
  63. package/node_modules/@herbertgao/pi-stash/package.json +3 -3
  64. package/node_modules/@herbertgao/pi-subagents/CHANGELOG.md +8 -0
  65. package/node_modules/@herbertgao/pi-subagents/README.md +16 -3
  66. package/node_modules/@herbertgao/pi-subagents/package.json +1 -1
  67. package/node_modules/@herbertgao/pi-subagents/src/cross-extension-rpc.ts +15 -4
  68. package/node_modules/@herbertgao/pi-subagents/src/index.ts +16 -0
  69. package/node_modules/@herbertgao/pi-subagents/src/ui/conversation-viewer.ts +5 -1
  70. package/node_modules/@juicesharp/rpiv-ask-user-question/README.md +4 -4
  71. package/node_modules/@juicesharp/rpiv-ask-user-question/ask-user-question.ts +154 -96
  72. package/node_modules/@juicesharp/rpiv-ask-user-question/docs/hosts.md +3 -0
  73. package/node_modules/@juicesharp/rpiv-ask-user-question/docs/keyboard.md +10 -2
  74. package/node_modules/@juicesharp/rpiv-ask-user-question/docs/localization.md +7 -5
  75. package/node_modules/@juicesharp/rpiv-ask-user-question/docs/tool-schema.md +11 -5
  76. package/node_modules/@juicesharp/rpiv-ask-user-question/locales/en.json +3 -0
  77. package/node_modules/@juicesharp/rpiv-ask-user-question/package.json +2 -2
  78. package/node_modules/@juicesharp/rpiv-ask-user-question/state/key-router.ts +137 -96
  79. package/node_modules/@juicesharp/rpiv-ask-user-question/state/questionnaire-session.ts +44 -32
  80. package/node_modules/@juicesharp/rpiv-ask-user-question/state/selectors/projections.ts +22 -15
  81. package/node_modules/@juicesharp/rpiv-ask-user-question/state/state-reducer.ts +11 -1
  82. package/node_modules/@juicesharp/rpiv-ask-user-question/tool/response-envelope.ts +11 -0
  83. package/node_modules/@juicesharp/rpiv-ask-user-question/tool/types.ts +11 -0
  84. package/node_modules/@juicesharp/rpiv-ask-user-question/view/components/multi-select-view.ts +31 -17
  85. package/node_modules/@juicesharp/rpiv-ask-user-question/view/components/preview/preview-block-renderer.ts +15 -6
  86. package/node_modules/@juicesharp/rpiv-ask-user-question/view/components/wrapping-select.ts +15 -6
  87. package/node_modules/@juicesharp/rpiv-ask-user-question/view/dialog-builder.ts +56 -34
  88. package/node_modules/@juicesharp/rpiv-ask-user-question/view/tab-content-strategy.ts +50 -4
  89. package/node_modules/@narumitw/pi-btw/package.json +2 -2
  90. package/node_modules/@pi-plugins/fast-mode/dist/index.mjs +44 -1856
  91. package/node_modules/@pi-plugins/fast-mode/dist/index.mjs.map +1 -1
  92. package/node_modules/@pi-plugins/fast-mode/package.json +4 -6
  93. package/node_modules/pi-lens/CHANGELOG.md +90 -0
  94. package/node_modules/pi-lens/README.md +106 -0
  95. package/node_modules/pi-lens/dist/clients/bash-file-access.js +97 -6
  96. package/node_modules/pi-lens/dist/clients/blocker-past-eof.js +16 -6
  97. package/node_modules/pi-lens/dist/clients/bounded-telemetry.js +14 -0
  98. package/node_modules/pi-lens/dist/clients/cache-observability.js +353 -76
  99. package/node_modules/pi-lens/dist/clients/degradation-ledger.js +22 -1
  100. package/node_modules/pi-lens/dist/clients/demoted-finding-render.js +155 -0
  101. package/node_modules/pi-lens/dist/clients/deps/pi-tui.js +8 -2
  102. package/node_modules/pi-lens/dist/clients/deps/typebox.js +8 -3
  103. package/node_modules/pi-lens/dist/clients/dispatch/runners/detekt.js +9 -6
  104. package/node_modules/pi-lens/dist/clients/dispatch/runners/hadolint.js +9 -7
  105. package/node_modules/pi-lens/dist/clients/dispatch/runners/htmlhint.js +8 -6
  106. package/node_modules/pi-lens/dist/clients/dispatch/runners/markdownlint.js +8 -10
  107. package/node_modules/pi-lens/dist/clients/dispatch/runners/mypy.js +10 -10
  108. package/node_modules/pi-lens/dist/clients/dispatch/runners/oxlint.js +20 -5
  109. package/node_modules/pi-lens/dist/clients/dispatch/runners/phpstan.js +44 -11
  110. package/node_modules/pi-lens/dist/clients/dispatch/runners/spellcheck.js +10 -15
  111. package/node_modules/pi-lens/dist/clients/dispatch/runners/sqlfluff.js +19 -11
  112. package/node_modules/pi-lens/dist/clients/dispatch/runners/stylelint.js +35 -13
  113. package/node_modules/pi-lens/dist/clients/dispatch/runners/swiftlint.js +8 -8
  114. package/node_modules/pi-lens/dist/clients/dispatch/runners/taplo.js +110 -23
  115. package/node_modules/pi-lens/dist/clients/dispatch/runners/tflint.js +12 -7
  116. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/tool-failure.js +117 -1
  117. package/node_modules/pi-lens/dist/clients/dispatch/runners/vale.js +16 -14
  118. package/node_modules/pi-lens/dist/clients/dispatch/runners/yamllint.js +8 -9
  119. package/node_modules/pi-lens/dist/clients/finding-delivery-gate.js +22 -0
  120. package/node_modules/pi-lens/dist/clients/jscpd-client.js +5 -2
  121. package/node_modules/pi-lens/dist/clients/language-profile.js +7 -2
  122. package/node_modules/pi-lens/dist/clients/lsp/cascade-tier.js +27 -2
  123. package/node_modules/pi-lens/dist/clients/lsp/client.js +144 -5
  124. package/node_modules/pi-lens/dist/clients/lsp/index.js +151 -6
  125. package/node_modules/pi-lens/dist/clients/module-report.js +14 -1
  126. package/node_modules/pi-lens/dist/clients/ndjson-logger.js +114 -23
  127. package/node_modules/pi-lens/dist/clients/project-diagnostics/scanner.js +18 -9
  128. package/node_modules/pi-lens/dist/clients/project-report.js +62 -27
  129. package/node_modules/pi-lens/dist/clients/read-guard-logger.js +7 -2
  130. package/node_modules/pi-lens/dist/clients/read-guard.js +88 -19
  131. package/node_modules/pi-lens/dist/clients/review-graph/builder.js +194 -31
  132. package/node_modules/pi-lens/dist/clients/review-graph/revision-drift.js +21 -0
  133. package/node_modules/pi-lens/dist/clients/review-graph/workspace-modules.js +6 -3
  134. package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +44 -0
  135. package/node_modules/pi-lens/dist/clients/runtime-session.js +41 -0
  136. package/node_modules/pi-lens/dist/clients/runtime-turn.js +47 -2
  137. package/node_modules/pi-lens/dist/clients/session-event-guard.js +184 -0
  138. package/node_modules/pi-lens/dist/clients/source-filter.js +6 -2
  139. package/node_modules/pi-lens/dist/clients/startup-scan.js +5 -2
  140. package/node_modules/pi-lens/dist/clients/tree-sitter-logger.js +1 -0
  141. package/node_modules/pi-lens/dist/index.js +1224 -393
  142. package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +10 -3
  143. package/node_modules/pi-lens/package.json +18 -5
  144. package/node_modules/pi-lens/scripts/install-selftest.mjs +99 -11
  145. package/node_modules/pi-lens/scripts/lib/host-provided-deps.mjs +75 -0
  146. package/node_modules/pi-lens/scripts/lib/warm-loader-cache.mjs +285 -0
  147. package/node_modules/pi-lens/scripts/warm-loader-cache.mjs +233 -0
  148. package/node_modules/pi-web-access/CHANGELOG.md +27 -0
  149. package/node_modules/pi-web-access/README.md +8 -7
  150. package/node_modules/pi-web-access/chrome-cookies.ts +96 -20
  151. package/node_modules/pi-web-access/data-uri-sanitize.ts +406 -0
  152. package/node_modules/pi-web-access/extract.ts +12 -2
  153. package/node_modules/pi-web-access/firecrawl.ts +18 -2
  154. package/node_modules/pi-web-access/gemini-search.ts +28 -10
  155. package/node_modules/pi-web-access/github-extract.ts +65 -36
  156. package/node_modules/pi-web-access/index.ts +43 -14
  157. package/node_modules/pi-web-access/openai-search.ts +17 -6
  158. package/node_modules/pi-web-access/package.json +1 -1
  159. package/node_modules/pi-web-access/pdf-extract.ts +18 -5
  160. package/node_modules/pi-web-access/ssrf-protection.ts +12 -2
  161. package/package.json +19 -18
@@ -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
 
@@ -9,7 +9,7 @@ import { Key, Markdown, matchesKey, truncateToWidth, visibleWidth } from "@earen
9
9
  export type ContextPart = {
10
10
  label: string;
11
11
  tokens: number;
12
- color: "accent" | "success" | "warning" | "muted" | "dim";
12
+ color: "accent" | "success" | "warning" | "muted" | "dim" | "error";
13
13
  };
14
14
 
15
15
  type PreviewKey = "systemPrompt" | "tools" | "toolResults" | "contextFiles" | "skills";
@@ -43,11 +43,16 @@ export function escCloseHitbox(bounds: DialogBounds): {
43
43
  };
44
44
  }
45
45
 
46
- let activeTextPreviews = 0;
46
+ let activeContextOverlays = 0;
47
47
 
48
- /** fullscreen 输入包装用于把鼠标事件继续传给当前文本预览 overlay。 */
48
+ /** fullscreen 输入包装用于把鼠标事件继续传给当前 context overlay。 */
49
49
  export function hasActiveTextPreview(): boolean {
50
- return activeTextPreviews > 0;
50
+ return activeContextOverlays > 0;
51
+ }
52
+
53
+ /** fullscreen overlays fall back to 1002; restore motion events for hover states. */
54
+ function ensureFullscreenMouseMotion(tui: any): void {
55
+ if (tui.mode === "fullscreen") tui.terminal?.write?.("\x1b[?1003h\x1b[?1006h");
51
56
  }
52
57
 
53
58
  export async function showTextPreview(
@@ -56,10 +61,11 @@ export async function showTextPreview(
56
61
  rawContent: string,
57
62
  ): Promise<void> {
58
63
  const content = normalizePreviewText(rawContent);
59
- activeTextPreviews++;
64
+ activeContextOverlays++;
60
65
  try {
61
66
  await ctx.ui.custom(
62
67
  (tui, theme, _keybindings, done) => {
68
+ ensureFullscreenMouseMotion(tui);
63
69
  let scrollOffset = 0;
64
70
  let pageSize = 1;
65
71
  let totalLines = 1;
@@ -249,7 +255,7 @@ export async function showTextPreview(
249
255
  },
250
256
  );
251
257
  } finally {
252
- activeTextPreviews--;
258
+ activeContextOverlays--;
253
259
  }
254
260
  }
255
261
 
@@ -424,7 +430,7 @@ export function collectContextBreakdown(ctx: ExtensionCommandContext): ContextBr
424
430
  const parts: ContextPart[] = [
425
431
  { label: "System prompt", tokens: baseSystem, color: "accent" },
426
432
  { label: "Tools", tokens: toolTokens, color: "success" },
427
- { label: "Context files", tokens: contextFileTokens, color: "warning" },
433
+ { label: "Memory", tokens: contextFileTokens, color: "error" },
428
434
  { label: "Skills", tokens: skillTokens, color: "warning" },
429
435
  { label: "User messages", tokens: user, color: "muted" },
430
436
  { label: "Assistant messages", tokens: assistant, color: "accent" },
@@ -448,10 +454,12 @@ export default function contextUsageExtension(pi: ExtensionAPI) {
448
454
  const breakdown = collectContextBreakdown(ctx);
449
455
  const estimated = breakdown.parts.reduce((sum, part) => sum + part.tokens, 0);
450
456
  const fixedParts = breakdown.parts.filter(
451
- (part) => part.label === "System prompt" || part.label === "Tools",
457
+ (part) =>
458
+ part.label === "System prompt" || part.label === "Memory" || part.label === "Tools",
452
459
  );
453
460
  const variableParts = breakdown.parts.filter(
454
- (part) => part.label !== "System prompt" && part.label !== "Tools",
461
+ (part) =>
462
+ part.label !== "System prompt" && part.label !== "Memory" && part.label !== "Tools",
455
463
  );
456
464
  const orderedParts = [...fixedParts, ...variableParts];
457
465
  const fixedTokens = fixedParts.reduce((sum, part) => sum + part.tokens, 0);
@@ -527,9 +535,9 @@ export default function contextUsageExtension(pi: ExtensionAPI) {
527
535
  },
528
536
  {
529
537
  key: "contextFiles",
530
- label: "Context files",
531
- title: "Context Files",
532
- content: contextFilesContent || "No context files loaded.",
538
+ label: "Memory",
539
+ title: "Memory Files",
540
+ content: contextFilesContent || "No memory files loaded.",
533
541
  },
534
542
  {
535
543
  key: "skills",
@@ -549,8 +557,10 @@ export default function contextUsageExtension(pi: ExtensionAPI) {
549
557
  let selectedPreviewIndex = 0;
550
558
 
551
559
  while (true) {
552
- const action = await ctx.ui.custom(
560
+ activeContextOverlays++;
561
+ const actionPromise = ctx.ui.custom(
553
562
  (tui, theme, _keybindings, done) => {
563
+ ensureFullscreenMouseMotion(tui);
554
564
  let previewHitboxes: Array<{
555
565
  key: PreviewKey;
556
566
  row: number;
@@ -558,6 +568,8 @@ export default function contextUsageExtension(pi: ExtensionAPI) {
558
568
  endCol: number;
559
569
  }> = [];
560
570
  let escHitbox: { row: number; startCol: number; endCol: number } | undefined;
571
+ let escHovered = false;
572
+ let hoveredKey: PreviewKey | undefined;
561
573
 
562
574
  const padLine = (text: string, width: number): string => {
563
575
  const truncated = truncateToWidth(text, width, "…");
@@ -588,27 +600,32 @@ export default function contextUsageExtension(pi: ExtensionAPI) {
588
600
  }
589
601
 
590
602
  const mouse = parseSgrMousePacket(data);
591
- if (
592
- mouse?.final !== "M" ||
593
- mouseBaseButton(mouse.code) !== 0 ||
594
- (mouse.code & 32) !== 0
595
- )
596
- return;
597
- if (
603
+ if (!mouse || mouse.final !== "M") return;
604
+ const overEsc = Boolean(
598
605
  escHitbox &&
599
- mouse.row === escHitbox.row &&
600
- mouse.col >= escHitbox.startCol &&
601
- mouse.col <= escHitbox.endCol
602
- ) {
603
- done(undefined);
604
- return;
605
- }
606
+ mouse.row === escHitbox.row &&
607
+ mouse.col >= escHitbox.startCol &&
608
+ mouse.col <= escHitbox.endCol,
609
+ );
606
610
  const hitbox = previewHitboxes.find(
607
611
  (candidate) =>
608
612
  mouse.row === candidate.row &&
609
613
  mouse.col >= candidate.startCol &&
610
614
  mouse.col <= candidate.endCol,
611
615
  );
616
+ if ((mouse.code & 32) !== 0) {
617
+ if (overEsc !== escHovered || hitbox?.key !== hoveredKey) {
618
+ escHovered = overEsc;
619
+ hoveredKey = hitbox?.key;
620
+ tui.requestRender();
621
+ }
622
+ return;
623
+ }
624
+ if (mouseBaseButton(mouse.code) !== 0) return;
625
+ if (overEsc) {
626
+ done(undefined);
627
+ return;
628
+ }
612
629
  if (hitbox) {
613
630
  selectedPreviewIndex = Math.max(
614
631
  0,
@@ -643,6 +660,9 @@ export default function contextUsageExtension(pi: ExtensionAPI) {
643
660
  Math.max(...allParts.map((part) => part.label.length)),
644
661
  );
645
662
  const selectedLabel = visiblePreviews[selectedPreviewIndex]?.label;
663
+ const hoverLabel = visiblePreviews.find(
664
+ (preview) => preview.key === hoveredKey,
665
+ )?.label;
646
666
  const partRows = allParts.map((part) => {
647
667
  const pct = contextWindow > 0 ? (part.tokens / contextWindow) * 100 : 0;
648
668
  const swatch = theme.fg(part.color, "■");
@@ -651,12 +671,13 @@ export default function contextUsageExtension(pi: ExtensionAPI) {
651
671
  const selected = part.label === selectedLabel;
652
672
  const prefix = selected ? "› " : " ";
653
673
  const row = padLine(`${prefix}${swatch} ${label} ${amount}`, inner);
654
- return selected ? theme.bg("selectedBg", row) : row;
674
+ if (selected) return theme.bg("selectedBg", row);
675
+ return part.label === hoverLabel ? theme.bg("customMessageBg", row) : row;
655
676
  });
656
677
  const border = (text: string) => theme.fg("border", text);
657
678
  const lines = [
658
679
  border(`╭${"─".repeat(inner)}╮`),
659
- `${border("│")}${padLine(` ${title} ${theme.fg("muted", subtitle)}`, inner - escWidth)}${theme.fg("muted", "[esc]")}${border("│")}`,
680
+ `${border("│")}${padLine(` ${title} ${theme.fg("muted", subtitle)}`, inner - escWidth)}${theme.fg(escHovered ? "text" : "muted", "[esc]")}${border("│")}`,
660
681
  `${border("├")}${border("─".repeat(inner))}${border("┤")}`,
661
682
  `${border("│")}${padLine(` ${segments}`, inner)}${border("│")}`,
662
683
  `${border("│")}${" ".repeat(inner)}${border("│")}`,
@@ -706,6 +727,12 @@ export default function contextUsageExtension(pi: ExtensionAPI) {
706
727
  },
707
728
  },
708
729
  );
730
+ let action;
731
+ try {
732
+ action = await actionPromise;
733
+ } finally {
734
+ activeContextOverlays--;
735
+ }
709
736
 
710
737
  if (!action) break;
711
738
  const preview = previewByKey.get(action as PreviewKey);
@@ -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: Record<PropertyKey, unknown>): 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;