@herbertgao/pi-extensions 2026.9.4 → 2026.9.6

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 (146) hide show
  1. package/README.md +3 -15
  2. package/node_modules/@dietrichgebert/ponytail/.qoder-plugin/plugin.json +1 -1
  3. package/node_modules/@dietrichgebert/ponytail/README.es.md +33 -2
  4. package/node_modules/@dietrichgebert/ponytail/README.ko.md +33 -2
  5. package/node_modules/@dietrichgebert/ponytail/README.md +49 -5
  6. package/node_modules/@dietrichgebert/ponytail/assets/retriever-icon.png +0 -0
  7. package/node_modules/@dietrichgebert/ponytail/assets/retriever-logo-dark.svg +4 -0
  8. package/node_modules/@dietrichgebert/ponytail/assets/retriever-logo-light.svg +4 -0
  9. package/node_modules/@dietrichgebert/ponytail/hooks/claude-codex-hooks.json +0 -3
  10. package/node_modules/@dietrichgebert/ponytail/hooks/cursor-hooks.json +17 -0
  11. package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-activate.js +22 -3
  12. package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-mode-tracker.js +27 -2
  13. package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-runtime.js +61 -2
  14. package/node_modules/@dietrichgebert/ponytail/package.json +2 -1
  15. package/node_modules/@dietrichgebert/ponytail/scripts/cursor-hooks.js +135 -0
  16. package/node_modules/@dietrichgebert/ponytail/scripts/uninstall.js +21 -4
  17. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/reference/index.ts +33 -15
  18. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/interaction.ts +6 -2
  19. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/scroll.ts +23 -1
  20. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-renderer.ts +69 -14
  21. package/node_modules/@herbertgao/pi-cc-extensions/package.json +3 -3
  22. package/node_modules/@herbertgao/sol-pi/docs/compatibility.md +1 -1
  23. package/node_modules/@herbertgao/sol-pi/package.json +1 -1
  24. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/action-fusion/index.ts +0 -5
  25. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/economics.ts +4 -4
  26. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/extension.ts +41 -1
  27. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/state.ts +3 -1
  28. package/node_modules/@juicesharp/rpiv-ask-user-question/ask-user-question.ts +5 -1
  29. package/node_modules/@juicesharp/rpiv-ask-user-question/events.ts +5 -1
  30. package/node_modules/@juicesharp/rpiv-ask-user-question/package.json +3 -2
  31. package/node_modules/@juicesharp/rpiv-ask-user-question/tool/normalize-params.ts +56 -0
  32. package/node_modules/@juicesharp/rpiv-ask-user-question/view/components/option-list-view.ts +1 -1
  33. package/node_modules/@juicesharp/rpiv-ask-user-question/view/components/tab-bar.ts +1 -2
  34. package/node_modules/@pi-plugins/fast-mode/README.md +31 -44
  35. package/node_modules/@pi-plugins/fast-mode/dist/index.mjs +20914 -30
  36. package/node_modules/@pi-plugins/fast-mode/dist/index.mjs.map +1 -1
  37. package/node_modules/@pi-plugins/fast-mode/package.json +5 -7
  38. package/node_modules/@tifan/pi-copy-response/README.md +2 -2
  39. package/node_modules/@tifan/pi-copy-response/package.json +3 -2
  40. package/node_modules/@tifan/pi-inline-skills/README.md +2 -2
  41. package/node_modules/@tifan/pi-inline-skills/package.json +3 -2
  42. package/node_modules/@tifan/pi-mermaid-open/README.md +1 -1
  43. package/node_modules/@tifan/pi-mermaid-open/package.json +3 -2
  44. package/node_modules/@tifan/pi-preferred-thinking/package.json +3 -2
  45. package/node_modules/@tifan/pi-recap/package.json +3 -2
  46. package/node_modules/pi-mcp-adapter/CHANGELOG.md +78 -0
  47. package/node_modules/pi-mcp-adapter/README.md +125 -19
  48. package/node_modules/pi-mcp-adapter/agent-plugin-loader.ts +69 -30
  49. package/node_modules/pi-mcp-adapter/agent-plugin-provenance.ts +36 -0
  50. package/node_modules/pi-mcp-adapter/claude-plugin-loader.ts +365 -0
  51. package/node_modules/pi-mcp-adapter/commands.ts +13 -15
  52. package/node_modules/pi-mcp-adapter/config.ts +198 -30
  53. package/node_modules/pi-mcp-adapter/consent-manager.ts +40 -9
  54. package/node_modules/pi-mcp-adapter/direct-tool-surface.ts +250 -0
  55. package/node_modules/pi-mcp-adapter/direct-tools.ts +9 -228
  56. package/node_modules/pi-mcp-adapter/dist/agent-plugin-loader.js +74 -32
  57. package/node_modules/pi-mcp-adapter/dist/agent-plugin-loader.js.map +1 -1
  58. package/node_modules/pi-mcp-adapter/dist/agent-plugin-provenance.d.ts +1 -0
  59. package/node_modules/pi-mcp-adapter/dist/agent-plugin-provenance.js +31 -0
  60. package/node_modules/pi-mcp-adapter/dist/agent-plugin-provenance.js.map +1 -0
  61. package/node_modules/pi-mcp-adapter/dist/claude-plugin-loader.d.ts +9 -0
  62. package/node_modules/pi-mcp-adapter/dist/claude-plugin-loader.js +362 -0
  63. package/node_modules/pi-mcp-adapter/dist/claude-plugin-loader.js.map +1 -0
  64. package/node_modules/pi-mcp-adapter/dist/config.d.ts +3 -1
  65. package/node_modules/pi-mcp-adapter/dist/config.js +189 -27
  66. package/node_modules/pi-mcp-adapter/dist/config.js.map +1 -1
  67. package/node_modules/pi-mcp-adapter/dist/metadata-cache.js +7 -16
  68. package/node_modules/pi-mcp-adapter/dist/metadata-cache.js.map +1 -1
  69. package/node_modules/pi-mcp-adapter/dist/package-mcp-loader.js +5 -17
  70. package/node_modules/pi-mcp-adapter/dist/package-mcp-loader.js.map +1 -1
  71. package/node_modules/pi-mcp-adapter/dist/types.d.ts +30 -4
  72. package/node_modules/pi-mcp-adapter/dist/types.js +26 -0
  73. package/node_modules/pi-mcp-adapter/dist/types.js.map +1 -1
  74. package/node_modules/pi-mcp-adapter/dist/utils.d.ts +6 -0
  75. package/node_modules/pi-mcp-adapter/dist/utils.js +65 -6
  76. package/node_modules/pi-mcp-adapter/dist/utils.js.map +1 -1
  77. package/node_modules/pi-mcp-adapter/host-html-template.ts +20 -19
  78. package/node_modules/pi-mcp-adapter/http-ca.ts +84 -0
  79. package/node_modules/pi-mcp-adapter/index.ts +827 -164
  80. package/node_modules/pi-mcp-adapter/init.ts +48 -10
  81. package/node_modules/pi-mcp-adapter/lazy-loader.ts +7 -0
  82. package/node_modules/pi-mcp-adapter/mcp-auth-fetch.ts +113 -0
  83. package/node_modules/pi-mcp-adapter/mcp-auth-flow.ts +256 -77
  84. package/node_modules/pi-mcp-adapter/mcp-auth.ts +316 -54
  85. package/node_modules/pi-mcp-adapter/mcp-code.ts +45 -18
  86. package/node_modules/pi-mcp-adapter/mcp-install.ts +60 -0
  87. package/node_modules/pi-mcp-adapter/mcp-oauth-provider.ts +70 -6
  88. package/node_modules/pi-mcp-adapter/mcp-output-guard.ts +53 -42
  89. package/node_modules/pi-mcp-adapter/mcp-panel-theme.ts +104 -0
  90. package/node_modules/pi-mcp-adapter/mcp-panel.ts +366 -332
  91. package/node_modules/pi-mcp-adapter/mcp-references.ts +9 -14
  92. package/node_modules/pi-mcp-adapter/mcp-script-worker.mjs +3 -1
  93. package/node_modules/pi-mcp-adapter/mcp-setup-panel.ts +383 -341
  94. package/node_modules/pi-mcp-adapter/mcp-status.ts +5 -0
  95. package/node_modules/pi-mcp-adapter/metadata-cache.ts +7 -16
  96. package/node_modules/pi-mcp-adapter/namespace-tools.ts +61 -3
  97. package/node_modules/pi-mcp-adapter/oauth.ts +6 -2
  98. package/node_modules/pi-mcp-adapter/package-mcp-loader.ts +5 -17
  99. package/node_modules/pi-mcp-adapter/package.json +13 -3
  100. package/node_modules/pi-mcp-adapter/prompts.ts +35 -3
  101. package/node_modules/pi-mcp-adapter/proxy-modes.ts +108 -7
  102. package/node_modules/pi-mcp-adapter/runtime-owner.ts +16 -1
  103. package/node_modules/pi-mcp-adapter/sampling-handler.ts +9 -32
  104. package/node_modules/pi-mcp-adapter/sandbox-proxy-template.ts +20 -93
  105. package/node_modules/pi-mcp-adapter/server-manager.ts +227 -40
  106. package/node_modules/pi-mcp-adapter/session-approvals.ts +187 -0
  107. package/node_modules/pi-mcp-adapter/skills/mcp-scripting/SKILL.md +9 -1
  108. package/node_modules/pi-mcp-adapter/state.ts +10 -1
  109. package/node_modules/pi-mcp-adapter/tool-approval.ts +10 -22
  110. package/node_modules/pi-mcp-adapter/tool-metadata.ts +14 -0
  111. package/node_modules/pi-mcp-adapter/types.ts +57 -4
  112. package/node_modules/pi-mcp-adapter/ui-server.ts +36 -28
  113. package/node_modules/pi-mcp-adapter/utils.ts +64 -6
  114. package/package.json +13 -21
  115. package/examples/pi-footer.json +0 -275
  116. package/node_modules/@czottmann/pi-automode/CHANGELOG.md +0 -70
  117. package/node_modules/@czottmann/pi-automode/LICENSE.md +0 -21
  118. package/node_modules/@czottmann/pi-automode/README.md +0 -215
  119. package/node_modules/@czottmann/pi-automode/docs/GLOSSARY.md +0 -39
  120. package/node_modules/@czottmann/pi-automode/docs/adr/ADR-001-permission-precedence-and-trust-boundaries.md +0 -46
  121. package/node_modules/@czottmann/pi-automode/docs/adr/ADR-002-global-config-in-extension-data-directory.md +0 -60
  122. package/node_modules/@czottmann/pi-automode/docs/adr/INDEX.md +0 -6
  123. package/node_modules/@czottmann/pi-automode/docs/automode-classifier-flow.md +0 -428
  124. package/node_modules/@czottmann/pi-automode/docs/configuration.md +0 -171
  125. package/node_modules/@czottmann/pi-automode/docs/defaults.md +0 -178
  126. package/node_modules/@czottmann/pi-automode/docs/diagnostics.md +0 -90
  127. package/node_modules/@czottmann/pi-automode/docs/observability-logging.md +0 -160
  128. package/node_modules/@czottmann/pi-automode/examples/automode.local.json +0 -44
  129. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/bash.ts +0 -692
  130. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/classifier.ts +0 -852
  131. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/config.ts +0 -904
  132. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/constants.ts +0 -228
  133. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/extension.ts +0 -954
  134. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/hard-deny.ts +0 -429
  135. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/log.ts +0 -173
  136. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/model-selector.ts +0 -113
  137. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/model.ts +0 -13
  138. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/paths.ts +0 -297
  139. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/permissions.ts +0 -623
  140. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/state.ts +0 -102
  141. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/transcript.ts +0 -236
  142. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/types.ts +0 -205
  143. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/utils.ts +0 -54
  144. package/node_modules/@czottmann/pi-automode/extensions/auto-mode.ts +0 -26
  145. package/node_modules/@czottmann/pi-automode/package.json +0 -61
  146. package/node_modules/@czottmann/pi-automode/skills/automode-diagnostics/SKILL.md +0 -63
@@ -50,7 +50,7 @@ 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
55
  export interface DiffLineEntry {
56
56
  kind: "line";
@@ -64,13 +64,19 @@ export 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
81
  export interface ParsedDiff {
76
82
  entries: ParsedDiffEntry[];
@@ -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
  }
@@ -544,7 +551,7 @@ export function parseDiff(diffText: string): ParsedDiff {
544
551
 
545
552
  // Pi 用空白行号填充省略上下文;它不是源码行,也不推进行号游标。
546
553
  if (!hasHunkHeader && PI_OMISSION_LINE_PATTERN.test(rawLine)) {
547
- entries.push(createMetaEntry(rawLine, hunkIndex));
554
+ entries.push({ kind: "omission", raw: rawLine, hunkIndex });
548
555
  continue;
549
556
  }
550
557
 
@@ -816,6 +823,12 @@ function buildSplitRows(entries: ParsedDiffEntry[]): SplitDiffRow[] {
816
823
  break;
817
824
  }
818
825
 
826
+ if (entry.kind === "omission") {
827
+ rows.push({ omission: entry, hunkIndex: entry.hunkIndex || null });
828
+ index++;
829
+ continue;
830
+ }
831
+
819
832
  if (entry.kind !== "line") {
820
833
  rows.push({ meta: entry, hunkIndex: entry.hunkIndex || null });
821
834
  index++;
@@ -1799,8 +1812,44 @@ interface DiffRenderContext {
1799
1812
  showHashlineAnchors: boolean;
1800
1813
  }
1801
1814
 
1802
- 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(
1803
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)[],
1804
1853
  ctx: DiffRenderContext,
1805
1854
  processLine: (entry: DiffLineEntry) => string[],
1806
1855
  ): RenderedRow[] {
@@ -1821,7 +1870,7 @@ function renderUnified(
1821
1870
  ctx: DiffRenderContext,
1822
1871
  lineNumberWidth: number,
1823
1872
  ): RenderedRow[] {
1824
- return processDiffEntries(entries, ctx, (entry) => {
1873
+ return processDiffEntries(materializeOmissionEntries(entries), ctx, (entry) => {
1825
1874
  const lineNumber =
1826
1875
  entry.lineKind === "add"
1827
1876
  ? formatLineNumberLabel(
@@ -1871,6 +1920,10 @@ function toUnifiedFallbackRows(
1871
1920
  ): RenderedRow[] {
1872
1921
  const flattened: ParsedDiffEntry[] = [];
1873
1922
  for (const row of rows) {
1923
+ if (row.omission) {
1924
+ flattened.push(row.omission);
1925
+ continue;
1926
+ }
1874
1927
  if (row.meta) {
1875
1928
  flattened.push(row.meta);
1876
1929
  continue;
@@ -1886,7 +1939,7 @@ function toUnifiedFallbackRows(
1886
1939
  }
1887
1940
 
1888
1941
  function renderCompact(entries: ParsedDiffEntry[], ctx: DiffRenderContext): RenderedRow[] {
1889
- return processDiffEntries(entries, ctx, (entry) => {
1942
+ return processDiffEntries(materializeOmissionEntries(entries, true), ctx, (entry) => {
1890
1943
  const codeText = normalizeCodeWhitespace(
1891
1944
  getCompactLineRenderContent(entry, ctx.showHashlineAnchors),
1892
1945
  );
@@ -2105,8 +2158,9 @@ function renderSplit(
2105
2158
  continue;
2106
2159
  }
2107
2160
 
2161
+ const omissionLine = row.omission ? createOmissionRenderLine(row.omission) : undefined;
2108
2162
  const leftCells = renderSplitCell(
2109
- row.left,
2163
+ omissionLine ?? row.left,
2110
2164
  "left",
2111
2165
  leftWidth,
2112
2166
  splitLineNumberWidth,
@@ -2120,7 +2174,7 @@ function renderSplit(
2120
2174
  showHashlineAnchors,
2121
2175
  );
2122
2176
  const rightCells = renderSplitCell(
2123
- row.right,
2177
+ omissionLine ?? row.right,
2124
2178
  "right",
2125
2179
  rightWidth,
2126
2180
  splitLineNumberWidth,
@@ -2575,18 +2629,19 @@ export function renderEditDiffResult(
2575
2629
  return new Text(theme.fg("muted", "↳ no diff data"), 0, 0);
2576
2630
  }
2577
2631
 
2578
- const splitRows = buildSplitRows(parsed.entries);
2632
+ const renderEntries = omitTerminalOmissions(parsed.entries);
2633
+ const splitRows = buildSplitRows(renderEntries);
2579
2634
  const showHashlineAnchors =
2580
2635
  options.expanded === true &&
2581
- parsed.entries.some((entry) => entry.kind === "line" && !!entry.hashlineAnchorContent);
2582
- const lineNumberWidth = getLineNumberWidth(parsed.entries, showHashlineAnchors);
2636
+ renderEntries.some((entry) => entry.kind === "line" && !!entry.hashlineAnchorContent);
2637
+ const lineNumberWidth = getLineNumberWidth(renderEntries, showHashlineAnchors);
2583
2638
  const palette = resolveDiffPalette(theme);
2584
2639
  // Rich diffs use ccstyle's self shell. Keep the panel transparent so the
2585
2640
  // separator cannot leak toolSuccessBg across the entire new column.
2586
2641
  const containerBgAnsi = undefined;
2587
2642
  const language = resolveLanguageFromPath(options.filePath);
2588
2643
  const cache = createDiffRenderCache();
2589
- const highlightLine = createCodeLineHighlighter(language, theme, parsed.entries, () => {
2644
+ const highlightLine = createCodeLineHighlighter(language, theme, renderEntries, () => {
2590
2645
  cache.invalidate();
2591
2646
  options.invalidate?.();
2592
2647
  });
@@ -2635,7 +2690,7 @@ export function renderEditDiffResult(
2635
2690
  const processBudget = resolveDiffProcessBudget(displayLimit, wordWrap);
2636
2691
  // Only highlight/render a prefix that can fill the display limit; full-diff
2637
2692
  // LCS + syntax highlight on thousands of hidden lines is pure waste when collapsed.
2638
- const entryBudget = takeEntriesForLineBudget(parsed.entries, processBudget);
2693
+ const entryBudget = takeEntriesForLineBudget(renderEntries, processBudget);
2639
2694
  const splitBudget = takeSplitRowsForBudget(splitRows, processBudget);
2640
2695
  const inlineHighlights = buildInlineHighlightMap(splitBudget.rows);
2641
2696
  const renderCtx: DiffRenderContext = {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@herbertgao/pi-cc-extensions",
3
- "version": "0.9.1",
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.70",
72
+ "version": "0.8.71",
73
73
  "repository": "https://github.com/minuque/pi-cc-extensions",
74
- "commit": "9f200064951ffb148ecec2e1cf0dab7a2940f823"
74
+ "commit": "e43e0041b59f5d7f03be9b9d103a5f9e954e4c11"
75
75
  }
76
76
  }
@@ -21,7 +21,7 @@ Action Fusion decodes `file://` targets with Node's `fileURLToPath()` before res
21
21
 
22
22
  The queue covers only fused operations registered by this SoL-Pi instance. External processes, direct built-in-tool calls outside the replacement, and unrelated extensions are not globally locked. SoL-Pi hashes the target immediately before launching `then_run` and skips the command if it observes an intervening content change.
23
23
 
24
- When another extension already owns `write`, Action Fusion leaves that tool in place and only registers its `edit` replacement. When `pi-automode` exposes `automode_inspect`, Action Fusion registers neither mutation replacement: nested `then_run` cannot re-enter Pi's public `tool_call` permission pipeline, so disabling the fusion is the safe fallback.
24
+ When another extension already owns `write`, Action Fusion leaves that tool in place and only registers its `edit` replacement.
25
25
 
26
26
  ## ObservationPack
27
27
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@herbertgao/sol-pi",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Token-efficient context and tool extensions for Pi, maintained from NVlabs/SoL-Pi.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -80,11 +80,6 @@ export function createActionFusionExtension(options: ActionFusionOptions = {}):
80
80
  const baseWrite = memoizeByCwd((cwd: string) => createWriteToolDefinition(cwd, options.writeOptions));
81
81
 
82
82
  return (pi: ExtensionAPI) => {
83
- // then_run executes Bash inside the mutation tool, so it cannot pass through
84
- // Pi's public tool_call permission handlers. Leave it to automode instead of
85
- // creating a nested shell escape hatch.
86
- if (hasExternalToolOwner(pi, "automode_inspect")) return;
87
-
88
83
  const editTemplate = baseEdit(process.cwd());
89
84
  const writeTemplate = baseWrite(process.cwd());
90
85
 
@@ -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;
@@ -180,7 +180,9 @@ export function recordCompaction(
180
180
  return {
181
181
  ...state,
182
182
  epoch: state.epoch + 1,
183
- plan: [],
183
+ // Keep the plan so the post-compaction rebuild does not reclassify
184
+ // already-completed steps as a new boundary.
185
+ plan: [...state.plan],
184
186
  pendingProgress: [],
185
187
  lastContextTokens: null,
186
188
  positiveContextDeltaTotal: 0,
@@ -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
@@ -6,8 +6,7 @@ import type { StatefulView } from "../stateful-view.js";
6
6
  * Per-tick projection of TabBar state. The selector
7
7
  * (`selectTabBarProps`) hoists every render-time derivation
8
8
  * (`allAnswered`, `answered`, `isActive`, `submitActive`) into props so
9
- * `render()` is pure styling. Replaces the prior `setConfig(TabBarConfig)`
10
- * snowflake and the inline `+ 1` magic at `props-adapter.ts:127`.
9
+ * `render()` is pure styling.
11
10
  */
12
11
  export interface TabBarProps {
13
12
  /** One per author-defined question, in order. */
@@ -1,12 +1,7 @@
1
1
  # `@pi-plugins/fast-mode`
2
2
 
3
- A [pi-agent](https://github.com/earendil-works/pi) extension that requests a
4
- provider's fast (priority) service tier for configured models — faster inference at a
5
- higher token cost.
6
-
7
- Fast mode is a per-model opt-in: you decide which `provider/model-id` pairs it
8
- applies to, and a single session toggle turns it on and off. Requests for any other
9
- model are left untouched.
3
+ Request priority inference for selected models in
4
+ [pi](https://github.com/earendil-works/pi) with a session toggle.
10
5
 
11
6
  ## Install
12
7
 
@@ -14,49 +9,45 @@ model are left untouched.
14
9
  pi install npm:@pi-plugins/fast-mode
15
10
  ```
16
11
 
17
- For a one-off run without adding it to settings:
12
+ To try it for one session without changing your settings:
18
13
 
19
14
  ```bash
20
15
  pi -e npm:@pi-plugins/fast-mode
21
16
  ```
22
17
 
23
- For local development, load it straight from this directory:
24
-
25
- ```bash
26
- pi -e ./plugins/fast-mode
27
- ```
28
-
29
- ## Supported providers
30
-
31
- | Provider | Mechanism |
32
- | -------------- | ---------------------------------- |
33
- | `openai` | `service_tier: "priority"` payload |
34
- | `openai-codex` | `service_tier: "priority"` payload |
35
-
36
18
  ## Usage
37
19
 
38
- ```text
39
- /fast # toggle fast mode for this session
40
- /fast on # enable
41
- /fast off # disable
42
- /fast status # show the current state and why it is (in)active
43
- ```
20
+ Use `/fast` to control fast mode for the current session:
44
21
 
45
- Start a session with fast mode already enabled:
22
+ | Command | Action |
23
+ | -------------- | ---------------------------------------------------------------------------- |
24
+ | `/fast` | Toggle fast mode. |
25
+ | `/fast on` | Enable fast mode. |
26
+ | `/fast off` | Disable fast mode. |
27
+ | `/fast status` | Show the current state and explain whether it applies to the selected model. |
28
+
29
+ To start a session with fast mode enabled:
46
30
 
47
31
  ```bash
48
32
  pi --fast
49
33
  ```
50
34
 
51
- While fast mode is on and the current model is configured for it, a dim `[fast mode]`
52
- indicator is shown at the right of a status line right above the editor, shared with
53
- other pi-plugins extensions (e.g. `@pi-plugins/speed` renders its measurements at the
54
- left of the same line).
35
+ Fast mode applies only to models listed in the configuration. Other models are left
36
+ unchanged. A `[fast mode]` indicator appears above the editor while active.
37
+
38
+ ## Supported providers
39
+
40
+ | Provider | Pi provider |
41
+ | ------------ | -------------- |
42
+ | OpenAI | `openai` |
43
+ | OpenAI Codex | `openai-codex` |
44
+
45
+ Priority availability depends on the provider and model.
55
46
 
56
47
  ## Configuration
57
48
 
58
- Optional config file at `<agent-dir>/extensions/fast-mode.json` (typically
59
- `~/.pi/agent/extensions/fast-mode.json`):
49
+ Optionally create `<agent-dir>/extensions/fast-mode.json`, typically
50
+ `~/.pi/agent/extensions/fast-mode.json`. The defaults are:
60
51
 
61
52
  ```json
62
53
  {
@@ -79,16 +70,12 @@ Optional config file at `<agent-dir>/extensions/fast-mode.json` (typically
79
70
  }
80
71
  ```
81
72
 
82
- - `enabled`: fast-mode state at session start (`/fast` overrides it for the session
83
- without writing the toggle back to the file).
84
- - `models`: the `provider/model-id` keys fast mode applies to. The defaults above are
85
- used when the file is absent. Models whose API has no fast-mode support are
86
- ignored.
87
- - `showStatus`: show the `[fast mode]` indicator above the editor while active.
73
+ | Setting | Description |
74
+ | ------------ | ----------------------------------------------------------------------------------------------------------- |
75
+ | `enabled` | Enable fast mode at session start. `/fast` changes only the current session and does not write to the file. |
76
+ | `models` | The exact `provider/model-id` pairs fast mode applies to. |
77
+ | `showStatus` | Show the status indicator while fast mode is active. |
88
78
 
89
79
  ## Notes
90
80
 
91
- - The priority service tier bills at a higher rate than the default tier — check your
92
- provider's pricing before leaving it enabled.
93
- - If another extension already set a `service_tier` on the payload, this extension
94
- leaves it alone.
81
+ Priority inference can cost more. Check your provider's pricing before enabling it.