@herbertgao/pi-extensions 2026.9.3 → 2026.9.5

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 (143) hide show
  1. package/README.md +2 -1
  2. package/THIRD_PARTY_NOTICES.md +25 -0
  3. package/node_modules/@dietrichgebert/ponytail/.qoder-plugin/plugin.json +1 -1
  4. package/node_modules/@dietrichgebert/ponytail/README.es.md +33 -2
  5. package/node_modules/@dietrichgebert/ponytail/README.ko.md +33 -2
  6. package/node_modules/@dietrichgebert/ponytail/README.md +49 -5
  7. package/node_modules/@dietrichgebert/ponytail/assets/retriever-icon.png +0 -0
  8. package/node_modules/@dietrichgebert/ponytail/assets/retriever-logo-dark.svg +4 -0
  9. package/node_modules/@dietrichgebert/ponytail/assets/retriever-logo-light.svg +4 -0
  10. package/node_modules/@dietrichgebert/ponytail/hooks/claude-codex-hooks.json +0 -3
  11. package/node_modules/@dietrichgebert/ponytail/hooks/cursor-hooks.json +17 -0
  12. package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-activate.js +22 -3
  13. package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-mode-tracker.js +27 -2
  14. package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-runtime.js +61 -2
  15. package/node_modules/@dietrichgebert/ponytail/package.json +2 -1
  16. package/node_modules/@dietrichgebert/ponytail/scripts/cursor-hooks.js +135 -0
  17. package/node_modules/@dietrichgebert/ponytail/scripts/uninstall.js +21 -4
  18. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/reference/index.ts +33 -15
  19. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/compact-mode.ts +3 -2
  20. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/default-mode.ts +16 -11
  21. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/interaction.ts +6 -2
  22. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/scroll.ts +23 -1
  23. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-renderer.ts +88 -19
  24. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/grouping.ts +12 -7
  25. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/result.ts +98 -0
  26. package/node_modules/@herbertgao/pi-cc-extensions/package.json +3 -3
  27. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/economics.ts +4 -4
  28. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/extension.ts +41 -1
  29. package/node_modules/@juicesharp/rpiv-ask-user-question/ask-user-question.ts +5 -1
  30. package/node_modules/@juicesharp/rpiv-ask-user-question/events.ts +5 -1
  31. package/node_modules/@juicesharp/rpiv-ask-user-question/package.json +3 -2
  32. package/node_modules/@juicesharp/rpiv-ask-user-question/tool/normalize-params.ts +56 -0
  33. package/node_modules/@juicesharp/rpiv-ask-user-question/view/components/option-list-view.ts +1 -1
  34. package/node_modules/@juicesharp/rpiv-ask-user-question/view/components/tab-bar.ts +1 -2
  35. package/node_modules/@pi-plugins/fast-mode/README.md +31 -44
  36. package/node_modules/@pi-plugins/fast-mode/dist/index.mjs +20914 -30
  37. package/node_modules/@pi-plugins/fast-mode/dist/index.mjs.map +1 -1
  38. package/node_modules/@pi-plugins/fast-mode/package.json +5 -7
  39. package/node_modules/@tifan/pi-copy-response/README.md +2 -2
  40. package/node_modules/@tifan/pi-copy-response/package.json +3 -2
  41. package/node_modules/@tifan/pi-inline-skills/README.md +2 -2
  42. package/node_modules/@tifan/pi-inline-skills/package.json +3 -2
  43. package/node_modules/@tifan/pi-mermaid-open/README.md +1 -1
  44. package/node_modules/@tifan/pi-mermaid-open/package.json +3 -2
  45. package/node_modules/@tifan/pi-preferred-thinking/package.json +3 -2
  46. package/node_modules/@tifan/pi-recap/package.json +3 -2
  47. package/node_modules/pi-antigravity/LICENSE +21 -0
  48. package/node_modules/pi-antigravity/README.md +194 -0
  49. package/node_modules/pi-antigravity/package.json +67 -0
  50. package/node_modules/pi-antigravity/src/auth/index.ts +14 -0
  51. package/node_modules/pi-antigravity/src/auth/oauth.ts +442 -0
  52. package/node_modules/pi-antigravity/src/client/client.ts +561 -0
  53. package/node_modules/pi-antigravity/src/client/index.ts +1 -0
  54. package/node_modules/pi-antigravity/src/diagnostics/diagnostics.ts +96 -0
  55. package/node_modules/pi-antigravity/src/diagnostics/index.ts +1 -0
  56. package/node_modules/pi-antigravity/src/image/image.ts +336 -0
  57. package/node_modules/pi-antigravity/src/image/index.ts +1 -0
  58. package/node_modules/pi-antigravity/src/index.ts +280 -0
  59. package/node_modules/pi-antigravity/src/models/discovery.ts +154 -0
  60. package/node_modules/pi-antigravity/src/models/grouping.ts +424 -0
  61. package/node_modules/pi-antigravity/src/models/index.ts +3 -0
  62. package/node_modules/pi-antigravity/src/models/models.ts +500 -0
  63. package/node_modules/pi-antigravity/src/stream/index.ts +1 -0
  64. package/node_modules/pi-antigravity/src/stream/stream.ts +1460 -0
  65. package/node_modules/pi-antigravity/src/types/enums.ts +42 -0
  66. package/node_modules/pi-antigravity/src/types/index.ts +2 -0
  67. package/node_modules/pi-antigravity/src/types/types.ts +292 -0
  68. package/node_modules/pi-antigravity/src/usage/index.ts +1 -0
  69. package/node_modules/pi-antigravity/src/usage/usage.ts +371 -0
  70. package/node_modules/pi-antigravity/src/utils/http.ts +91 -0
  71. package/node_modules/pi-antigravity/src/utils/index.ts +3 -0
  72. package/node_modules/pi-antigravity/src/utils/security.ts +73 -0
  73. package/node_modules/pi-antigravity/src/utils/util.ts +132 -0
  74. package/node_modules/pi-antigravity/tsconfig.json +21 -0
  75. package/node_modules/pi-mcp-adapter/CHANGELOG.md +78 -0
  76. package/node_modules/pi-mcp-adapter/README.md +125 -19
  77. package/node_modules/pi-mcp-adapter/agent-plugin-loader.ts +69 -30
  78. package/node_modules/pi-mcp-adapter/agent-plugin-provenance.ts +36 -0
  79. package/node_modules/pi-mcp-adapter/claude-plugin-loader.ts +365 -0
  80. package/node_modules/pi-mcp-adapter/commands.ts +13 -15
  81. package/node_modules/pi-mcp-adapter/config.ts +198 -30
  82. package/node_modules/pi-mcp-adapter/consent-manager.ts +40 -9
  83. package/node_modules/pi-mcp-adapter/direct-tool-surface.ts +250 -0
  84. package/node_modules/pi-mcp-adapter/direct-tools.ts +9 -228
  85. package/node_modules/pi-mcp-adapter/dist/agent-plugin-loader.js +74 -32
  86. package/node_modules/pi-mcp-adapter/dist/agent-plugin-loader.js.map +1 -1
  87. package/node_modules/pi-mcp-adapter/dist/agent-plugin-provenance.d.ts +1 -0
  88. package/node_modules/pi-mcp-adapter/dist/agent-plugin-provenance.js +31 -0
  89. package/node_modules/pi-mcp-adapter/dist/agent-plugin-provenance.js.map +1 -0
  90. package/node_modules/pi-mcp-adapter/dist/claude-plugin-loader.d.ts +9 -0
  91. package/node_modules/pi-mcp-adapter/dist/claude-plugin-loader.js +362 -0
  92. package/node_modules/pi-mcp-adapter/dist/claude-plugin-loader.js.map +1 -0
  93. package/node_modules/pi-mcp-adapter/dist/config.d.ts +3 -1
  94. package/node_modules/pi-mcp-adapter/dist/config.js +189 -27
  95. package/node_modules/pi-mcp-adapter/dist/config.js.map +1 -1
  96. package/node_modules/pi-mcp-adapter/dist/metadata-cache.js +7 -16
  97. package/node_modules/pi-mcp-adapter/dist/metadata-cache.js.map +1 -1
  98. package/node_modules/pi-mcp-adapter/dist/package-mcp-loader.js +5 -17
  99. package/node_modules/pi-mcp-adapter/dist/package-mcp-loader.js.map +1 -1
  100. package/node_modules/pi-mcp-adapter/dist/types.d.ts +30 -4
  101. package/node_modules/pi-mcp-adapter/dist/types.js +26 -0
  102. package/node_modules/pi-mcp-adapter/dist/types.js.map +1 -1
  103. package/node_modules/pi-mcp-adapter/dist/utils.d.ts +6 -0
  104. package/node_modules/pi-mcp-adapter/dist/utils.js +65 -6
  105. package/node_modules/pi-mcp-adapter/dist/utils.js.map +1 -1
  106. package/node_modules/pi-mcp-adapter/host-html-template.ts +20 -19
  107. package/node_modules/pi-mcp-adapter/http-ca.ts +84 -0
  108. package/node_modules/pi-mcp-adapter/index.ts +827 -164
  109. package/node_modules/pi-mcp-adapter/init.ts +48 -10
  110. package/node_modules/pi-mcp-adapter/lazy-loader.ts +7 -0
  111. package/node_modules/pi-mcp-adapter/mcp-auth-fetch.ts +113 -0
  112. package/node_modules/pi-mcp-adapter/mcp-auth-flow.ts +256 -77
  113. package/node_modules/pi-mcp-adapter/mcp-auth.ts +316 -54
  114. package/node_modules/pi-mcp-adapter/mcp-code.ts +45 -18
  115. package/node_modules/pi-mcp-adapter/mcp-install.ts +60 -0
  116. package/node_modules/pi-mcp-adapter/mcp-oauth-provider.ts +70 -6
  117. package/node_modules/pi-mcp-adapter/mcp-output-guard.ts +53 -42
  118. package/node_modules/pi-mcp-adapter/mcp-panel-theme.ts +104 -0
  119. package/node_modules/pi-mcp-adapter/mcp-panel.ts +366 -332
  120. package/node_modules/pi-mcp-adapter/mcp-references.ts +9 -14
  121. package/node_modules/pi-mcp-adapter/mcp-script-worker.mjs +3 -1
  122. package/node_modules/pi-mcp-adapter/mcp-setup-panel.ts +383 -341
  123. package/node_modules/pi-mcp-adapter/mcp-status.ts +5 -0
  124. package/node_modules/pi-mcp-adapter/metadata-cache.ts +7 -16
  125. package/node_modules/pi-mcp-adapter/namespace-tools.ts +61 -3
  126. package/node_modules/pi-mcp-adapter/oauth.ts +6 -2
  127. package/node_modules/pi-mcp-adapter/package-mcp-loader.ts +5 -17
  128. package/node_modules/pi-mcp-adapter/package.json +13 -3
  129. package/node_modules/pi-mcp-adapter/prompts.ts +35 -3
  130. package/node_modules/pi-mcp-adapter/proxy-modes.ts +108 -7
  131. package/node_modules/pi-mcp-adapter/runtime-owner.ts +16 -1
  132. package/node_modules/pi-mcp-adapter/sampling-handler.ts +9 -32
  133. package/node_modules/pi-mcp-adapter/sandbox-proxy-template.ts +20 -93
  134. package/node_modules/pi-mcp-adapter/server-manager.ts +227 -40
  135. package/node_modules/pi-mcp-adapter/session-approvals.ts +187 -0
  136. package/node_modules/pi-mcp-adapter/skills/mcp-scripting/SKILL.md +9 -1
  137. package/node_modules/pi-mcp-adapter/state.ts +10 -1
  138. package/node_modules/pi-mcp-adapter/tool-approval.ts +10 -22
  139. package/node_modules/pi-mcp-adapter/tool-metadata.ts +14 -0
  140. package/node_modules/pi-mcp-adapter/types.ts +57 -4
  141. package/node_modules/pi-mcp-adapter/ui-server.ts +36 -28
  142. package/node_modules/pi-mcp-adapter/utils.ts +64 -6
  143. package/package.json +15 -15
@@ -50,9 +50,9 @@ interface DiffTheme {
50
50
  }
51
51
 
52
52
  type DiffLineKind = "add" | "remove" | "context";
53
- type DiffEntryKind = "line" | "meta" | "hunk" | "file";
53
+ type DiffEntryKind = "line" | "omission" | "meta" | "hunk" | "file";
54
54
 
55
- interface DiffLineEntry {
55
+ export interface DiffLineEntry {
56
56
  kind: "line";
57
57
  lineKind: DiffLineKind;
58
58
  oldLineNumber: number | null;
@@ -64,15 +64,21 @@ interface DiffLineEntry {
64
64
  hunkIndex: number;
65
65
  }
66
66
 
67
+ export interface DiffOmissionEntry {
68
+ kind: "omission";
69
+ raw: string;
70
+ hunkIndex: number;
71
+ }
72
+
67
73
  interface DiffMetaEntry {
68
- kind: Exclude<DiffEntryKind, "line">;
74
+ kind: Exclude<DiffEntryKind, "line" | "omission">;
69
75
  raw: string;
70
76
  hunkIndex: number;
71
77
  }
72
78
 
73
- type ParsedDiffEntry = DiffLineEntry | DiffMetaEntry;
79
+ type ParsedDiffEntry = DiffLineEntry | DiffOmissionEntry | DiffMetaEntry;
74
80
 
75
- interface ParsedDiff {
81
+ export interface ParsedDiff {
76
82
  entries: ParsedDiffEntry[];
77
83
  stats: DiffStats;
78
84
  }
@@ -94,6 +100,7 @@ interface RenderedRow {
94
100
  interface SplitDiffRow {
95
101
  left?: DiffLineEntry;
96
102
  right?: DiffLineEntry;
103
+ omission?: DiffOmissionEntry;
97
104
  meta?: DiffMetaEntry;
98
105
  hunkIndex: number | null;
99
106
  }
@@ -131,7 +138,9 @@ type CodeLineHighlighter = (line: string, entry: DiffLineEntry) => string;
131
138
 
132
139
  const CANONICAL_LINE_PATTERN = /^([+\- ])(\s*\d+)\|(.*)$/;
133
140
  const HASHLINE_ANCHOR_LINE_PATTERN = /^([+\- ])(\s*\d+)#([A-Za-z0-9]+| {2}):(.*)$/;
141
+ // Pi 仍使用空格分隔的行号格式;仅在无 hunk 头时可安全识别。
134
142
  const LEGACY_LINE_PATTERN = /^([+\- ])(\s*\d+)\s(.*)$/;
143
+ const PI_OMISSION_LINE_PATTERN = /^ {3,}\.\.\.$/;
135
144
  const HUNK_HEADER_PATTERN = /^@@\s+-(\d+)(?:,(\d+))?\s+\+(\d+)(?:,(\d+))?\s+@@(.*)$/;
136
145
  const SPLIT_SEPARATOR = " │ ";
137
146
  const MIN_LINE_NUMBER_WIDTH = 2;
@@ -374,7 +383,10 @@ function toParsedDiffLine(
374
383
  return { lineKind: "context", lineNumber: normalizedLineNumber, content };
375
384
  }
376
385
 
377
- function parseCanonicalDiffLine(line: string): {
386
+ function parseCanonicalDiffLine(
387
+ line: string,
388
+ allowPiFormat: boolean,
389
+ ): {
378
390
  lineKind: DiffLineKind;
379
391
  lineNumber: string;
380
392
  content: string;
@@ -393,7 +405,7 @@ function parseCanonicalDiffLine(line: string): {
393
405
  }
394
406
 
395
407
  const canonicalMatch = line.match(CANONICAL_LINE_PATTERN);
396
- const legacyMatch = canonicalMatch ? null : line.match(LEGACY_LINE_PATTERN);
408
+ const legacyMatch = canonicalMatch || !allowPiFormat ? null : line.match(LEGACY_LINE_PATTERN);
397
409
  const matched = canonicalMatch ?? legacyMatch;
398
410
  if (!matched) {
399
411
  return null;
@@ -486,7 +498,7 @@ function ensureImplicitHunk(currentHunk: number): number {
486
498
  return currentHunk > 0 ? currentHunk : 1;
487
499
  }
488
500
 
489
- function parseDiff(diffText: string): ParsedDiff {
501
+ export function parseDiff(diffText: string): ParsedDiff {
490
502
  const stats: DiffStats = {
491
503
  added: 0,
492
504
  removed: 0,
@@ -505,12 +517,14 @@ function parseDiff(diffText: string): ParsedDiff {
505
517
  let oldLineCursor: number | null = null;
506
518
  let newLineCursor: number | null = null;
507
519
  let lineNumberDelta = 0;
520
+ let hasHunkHeader = false;
508
521
 
509
522
  for (const rawLine of diffText.replace(/\r/g, "").split("\n")) {
510
523
  stats.lines++;
511
524
 
512
525
  const hunkMatch = rawLine.match(HUNK_HEADER_PATTERN);
513
526
  if (hunkMatch) {
527
+ hasHunkHeader = true;
514
528
  hunkIndex++;
515
529
  stats.hunks = Math.max(stats.hunks, hunkIndex);
516
530
  oldLineCursor = toNumber(hunkMatch[1]);
@@ -535,7 +549,14 @@ function parseDiff(diffText: string): ParsedDiff {
535
549
  lineNumberDelta = 0;
536
550
  }
537
551
 
538
- const canonical = parseCanonicalDiffLine(rawLine);
552
+ // Pi 用空白行号填充省略上下文;它不是源码行,也不推进行号游标。
553
+ if (!hasHunkHeader && PI_OMISSION_LINE_PATTERN.test(rawLine)) {
554
+ entries.push({ kind: "omission", raw: rawLine, hunkIndex });
555
+ continue;
556
+ }
557
+
558
+ // Pi 无 hunk 头时的数字行号与 unified hunk 中的数字源码文本存在歧义。
559
+ const canonical = parseCanonicalDiffLine(rawLine, !hasHunkHeader);
539
560
  if (canonical) {
540
561
  hunkIndex = ensureImplicitHunk(hunkIndex);
541
562
  stats.hunks = Math.max(stats.hunks, hunkIndex);
@@ -802,6 +823,12 @@ function buildSplitRows(entries: ParsedDiffEntry[]): SplitDiffRow[] {
802
823
  break;
803
824
  }
804
825
 
826
+ if (entry.kind === "omission") {
827
+ rows.push({ omission: entry, hunkIndex: entry.hunkIndex || null });
828
+ index++;
829
+ continue;
830
+ }
831
+
805
832
  if (entry.kind !== "line") {
806
833
  rows.push({ meta: entry, hunkIndex: entry.hunkIndex || null });
807
834
  index++;
@@ -1785,8 +1812,44 @@ interface DiffRenderContext {
1785
1812
  showHashlineAnchors: boolean;
1786
1813
  }
1787
1814
 
1788
- function processDiffEntries(
1815
+ const OMISSION_GLYPH = "⋮";
1816
+
1817
+ function createOmissionRenderLine(entry: DiffOmissionEntry, compact = false): DiffLineEntry {
1818
+ return {
1819
+ kind: "line",
1820
+ lineKind: "context",
1821
+ oldLineNumber: null,
1822
+ newLineNumber: null,
1823
+ fallbackLineNumber: compact ? "" : OMISSION_GLYPH,
1824
+ content: compact ? OMISSION_GLYPH : "",
1825
+ raw: entry.raw,
1826
+ hunkIndex: entry.hunkIndex,
1827
+ };
1828
+ }
1829
+
1830
+ function materializeOmissionEntries(
1789
1831
  entries: ParsedDiffEntry[],
1832
+ compact = false,
1833
+ ): (DiffLineEntry | DiffMetaEntry)[] {
1834
+ return entries.map((entry) =>
1835
+ entry.kind === "omission" ? createOmissionRenderLine(entry, compact) : entry,
1836
+ );
1837
+ }
1838
+
1839
+ function omitTerminalOmissions(entries: ParsedDiffEntry[]): ParsedDiffEntry[] {
1840
+ let lastLineIndex = -1;
1841
+ for (let index = entries.length - 1; index >= 0; index--) {
1842
+ if (entries[index]?.kind === "line") {
1843
+ lastLineIndex = index;
1844
+ break;
1845
+ }
1846
+ }
1847
+
1848
+ return entries.filter((entry, index) => entry.kind !== "omission" || index < lastLineIndex);
1849
+ }
1850
+
1851
+ function processDiffEntries(
1852
+ entries: (DiffLineEntry | DiffMetaEntry)[],
1790
1853
  ctx: DiffRenderContext,
1791
1854
  processLine: (entry: DiffLineEntry) => string[],
1792
1855
  ): RenderedRow[] {
@@ -1807,7 +1870,7 @@ function renderUnified(
1807
1870
  ctx: DiffRenderContext,
1808
1871
  lineNumberWidth: number,
1809
1872
  ): RenderedRow[] {
1810
- return processDiffEntries(entries, ctx, (entry) => {
1873
+ return processDiffEntries(materializeOmissionEntries(entries), ctx, (entry) => {
1811
1874
  const lineNumber =
1812
1875
  entry.lineKind === "add"
1813
1876
  ? formatLineNumberLabel(
@@ -1857,6 +1920,10 @@ function toUnifiedFallbackRows(
1857
1920
  ): RenderedRow[] {
1858
1921
  const flattened: ParsedDiffEntry[] = [];
1859
1922
  for (const row of rows) {
1923
+ if (row.omission) {
1924
+ flattened.push(row.omission);
1925
+ continue;
1926
+ }
1860
1927
  if (row.meta) {
1861
1928
  flattened.push(row.meta);
1862
1929
  continue;
@@ -1872,7 +1939,7 @@ function toUnifiedFallbackRows(
1872
1939
  }
1873
1940
 
1874
1941
  function renderCompact(entries: ParsedDiffEntry[], ctx: DiffRenderContext): RenderedRow[] {
1875
- return processDiffEntries(entries, ctx, (entry) => {
1942
+ return processDiffEntries(materializeOmissionEntries(entries, true), ctx, (entry) => {
1876
1943
  const codeText = normalizeCodeWhitespace(
1877
1944
  getCompactLineRenderContent(entry, ctx.showHashlineAnchors),
1878
1945
  );
@@ -2091,8 +2158,9 @@ function renderSplit(
2091
2158
  continue;
2092
2159
  }
2093
2160
 
2161
+ const omissionLine = row.omission ? createOmissionRenderLine(row.omission) : undefined;
2094
2162
  const leftCells = renderSplitCell(
2095
- row.left,
2163
+ omissionLine ?? row.left,
2096
2164
  "left",
2097
2165
  leftWidth,
2098
2166
  splitLineNumberWidth,
@@ -2106,7 +2174,7 @@ function renderSplit(
2106
2174
  showHashlineAnchors,
2107
2175
  );
2108
2176
  const rightCells = renderSplitCell(
2109
- row.right,
2177
+ omissionLine ?? row.right,
2110
2178
  "right",
2111
2179
  rightWidth,
2112
2180
  splitLineNumberWidth,
@@ -2561,18 +2629,19 @@ export function renderEditDiffResult(
2561
2629
  return new Text(theme.fg("muted", "↳ no diff data"), 0, 0);
2562
2630
  }
2563
2631
 
2564
- const splitRows = buildSplitRows(parsed.entries);
2632
+ const renderEntries = omitTerminalOmissions(parsed.entries);
2633
+ const splitRows = buildSplitRows(renderEntries);
2565
2634
  const showHashlineAnchors =
2566
2635
  options.expanded === true &&
2567
- parsed.entries.some((entry) => entry.kind === "line" && !!entry.hashlineAnchorContent);
2568
- const lineNumberWidth = getLineNumberWidth(parsed.entries, showHashlineAnchors);
2636
+ renderEntries.some((entry) => entry.kind === "line" && !!entry.hashlineAnchorContent);
2637
+ const lineNumberWidth = getLineNumberWidth(renderEntries, showHashlineAnchors);
2569
2638
  const palette = resolveDiffPalette(theme);
2570
2639
  // Rich diffs use ccstyle's self shell. Keep the panel transparent so the
2571
2640
  // separator cannot leak toolSuccessBg across the entire new column.
2572
2641
  const containerBgAnsi = undefined;
2573
2642
  const language = resolveLanguageFromPath(options.filePath);
2574
2643
  const cache = createDiffRenderCache();
2575
- const highlightLine = createCodeLineHighlighter(language, theme, parsed.entries, () => {
2644
+ const highlightLine = createCodeLineHighlighter(language, theme, renderEntries, () => {
2576
2645
  cache.invalidate();
2577
2646
  options.invalidate?.();
2578
2647
  });
@@ -2621,7 +2690,7 @@ export function renderEditDiffResult(
2621
2690
  const processBudget = resolveDiffProcessBudget(displayLimit, wordWrap);
2622
2691
  // Only highlight/render a prefix that can fill the display limit; full-diff
2623
2692
  // LCS + syntax highlight on thousands of hidden lines is pure waste when collapsed.
2624
- const entryBudget = takeEntriesForLineBudget(parsed.entries, processBudget);
2693
+ const entryBudget = takeEntriesForLineBudget(renderEntries, processBudget);
2625
2694
  const splitBudget = takeSplitRowsForBudget(splitRows, processBudget);
2626
2695
  const inlineHighlights = buildInlineHighlightMap(splitBudget.rows);
2627
2696
  const renderCtx: DiffRenderContext = {
@@ -8,6 +8,7 @@ import {
8
8
  } from "@earendil-works/pi-tui";
9
9
  import { TOOL_LOADING_INTERVAL_MS, toolLoadingIcon } from "../../utils/tool-loading-icon.ts";
10
10
  import { sanitizeToolResultText } from "../../utils/tool-result-sanitize.ts";
11
+ import { fitToolCallSummary, pathSummary, type ToolCallSummary } from "./result.ts";
11
12
  import { isToolTuiFullscreen, showMoreHintText } from "../show-more-hint.ts";
12
13
 
13
14
  const PATCH_KEY = Symbol.for("pi.ccstyle.tool-grouping-patch");
@@ -172,7 +173,7 @@ function humanizeToolName(name: string): string {
172
173
  .replace(/\b\w/g, (char) => char.toUpperCase());
173
174
  }
174
175
 
175
- function toolSummary(tool: any): { main: string; detail: string } {
176
+ function toolSummary(tool: any): ToolCallSummary {
176
177
  const name = toolName(tool);
177
178
  const lowerName = name.toLowerCase();
178
179
  const args = tool?.args ?? {};
@@ -232,10 +233,11 @@ function toolSummary(tool: any): { main: string; detail: string } {
232
233
  args.offset !== undefined ? `offset=${args.offset}` : "",
233
234
  args.limit !== undefined ? `limit=${args.limit}` : "",
234
235
  ].filter(Boolean);
235
- return {
236
- main: `Read ${oneLine(args.path || "...")}`,
237
- detail: details.length ? ` (${details.join(", ")})` : "",
238
- };
236
+ const detail = details.length ? ` (${details.join(", ")})` : "";
237
+ if (typeof args.path === "string" && args.path) {
238
+ return pathSummary("Read", args.path, tool?.cwd, detail);
239
+ }
240
+ return { main: "Read ...", detail };
239
241
  }
240
242
  if (name === "bash") return { main: `Bash ${oneLine(args.command || "...")}`, detail: "" };
241
243
  if (name === "grep") {
@@ -456,11 +458,14 @@ export class ToolGroupComponent extends Container {
456
458
  const continuation = index === total - 1 ? " " : "│ ";
457
459
  if (!this._expanded) {
458
460
  const summary = toolSummary(tool);
461
+ const prefix = ` ${fg("dim", branch)} ${fg(color, statusIcon(toolStatus))} `;
462
+ const detail = fg("dim", summary.detail);
463
+ const mainWidth = Math.max(0, width - visibleWidth(prefix) - visibleWidth(detail));
459
464
  lines.push(
460
465
  truncateToWidth(
461
- ` ${fg("dim", branch)} ${fg(color, statusIcon(toolStatus))} ${fg("toolTitle", summary.main)}${fg("dim", summary.detail)}`,
466
+ `${prefix}${fg("toolTitle", fitToolCallSummary(summary, mainWidth))}${detail}`,
462
467
  width,
463
- "",
468
+ "",
464
469
  ),
465
470
  );
466
471
  continue;
@@ -1,4 +1,5 @@
1
1
  import { Text, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
2
+ import { posix, win32 } from "node:path";
2
3
  import { inspect } from "node:util";
3
4
  import { config } from "../../config/config.ts";
4
5
  import { showMoreHintText } from "../show-more-hint.ts";
@@ -557,6 +558,103 @@ export function middleTruncateToWidth(text: string, width: number): string {
557
558
  return `${left}…${right}`;
558
559
  }
559
560
 
561
+ export type ToolCallSummary = {
562
+ main: string;
563
+ detail: string;
564
+ /** 路径摘要保留结构,供最终渲染按实际宽度优先保留文件名。 */
565
+ path?: { prefix: string; value: string };
566
+ };
567
+
568
+ function pathApi(value: string) {
569
+ if (win32.isAbsolute(value)) return win32;
570
+ if (posix.isAbsolute(value)) return posix;
571
+ return undefined;
572
+ }
573
+
574
+ /** cwd 内绝对路径转相对路径;cwd 外路径保持不变。 */
575
+ export function displayPath(value: unknown, cwd?: string): string {
576
+ const text = oneLine(value, 4096);
577
+ const base = cwd ? oneLine(cwd, 4096) : "";
578
+ const api = pathApi(text);
579
+ if (!api || !base || !api.isAbsolute(base)) return text;
580
+ const relative = api.relative(base, text);
581
+ if (!relative) return api.basename(text) || ".";
582
+ if (relative === ".." || relative.startsWith(`..${api.sep}`) || api.isAbsolute(relative)) {
583
+ return text;
584
+ }
585
+ return relative;
586
+ }
587
+
588
+ function headToWidth(text: string, width: number, ellipsis = ""): string {
589
+ if (visibleWidth(text) <= width) return text;
590
+ if (width <= 0) return "";
591
+ const suffix = visibleWidth(ellipsis) <= width ? ellipsis : "";
592
+ const contentWidth = width - visibleWidth(suffix);
593
+ let head = "";
594
+ for (const char of Array.from(text)) {
595
+ if (visibleWidth(head + char) > contentWidth) break;
596
+ head += char;
597
+ }
598
+ return head + suffix;
599
+ }
600
+
601
+ function tailToWidth(text: string, width: number): string {
602
+ if (width <= 0) return "";
603
+ let tail = "";
604
+ for (const char of Array.from(text).reverse()) {
605
+ if (visibleWidth(char + tail) > width) break;
606
+ tail = char + tail;
607
+ }
608
+ return tail;
609
+ }
610
+
611
+ /** 中间截断路径:目录保留开头,末尾优先完整保留文件名。 */
612
+ export function truncatePathToWidth(path: string, width: number): string {
613
+ if (visibleWidth(path) <= width) return path;
614
+ if (width <= 1) return width === 1 ? "…" : "";
615
+ const separatorIndex = Math.max(path.lastIndexOf("/"), path.lastIndexOf("\\"));
616
+ const filename = separatorIndex >= 0 ? path.slice(separatorIndex + 1) : path;
617
+ const filenameWidth = visibleWidth(filename);
618
+ if (separatorIndex >= 0 && filenameWidth + 1 <= width) {
619
+ const separator = path[separatorIndex]!;
620
+ const suffix = `${separator}${filename}`;
621
+ const prefixWidth = width - visibleWidth(suffix) - 1;
622
+ const prefix = headToWidth(path.slice(0, separatorIndex), Math.max(0, prefixWidth));
623
+ return `${prefix}…${prefix ? suffix : filename}`;
624
+ }
625
+ const leftWidth = Math.max(1, Math.floor((width - 1) * 0.45));
626
+ const rightWidth = Math.max(0, width - leftWidth - 1);
627
+ return `${headToWidth(filename, leftWidth)}…${tailToWidth(filename, rightWidth)}`;
628
+ }
629
+
630
+ /** 路径展示的统一入口:相对化后按当前可用宽度中间截断。 */
631
+ export function formatDisplayPath(value: unknown, cwd: string | undefined, width: number): string {
632
+ return truncatePathToWidth(displayPath(value, cwd), Math.max(0, Math.floor(width)));
633
+ }
634
+
635
+ /** 按最终终端宽度渲染摘要;路径摘要不会再被整行头部截断。 */
636
+ export function fitToolCallSummary(summary: ToolCallSummary, width: number): string {
637
+ if (!summary.path) return headTruncateToWidth(summary.main, width);
638
+ const prefix = summary.path.prefix;
639
+ const pathWidth = Math.max(0, width - visibleWidth(prefix) - 1);
640
+ if (pathWidth <= 0) return headTruncateToWidth(prefix, width);
641
+ return `${prefix} ${truncatePathToWidth(summary.path.value, pathWidth)}`;
642
+ }
643
+
644
+ export function pathSummary(
645
+ prefix: string,
646
+ value: unknown,
647
+ cwd: string | undefined,
648
+ detail = "",
649
+ ): ToolCallSummary {
650
+ const path = displayPath(value, cwd);
651
+ return {
652
+ main: `${prefix} ${path}`,
653
+ detail,
654
+ path: { prefix, value: path },
655
+ };
656
+ }
657
+
560
658
  /** Pretty-print tool call args for the expanded Input section. */
561
659
  export function formatToolInputArgs(args: unknown, maxChars = 8_000): string {
562
660
  if (args === undefined || args === null) return "";
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@herbertgao/pi-cc-extensions",
3
- "version": "0.9.0",
3
+ "version": "0.9.2",
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.69",
72
+ "version": "0.8.71",
73
73
  "repository": "https://github.com/minuque/pi-cc-extensions",
74
- "commit": "5d7a2666f51da9f450bd019af10200c88cc1df98"
74
+ "commit": "e43e0041b59f5d7f03be9b9d103a5f9e954e4c11"
75
75
  }
76
76
  }
@@ -218,9 +218,8 @@ export function decideCompaction(input: {
218
218
  carriedDebtTokens: input.carriedDebtTokens,
219
219
  cacheDebtRepaymentTokens: input.cacheDebtRepaymentTokens,
220
220
  compact,
221
- reason: !compressible
222
- ? "non_positive_saving"
223
- : windowProtection
221
+ reason: compressible
222
+ ? windowProtection
224
223
  ? "window_protection"
225
224
  : economic
226
225
  ? "economic"
@@ -232,6 +231,7 @@ export function decideCompaction(input: {
232
231
  ? "deferred_subsequent_margin"
233
232
  : !firstCompaction && baseEconomic && !carriedDebtGateOpen
234
233
  ? "deferred_carried_debt"
235
- : "deferred_economic",
234
+ : "deferred_economic"
235
+ : "non_positive_saving",
236
236
  };
237
237
  }
@@ -2,12 +2,13 @@
2
2
  * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
3
3
  * SPDX-License-Identifier: MIT
4
4
  */
5
- import type { AgentMessage, AgentToolResult } from "@earendil-works/pi-agent-core";
5
+ import type { AgentMessage, AgentToolResult, ThinkingLevel } from "@earendil-works/pi-agent-core";
6
6
  import {
7
7
  buildSessionContext,
8
8
  estimateTokens,
9
9
  findCutPoint,
10
10
  sessionEntryToContextMessages,
11
+ type ExtensionAPI,
11
12
  type ExtensionContext,
12
13
  type ExtensionFactory,
13
14
  type SessionEntry,
@@ -165,6 +166,37 @@ function validPositiveInteger(value: unknown): value is number {
165
166
  return typeof value === "number" && Number.isSafeInteger(value) && value > 0;
166
167
  }
167
168
 
169
+ type CompactionThinkingOverride = {
170
+ readonly previous: ThinkingLevel;
171
+ readonly applied: ThinkingLevel;
172
+ };
173
+
174
+ // Native summaries share their output cap with reasoning; reserve the cap for summary text.
175
+ function lowerCompactionThinking(
176
+ pi: ExtensionAPI,
177
+ reasoning: boolean,
178
+ ): CompactionThinkingOverride | undefined {
179
+ if (!reasoning) return;
180
+ try {
181
+ const previous = pi.getThinkingLevel();
182
+ if (previous !== "high" && previous !== "xhigh" && previous !== "max") return;
183
+ pi.setThinkingLevel("medium");
184
+ const applied = pi.getThinkingLevel();
185
+ return applied === previous ? undefined : { previous, applied };
186
+ } catch {
187
+ return;
188
+ }
189
+ }
190
+
191
+ function restoreCompactionThinking(pi: ExtensionAPI, override: CompactionThinkingOverride | undefined): void {
192
+ if (!override) return;
193
+ try {
194
+ if (pi.getThinkingLevel() === override.applied) pi.setThinkingLevel(override.previous);
195
+ } catch {
196
+ // A compaction result must not be masked by a best-effort state restore.
197
+ }
198
+ }
199
+
168
200
  function releaseParentContinuation(continuation: PendingContinuation | undefined): void {
169
201
  if (continuation) setTimeout(continuation.resolve, 0);
170
202
  }
@@ -182,6 +214,7 @@ export function createOnlineContextCompactExtension(options: OnlineContextCompac
182
214
  let activeDebt: CacheDebt | undefined;
183
215
  let nextContinuation: PendingContinuation | undefined;
184
216
  let compactionInFlight = false;
217
+ let compactionThinkingOverride: CompactionThinkingOverride | undefined;
185
218
 
186
219
  const releaseContinuation = (): void => {
187
220
  const continuation = nextContinuation;
@@ -349,6 +382,7 @@ export function createOnlineContextCompactExtension(options: OnlineContextCompac
349
382
  let compactionError: Error | undefined;
350
383
  try {
351
384
  compactionInFlight = true;
385
+ compactionThinkingOverride = lowerCompactionThinking(pi, context.model?.reasoning === true);
352
386
  await new Promise<void>((resolve) => {
353
387
  let finished = false;
354
388
  const finish = (): void => {
@@ -382,6 +416,8 @@ export function createOnlineContextCompactExtension(options: OnlineContextCompac
382
416
  },
383
417
  });
384
418
  });
419
+ restoreCompactionThinking(pi, compactionThinkingOverride);
420
+ compactionThinkingOverride = undefined;
385
421
  compactionInFlight = false;
386
422
  if (
387
423
  compactionError &&
@@ -422,6 +458,8 @@ export function createOnlineContextCompactExtension(options: OnlineContextCompac
422
458
  await continuation.promise;
423
459
  }
424
460
  } finally {
461
+ restoreCompactionThinking(pi, compactionThinkingOverride);
462
+ compactionThinkingOverride = undefined;
425
463
  compactionInFlight = false;
426
464
  activeDebt = undefined;
427
465
  releaseParentContinuation(parentContinuation);
@@ -445,6 +483,8 @@ export function createOnlineContextCompactExtension(options: OnlineContextCompac
445
483
  });
446
484
 
447
485
  pi.on("session_shutdown", () => {
486
+ restoreCompactionThinking(pi, compactionThinkingOverride);
487
+ compactionThinkingOverride = undefined;
448
488
  releaseContinuation();
449
489
  pendingBoundary = undefined;
450
490
  selected = undefined;
@@ -18,6 +18,7 @@ import {
18
18
  import { type DialogUI, hasDialogUI, runRpcQuestionnaire } from "./rpc-fallback.js";
19
19
  import { displayLabel, t } from "./state/i18n-bridge.js";
20
20
  import { sentinelsToAppend } from "./state/row-intent.js";
21
+ import { normalizeQuestionParams } from "./tool/normalize-params.js";
21
22
  import { buildQuestionnaireResponse, buildToolResult } from "./tool/response-envelope.js";
22
23
  import {
23
24
  MAX_OPTIONS,
@@ -310,7 +311,10 @@ export function registerAskUserQuestionTool(pi: ExtensionAPI): void {
310
311
  parameters: QuestionParamsSchema,
311
312
 
312
313
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
313
- const typed = params as unknown as QuestionParams;
314
+ // Line-terminator normalization runs once here, ahead of validation, so
315
+ // every downstream consumer — validator, TUI, RPC walker, envelope, prompt
316
+ // event — sees the same clean text (#192).
317
+ const typed = normalizeQuestionParams(params as unknown as QuestionParams);
314
318
  if (!ctx.hasUI) return rejectWithoutUi();
315
319
 
316
320
  const validation = validateQuestionnaire(typed);
@@ -38,7 +38,11 @@ export interface AskUserBlockedEventPayload {
38
38
  }
39
39
 
40
40
  export interface AskUserPromptQuestion {
41
- /** The full question text, exactly as the agent authored it. */
41
+ /**
42
+ * The full question text as the agent authored it, with line terminators
43
+ * normalized at tool entry (`\r\n` → `\n`, lone `\r` removed — #192). The
44
+ * same normalization applies to `header` and every option field below.
45
+ */
42
46
  question: string;
43
47
  /** The short chip/tag shown next to the question. */
44
48
  header: string;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juicesharp/rpiv-ask-user-question",
3
- "version": "2.9.0",
3
+ "version": "2.10.1",
4
4
  "description": "Pi extension. A structured questionnaire the model can put to you when it would otherwise guess, with typed options instead of free-form replies.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -56,6 +56,7 @@
56
56
  "state/state-reducer.ts",
57
57
  "state/state.ts",
58
58
  "tool/format-answer.ts",
59
+ "tool/normalize-params.ts",
59
60
  "tool/response-envelope.ts",
60
61
  "tool/types.ts",
61
62
  "tool/validate-questionnaire.ts",
@@ -94,7 +95,7 @@
94
95
  }
95
96
  },
96
97
  "dependencies": {
97
- "@juicesharp/rpiv-config": "^2.9.0",
98
+ "@juicesharp/rpiv-config": "^2.10.1",
98
99
  "typebox": "^1.1.24"
99
100
  }
100
101
  }
@@ -0,0 +1,56 @@
1
+ import type { QuestionParams } from "./types.js";
2
+
3
+ /**
4
+ * Normalize line terminators in one model-supplied text field (#192).
5
+ *
6
+ * Some models serialize a bare carriage return inside tool-call string
7
+ * arguments at token boundaries where the text was meant to be contiguous
8
+ * (`GEMBA\r_LOG\r_FILE` for `GEMBA_LOG_FILE`). A raw CR is a cursor-control
9
+ * byte, not text: pi-tui ≤0.80 writes it straight into the row, so the
10
+ * terminal returns to column 0 and later text overwrites the pointer and
11
+ * number; pi-tui ≥0.84 splits `wrapTextWithAnsi` on `\r`, so one option
12
+ * fragments into stacked rows. Both symptoms have the same fix at our
13
+ * boundary:
14
+ *
15
+ * - `\r\n` → `\n` keeps genuine multi-line content (preview markdown) intact.
16
+ * - a lone `\r` is deleted — never a space (phantom gaps inside words) and
17
+ * never `\n` (reintroduces the vertical fragmentation). This matches
18
+ * pi-coding-agent's own display normalization (`normalizeDisplayText`).
19
+ */
20
+ export function normalizeLineTerminators(text: string): string {
21
+ return text.replace(/\r\n/g, "\n").replace(/\r/g, "");
22
+ }
23
+
24
+ /**
25
+ * Copy `obj`, normalizing the listed keys that hold strings. Keys absent from
26
+ * `obj` (e.g. an omitted `preview`) stay absent so `"preview" in option`
27
+ * checks and `hasPreview` derivations are unchanged.
28
+ */
29
+ function normalizeStringFields<T extends object>(obj: T, keys: readonly (keyof T)[]): T {
30
+ const out = { ...obj };
31
+ for (const key of keys) {
32
+ const value = obj[key];
33
+ if (typeof value === "string") out[key] = normalizeLineTerminators(value) as T[typeof key];
34
+ }
35
+ return out;
36
+ }
37
+
38
+ /**
39
+ * Return a copy of the tool params with every user-facing string field
40
+ * (`question`, `header`, `options[].label`, `options[].description`,
41
+ * `options[].preview`) line-terminator-normalized. Runs once at tool entry,
42
+ * BEFORE `validateQuestionnaire`, so the reserved-label and duplicate-label
43
+ * guards compare the text the user will actually see (`"Other\r"` must not
44
+ * slip past `reserved_label`), and so the TUI, the RPC dialog walker, the
45
+ * envelope echo, and the `rpiv:ask-user:prompt` payload all carry the same
46
+ * clean text. Pure: the input object is never mutated.
47
+ */
48
+ export function normalizeQuestionParams(params: QuestionParams): QuestionParams {
49
+ return {
50
+ ...params,
51
+ questions: params.questions.map((q) => ({
52
+ ...normalizeStringFields(q, ["question", "header"]),
53
+ options: q.options.map((o) => normalizeStringFields(o, ["label", "description", "preview"])),
54
+ })),
55
+ };
56
+ }
@@ -13,7 +13,7 @@ export interface OptionListViewConfig {
13
13
  }
14
14
 
15
15
  /**
16
- * Per-tick projection of OptionListView state. After Phase 11b, `inputBuffer`
16
+ * Per-tick projection of OptionListView state. `inputBuffer`
17
17
  * is part of the props bag — the session-owned `inlineInput` (a headless
18
18
  * `pi-tui` Editor instance) supplies its current `getText()` here per tick.
19
19
  * `OptionListView` is purely props-driven; the imperative buffer surface and