@herbertgao/pi-extensions 2026.9.18 → 2026.9.20

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 (226) hide show
  1. package/README.md +20 -28
  2. package/THIRD_PARTY_NOTICES.md +1 -1
  3. package/node_modules/@herbertgao/pi-cc-extensions/README.en.md +76 -59
  4. package/node_modules/@herbertgao/pi-cc-extensions/README.md +76 -59
  5. package/node_modules/@herbertgao/pi-cc-extensions/assets/readme/preview.webp +0 -0
  6. package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/config.ts +93 -34
  7. package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/panel.ts +346 -117
  8. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/agent-summary/core.ts +6 -36
  9. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/agent-summary/index.ts +2 -2
  10. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/compact-thinking.ts +24 -29
  11. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/context.ts +330 -360
  12. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/reference/index.ts +56 -108
  13. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/reference/session.ts +97 -15
  14. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/reference/subagent.ts +80 -45
  15. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/shell/flush-docked-bash.ts +36 -0
  16. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/shell/footer-layout.ts +229 -0
  17. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/shell/footer.ts +479 -0
  18. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/shell/startup-header.ts +92 -61
  19. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/shell/working-message.ts +75 -40
  20. package/node_modules/@herbertgao/pi-cc-extensions/extensions/index.ts +19 -3
  21. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/compact-mode.ts +1080 -229
  22. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/default-mode.ts +232 -259
  23. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/index.ts +42 -41
  24. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/markdown-enhance.ts +8 -2
  25. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/hover.ts +2 -7
  26. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/interaction.ts +277 -113
  27. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/layout.ts +15 -6
  28. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/packets.ts +18 -72
  29. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/scroll.ts +61 -26
  30. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/ansi-utils.ts +25 -57
  31. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-component.ts +125 -0
  32. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-edit-render.ts +227 -0
  33. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-header.ts +136 -0
  34. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-highlight.ts +115 -0
  35. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-inline.ts +324 -0
  36. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-layout.ts +762 -0
  37. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-limits.ts +152 -0
  38. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-palette.ts +410 -0
  39. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-parse.ts +535 -0
  40. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-presentation.ts +2 -2
  41. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-renderer.ts +21 -3219
  42. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-text.ts +44 -0
  43. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-write-render.ts +568 -0
  44. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/index.ts +47 -10
  45. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/line-width-safety.ts +2 -2
  46. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/write-execution.ts +1 -0
  47. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/grouping.ts +145 -190
  48. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/mcp-title.ts +39 -0
  49. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/{message-display.ts → tool/message-display.ts} +24 -90
  50. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/names.ts +426 -0
  51. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/result.ts +90 -132
  52. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/{show-more-hint.ts → tool/show-more-hint.ts} +10 -0
  53. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/transcript-refresh.ts +8 -26
  54. package/node_modules/@herbertgao/pi-cc-extensions/extensions/utils/ansi-color.ts +44 -0
  55. package/node_modules/@herbertgao/pi-cc-extensions/extensions/utils/ansi-text.ts +76 -0
  56. package/node_modules/@herbertgao/pi-cc-extensions/extensions/utils/component-tree.ts +34 -0
  57. package/node_modules/@herbertgao/pi-cc-extensions/extensions/utils/format.ts +28 -0
  58. package/node_modules/@herbertgao/pi-cc-extensions/extensions/utils/fullscreen-detect.ts +51 -0
  59. package/node_modules/@herbertgao/pi-cc-extensions/extensions/utils/patch-keys.ts +124 -0
  60. package/node_modules/@herbertgao/pi-cc-extensions/extensions/utils/sgr-mouse.ts +70 -0
  61. package/node_modules/@herbertgao/pi-cc-extensions/package.json +8 -8
  62. package/node_modules/pi-antigravity/README.md +29 -6
  63. package/node_modules/pi-antigravity/package.json +12 -7
  64. package/node_modules/pi-antigravity/src/auth/accounts.ts +327 -0
  65. package/node_modules/pi-antigravity/src/auth/index.ts +9 -0
  66. package/node_modules/pi-antigravity/src/client/client.ts +1 -1
  67. package/node_modules/pi-antigravity/src/image/image.ts +4 -4
  68. package/node_modules/pi-antigravity/src/index.ts +203 -13
  69. package/node_modules/pi-antigravity/src/search/index.ts +1 -0
  70. package/node_modules/pi-antigravity/src/search/search.ts +325 -0
  71. package/node_modules/pi-antigravity/src/stream/stream.ts +191 -14
  72. package/node_modules/pi-antigravity/src/types/enums.ts +45 -36
  73. package/node_modules/pi-antigravity/src/types/types.ts +5 -12
  74. package/node_modules/pi-antigravity/src/utils/http.ts +7 -2
  75. package/node_modules/pi-antigravity/src/utils/util.ts +31 -3
  76. package/node_modules/pi-typesafe/README.md +2 -0
  77. package/node_modules/pi-typesafe/package.json +2 -1
  78. package/node_modules/pi-web-access/CHANGELOG.md +72 -0
  79. package/node_modules/pi-web-access/README.md +34 -13
  80. package/node_modules/pi-web-access/chrome-cookies.ts +79 -25
  81. package/node_modules/pi-web-access/curator-page.ts +5 -1
  82. package/node_modules/pi-web-access/curator-server.ts +2 -0
  83. package/node_modules/pi-web-access/dist/index.js +1337 -1009
  84. package/node_modules/pi-web-access/exa.ts +20 -52
  85. package/node_modules/pi-web-access/extract.ts +41 -4
  86. package/node_modules/pi-web-access/gemini-search.ts +12 -4
  87. package/node_modules/pi-web-access/index.ts +29 -37
  88. package/node_modules/pi-web-access/openai-search.ts +35 -7
  89. package/node_modules/pi-web-access/package.json +6 -5
  90. package/node_modules/pi-web-access/page-query.ts +2 -2
  91. package/node_modules/pi-web-access/query-rewrite.ts +2 -3
  92. package/node_modules/pi-web-access/ssrf-protection.ts +27 -10
  93. package/node_modules/pi-web-access/storage.ts +48 -77
  94. package/node_modules/pi-web-access/summary-model-scope.ts +6 -77
  95. package/node_modules/pi-web-access/summary-review.ts +3 -4
  96. package/node_modules/pi-web-access/tavily.ts +67 -20
  97. package/node_modules/pi-web-access/tool-activation.ts +53 -33
  98. package/node_modules/pi-web-access/utils.ts +1 -1
  99. package/node_modules/pi-web-access/you.ts +164 -0
  100. package/node_modules/pi-web-access/zai.ts +161 -0
  101. package/package.json +7 -11
  102. package/node_modules/@herbertgao/pi-cc-extensions/assets/readme/hero.en.svg +0 -101
  103. package/node_modules/@herbertgao/pi-cc-extensions/assets/readme/hero.svg +0 -101
  104. package/node_modules/@herbertgao/pi-cc-extensions/assets/readme/welcome.webp +0 -0
  105. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/render-utils.ts +0 -5
  106. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/types.ts +0 -30
  107. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/write-display-utils.ts +0 -12
  108. package/node_modules/pi-footer/CHANGELOG.md +0 -145
  109. package/node_modules/pi-footer/LICENSE +0 -21
  110. package/node_modules/pi-footer/README.md +0 -354
  111. package/node_modules/pi-footer/package.json +0 -68
  112. package/node_modules/pi-footer/src/cache.ts +0 -106
  113. package/node_modules/pi-footer/src/colors.ts +0 -237
  114. package/node_modules/pi-footer/src/config.ts +0 -202
  115. package/node_modules/pi-footer/src/event-widgets.ts +0 -41
  116. package/node_modules/pi-footer/src/extension-statuses.ts +0 -102
  117. package/node_modules/pi-footer/src/git.ts +0 -135
  118. package/node_modules/pi-footer/src/index.ts +0 -268
  119. package/node_modules/pi-footer/src/metrics.ts +0 -141
  120. package/node_modules/pi-footer/src/presets.ts +0 -383
  121. package/node_modules/pi-footer/src/render.ts +0 -114
  122. package/node_modules/pi-footer/src/separators.ts +0 -52
  123. package/node_modules/pi-footer/src/types.ts +0 -122
  124. package/node_modules/pi-footer/src/ui/color-level-confirm.ts +0 -22
  125. package/node_modules/pi-footer/src/ui/color-options.ts +0 -195
  126. package/node_modules/pi-footer/src/ui/config-lifecycle.ts +0 -61
  127. package/node_modules/pi-footer/src/ui/edit-colors.ts +0 -13
  128. package/node_modules/pi-footer/src/ui/events.ts +0 -35
  129. package/node_modules/pi-footer/src/ui/extension-status-picker.ts +0 -60
  130. package/node_modules/pi-footer/src/ui/extension-statuses.ts +0 -73
  131. package/node_modules/pi-footer/src/ui/fields.ts +0 -252
  132. package/node_modules/pi-footer/src/ui/global-menu.ts +0 -126
  133. package/node_modules/pi-footer/src/ui/helpers.ts +0 -44
  134. package/node_modules/pi-footer/src/ui/layout.ts +0 -17
  135. package/node_modules/pi-footer/src/ui/line-list.ts +0 -19
  136. package/node_modules/pi-footer/src/ui/model.ts +0 -64
  137. package/node_modules/pi-footer/src/ui/navigation.ts +0 -29
  138. package/node_modules/pi-footer/src/ui/option-edit.ts +0 -93
  139. package/node_modules/pi-footer/src/ui/overlay-render.ts +0 -103
  140. package/node_modules/pi-footer/src/ui/screen-context.ts +0 -18
  141. package/node_modules/pi-footer/src/ui/screen-controller.ts +0 -27
  142. package/node_modules/pi-footer/src/ui/screen-render.ts +0 -32
  143. package/node_modules/pi-footer/src/ui/screen-state.ts +0 -20
  144. package/node_modules/pi-footer/src/ui/screen.ts +0 -243
  145. package/node_modules/pi-footer/src/ui/screens/add-widget.ts +0 -92
  146. package/node_modules/pi-footer/src/ui/screens/confirm-exit.ts +0 -52
  147. package/node_modules/pi-footer/src/ui/screens/controller.ts +0 -12
  148. package/node_modules/pi-footer/src/ui/screens/edit-colors.ts +0 -68
  149. package/node_modules/pi-footer/src/ui/screens/edit-widget.ts +0 -115
  150. package/node_modules/pi-footer/src/ui/screens/extension-status-row.ts +0 -61
  151. package/node_modules/pi-footer/src/ui/screens/global.ts +0 -75
  152. package/node_modules/pi-footer/src/ui/screens/line-list.ts +0 -108
  153. package/node_modules/pi-footer/src/ui/screens/main.ts +0 -99
  154. package/node_modules/pi-footer/src/ui/screens/terminal.ts +0 -121
  155. package/node_modules/pi-footer/src/ui/screens/widget-list.ts +0 -154
  156. package/node_modules/pi-footer/src/ui/terminal-menu.ts +0 -32
  157. package/node_modules/pi-footer/src/ui/theme.ts +0 -48
  158. package/node_modules/pi-footer/src/ui/title-bar.ts +0 -100
  159. package/node_modules/pi-footer/src/ui/widget-actions.ts +0 -76
  160. package/node_modules/pi-footer/src/ui.ts +0 -66
  161. package/node_modules/pi-footer/src/widgets/context.ts +0 -28
  162. package/node_modules/pi-footer/src/widgets/core/active-tools.ts +0 -17
  163. package/node_modules/pi-footer/src/widgets/core/context-window.ts +0 -24
  164. package/node_modules/pi-footer/src/widgets/core/cwd-basename.ts +0 -19
  165. package/node_modules/pi-footer/src/widgets/core/cwd.ts +0 -108
  166. package/node_modules/pi-footer/src/widgets/core/event.ts +0 -34
  167. package/node_modules/pi-footer/src/widgets/core/external-status.ts +0 -102
  168. package/node_modules/pi-footer/src/widgets/core/model-provider.ts +0 -20
  169. package/node_modules/pi-footer/src/widgets/core/model.ts +0 -35
  170. package/node_modules/pi-footer/src/widgets/core/provider.ts +0 -17
  171. package/node_modules/pi-footer/src/widgets/core/session-name.ts +0 -17
  172. package/node_modules/pi-footer/src/widgets/core/text-verbosity.ts +0 -17
  173. package/node_modules/pi-footer/src/widgets/core/thinking-level.ts +0 -17
  174. package/node_modules/pi-footer/src/widgets/git/ahead-behind.ts +0 -17
  175. package/node_modules/pi-footer/src/widgets/git/branch.ts +0 -81
  176. package/node_modules/pi-footer/src/widgets/git/clean.ts +0 -19
  177. package/node_modules/pi-footer/src/widgets/git/deletions.ts +0 -17
  178. package/node_modules/pi-footer/src/widgets/git/diff.ts +0 -38
  179. package/node_modules/pi-footer/src/widgets/git/insertions.ts +0 -17
  180. package/node_modules/pi-footer/src/widgets/git/remote.ts +0 -17
  181. package/node_modules/pi-footer/src/widgets/git/root.ts +0 -17
  182. package/node_modules/pi-footer/src/widgets/git/sha.ts +0 -17
  183. package/node_modules/pi-footer/src/widgets/git/staged.ts +0 -17
  184. package/node_modules/pi-footer/src/widgets/git/status.ts +0 -19
  185. package/node_modules/pi-footer/src/widgets/git/unstaged.ts +0 -17
  186. package/node_modules/pi-footer/src/widgets/git/untracked.ts +0 -17
  187. package/node_modules/pi-footer/src/widgets/instance.ts +0 -118
  188. package/node_modules/pi-footer/src/widgets/layout/custom-text.ts +0 -18
  189. package/node_modules/pi-footer/src/widgets/layout/flex-separator.ts +0 -32
  190. package/node_modules/pi-footer/src/widgets/layout/separator.ts +0 -57
  191. package/node_modules/pi-footer/src/widgets/layout/spacer.ts +0 -31
  192. package/node_modules/pi-footer/src/widgets/options.ts +0 -122
  193. package/node_modules/pi-footer/src/widgets/project/runtime.ts +0 -361
  194. package/node_modules/pi-footer/src/widgets/registry.ts +0 -223
  195. package/node_modules/pi-footer/src/widgets/session/assistant-messages.ts +0 -17
  196. package/node_modules/pi-footer/src/widgets/session/compactions.ts +0 -17
  197. package/node_modules/pi-footer/src/widgets/session/elapsed.ts +0 -18
  198. package/node_modules/pi-footer/src/widgets/session/last-activity.ts +0 -18
  199. package/node_modules/pi-footer/src/widgets/session/messages.ts +0 -19
  200. package/node_modules/pi-footer/src/widgets/session/session-id.ts +0 -17
  201. package/node_modules/pi-footer/src/widgets/session/session-start.ts +0 -18
  202. package/node_modules/pi-footer/src/widgets/session/tool-results.ts +0 -17
  203. package/node_modules/pi-footer/src/widgets/session/total-messages.ts +0 -19
  204. package/node_modules/pi-footer/src/widgets/session/total-time.ts +0 -18
  205. package/node_modules/pi-footer/src/widgets/session/user-messages.ts +0 -17
  206. package/node_modules/pi-footer/src/widgets/store.ts +0 -26
  207. package/node_modules/pi-footer/src/widgets/tokens/cache-hit-rate.ts +0 -55
  208. package/node_modules/pi-footer/src/widgets/tokens/cache-read.ts +0 -18
  209. package/node_modules/pi-footer/src/widgets/tokens/cache-write.ts +0 -18
  210. package/node_modules/pi-footer/src/widgets/tokens/context-bar.ts +0 -108
  211. package/node_modules/pi-footer/src/widgets/tokens/context-length.ts +0 -26
  212. package/node_modules/pi-footer/src/widgets/tokens/context-remaining.ts +0 -29
  213. package/node_modules/pi-footer/src/widgets/tokens/context.ts +0 -29
  214. package/node_modules/pi-footer/src/widgets/tokens/cost.ts +0 -51
  215. package/node_modules/pi-footer/src/widgets/tokens/input-speed.ts +0 -25
  216. package/node_modules/pi-footer/src/widgets/tokens/input-tokens.ts +0 -18
  217. package/node_modules/pi-footer/src/widgets/tokens/output-speed.ts +0 -25
  218. package/node_modules/pi-footer/src/widgets/tokens/output-tokens.ts +0 -18
  219. package/node_modules/pi-footer/src/widgets/tokens/tokens.ts +0 -20
  220. package/node_modules/pi-footer/src/widgets/tokens/total-speed.ts +0 -25
  221. package/node_modules/pi-footer/src/widgets/tokens/total-tokens.ts +0 -18
  222. package/node_modules/pi-footer/src/widgets/types.ts +0 -181
  223. package/node_modules/pi-footer/src/widgets/utils/colors.ts +0 -11
  224. package/node_modules/pi-footer/src/widgets/utils/context.ts +0 -112
  225. package/node_modules/pi-footer/src/widgets/utils/session.ts +0 -19
  226. package/node_modules/pi-footer/src/widgets/utils/token-format.ts +0 -78
@@ -0,0 +1,227 @@
1
+ import { Text, type Component } from "@earendil-works/pi-tui";
2
+ import type { EditToolDetails } from "@earendil-works/pi-coding-agent";
3
+ import { sanitizeToolResultText } from "../../../utils/tool-result-sanitize.ts";
4
+ import {
5
+ getLineNumberWidth,
6
+ parseDiff,
7
+ type ParsedDiff,
8
+ type ParsedDiffEntry,
9
+ } from "./diff-parse.ts";
10
+ import { buildInlineHighlightMap, buildSplitRows } from "./diff-inline.ts";
11
+ import { resolveDiffPalette, type DiffTheme } from "./diff-palette.ts";
12
+ import { createCodeLineHighlighter, resolveLanguageFromPath } from "./diff-highlight.ts";
13
+ import {
14
+ canRenderSplitLayout,
15
+ renderCompact,
16
+ renderSplit,
17
+ renderUnified,
18
+ type DiffRenderContext,
19
+ } from "./diff-layout.ts";
20
+ import { renderDiffFrameLine, renderHeaderRows, renderSingleDiffRow } from "./diff-header.ts";
21
+ import {
22
+ applyLineLimit,
23
+ clampDiffLinesToWidth,
24
+ resolveDiffDisplayLimit,
25
+ resolveDiffProcessBudget,
26
+ takeEntriesForLineBudget,
27
+ takeSplitRowsForBudget,
28
+ } from "./diff-limits.ts";
29
+ import {
30
+ RICH_DIFF_COMPONENT,
31
+ createDiffRenderCache,
32
+ displayConfigCacheKey,
33
+ resolveDiffIndicatorMode,
34
+ resolveLiveDisplayConfig,
35
+ normalizeCollapsedHintLine,
36
+ type DiffRenderOptions,
37
+ type DisplayConfigInput,
38
+ } from "./diff-component.ts";
39
+ import {
40
+ buildDiffSummaryText,
41
+ normalizeDiffRenderWidth,
42
+ resolveDiffPresentationMode,
43
+ } from "./diff-presentation.ts";
44
+
45
+ function safeGetDiff(details: unknown): string {
46
+ if (!details || typeof details !== "object") {
47
+ return "";
48
+ }
49
+ const typed = details as Partial<EditToolDetails>;
50
+ return typeof typed.diff === "string" ? typed.diff : "";
51
+ }
52
+
53
+ // Pi appends an omission marker after final context to say the file continues.
54
+ // It does not separate visible diff regions, so keep only omissions before a rendered line.
55
+ function omitTerminalOmissions(entries: ParsedDiffEntry[]): ParsedDiffEntry[] {
56
+ let lastLineIndex = -1;
57
+ for (let index = entries.length - 1; index >= 0; index--) {
58
+ if (entries[index]?.kind === "line") {
59
+ lastLineIndex = index;
60
+ break;
61
+ }
62
+ }
63
+
64
+ return entries.filter((entry, index) => entry.kind !== "omission" || index < lastLineIndex);
65
+ }
66
+
67
+ /**
68
+ * edit 变更行统计(compact 单行 `(+A -D)` 用):复用现有 parser,diff 文件头不计入。
69
+ * 无 diff 或解析失败时返回 undefined,不把未知状态误报为零变更。
70
+ */
71
+ export function countEditDiffStats(
72
+ details: unknown,
73
+ ): { added: number; removed: number } | undefined {
74
+ const diffText = sanitizeToolResultText(safeGetDiff(details));
75
+ if (!diffText.trim()) return undefined;
76
+ try {
77
+ const parsed = parseDiff(diffText);
78
+ return { added: parsed.stats.added, removed: parsed.stats.removed };
79
+ } catch {
80
+ return undefined;
81
+ }
82
+ }
83
+
84
+ export function renderEditDiffResult(
85
+ details: unknown,
86
+ options: DiffRenderOptions,
87
+ config: DisplayConfigInput,
88
+ theme: DiffTheme,
89
+ fallbackText: string,
90
+ ): Component {
91
+ const diffText = sanitizeToolResultText(safeGetDiff(details));
92
+ const safeFallbackText = sanitizeToolResultText(fallbackText);
93
+ if (!diffText.trim()) {
94
+ if (!safeFallbackText.trim()) {
95
+ return new Text(theme.fg("muted", "↳ edit completed (no diff payload)"), 0, 0);
96
+ }
97
+ return new Text(theme.fg("toolOutput", safeFallbackText), 0, 0);
98
+ }
99
+
100
+ let parsed: ParsedDiff;
101
+ try {
102
+ parsed = parseDiff(diffText);
103
+ } catch (error) {
104
+ const message = error instanceof Error ? error.message : String(error);
105
+ return new Text(theme.fg("warning", `↳ unable to render diff: ${message}`), 0, 0);
106
+ }
107
+
108
+ const renderEntries = omitTerminalOmissions(parsed.entries);
109
+ if (renderEntries.length === 0) {
110
+ return new Text(theme.fg("muted", "↳ no diff data"), 0, 0);
111
+ }
112
+
113
+ const splitRows = buildSplitRows(renderEntries);
114
+ const showHashlineAnchors =
115
+ options.expanded === true &&
116
+ renderEntries.some((entry) => entry.kind === "line" && !!entry.hashlineAnchorContent);
117
+ const lineNumberWidth = getLineNumberWidth(renderEntries, showHashlineAnchors);
118
+ const palette = resolveDiffPalette(theme);
119
+ // Rich diffs use ccstyle's self shell. Keep the panel transparent so the
120
+ // separator cannot leak toolSuccessBg across the entire new column.
121
+ const containerBgAnsi = undefined;
122
+ const language = resolveLanguageFromPath(options.filePath);
123
+ const cache = createDiffRenderCache();
124
+ const highlightLine = createCodeLineHighlighter(language, theme, renderEntries, () => {
125
+ cache.invalidate();
126
+ options.invalidate?.();
127
+ });
128
+
129
+ return {
130
+ [RICH_DIFF_COMPONENT]: true,
131
+ /** 鼠标层只在真正的 remainder 行上提供展开入口。 */
132
+ isCollapsedHintLine(plainLine: string): boolean {
133
+ const hint = cache.getHintLine();
134
+ return hint !== undefined && normalizeCollapsedHintLine(plainLine) === hint;
135
+ },
136
+ render(width: number): string[] {
137
+ // Live config: panel can change indicator/wrap/limits after this component is created.
138
+ const live = resolveLiveDisplayConfig(config);
139
+ const wordWrap = live.diffWordWrap;
140
+ const indicatorMode = resolveDiffIndicatorMode(live);
141
+ const configKey = displayConfigCacheKey(live);
142
+ const safeWidth = normalizeDiffRenderWidth(width);
143
+ const mode = resolveDiffPresentationMode(live, safeWidth, canRenderSplitLayout(safeWidth));
144
+ const hovered = options.isHovered?.() ?? false;
145
+ const cached = cache.get(safeWidth, options.expanded, mode, configKey, hovered);
146
+ if (cached) {
147
+ return cached;
148
+ }
149
+
150
+ if (mode === "summary") {
151
+ return cache.set(
152
+ safeWidth,
153
+ options.expanded,
154
+ mode,
155
+ configKey,
156
+ hovered,
157
+ clampDiffLinesToWidth(
158
+ renderSingleDiffRow(
159
+ buildDiffSummaryText(parsed.stats, safeWidth),
160
+ "toolOutput",
161
+ safeWidth,
162
+ theme,
163
+ ),
164
+ safeWidth,
165
+ ),
166
+ );
167
+ }
168
+
169
+ const headerRows = renderHeaderRows(parsed.stats, mode, safeWidth, theme);
170
+ const displayLimit = resolveDiffDisplayLimit(options.expanded, live.editDiffCollapsedLines);
171
+ const processBudget = resolveDiffProcessBudget(displayLimit, wordWrap);
172
+ // Only highlight/render a prefix that can fill the display limit; full-diff
173
+ // LCS + syntax highlight on thousands of hidden lines is pure waste when collapsed.
174
+ const entryBudget = takeEntriesForLineBudget(renderEntries, processBudget);
175
+ const splitBudget = takeSplitRowsForBudget(splitRows, processBudget);
176
+ const inlineHighlights = buildInlineHighlightMap(splitBudget.rows);
177
+ const renderCtx: DiffRenderContext = {
178
+ width: safeWidth,
179
+ theme,
180
+ inlineHighlights,
181
+ palette,
182
+ highlightLine,
183
+ containerBgAnsi,
184
+ wordWrap,
185
+ indicatorMode,
186
+ showHashlineAnchors,
187
+ };
188
+ const bodyRows =
189
+ mode === "split"
190
+ ? renderSplit(splitBudget.rows, renderCtx, lineNumberWidth)
191
+ : mode === "compact"
192
+ ? renderCompact(entryBudget.entries, renderCtx)
193
+ : renderUnified(entryBudget.entries, renderCtx, lineNumberWidth);
194
+ const unprocessedLogicalRows =
195
+ mode === "split"
196
+ ? Math.max(0, splitBudget.totalRows - splitBudget.processedRows)
197
+ : Math.max(0, entryBudget.totalLineEntries - entryBudget.processedLineEntries);
198
+ const bodyWithLimit = applyLineLimit(
199
+ bodyRows,
200
+ safeWidth,
201
+ options.expanded,
202
+ live.editDiffCollapsedLines,
203
+ parsed.stats.hunks,
204
+ theme,
205
+ unprocessedLogicalRows,
206
+ hovered,
207
+ );
208
+ const frame = renderDiffFrameLine(safeWidth, theme);
209
+ const renderedLines =
210
+ mode === "unified"
211
+ ? [...headerRows.map((row) => row.text), frame, ...bodyWithLimit.lines, frame]
212
+ : [...headerRows.map((row) => row.text), ...bodyWithLimit.lines];
213
+
214
+ const clampedLines = clampDiffLinesToWidth(renderedLines, safeWidth);
215
+ return cache.set(
216
+ safeWidth,
217
+ options.expanded,
218
+ mode,
219
+ configKey,
220
+ hovered,
221
+ clampedLines,
222
+ normalizeCollapsedHintLine(bodyWithLimit.hintLine),
223
+ );
224
+ },
225
+ invalidate: cache.invalidate,
226
+ } as Component;
227
+ }
@@ -0,0 +1,136 @@
1
+ import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
2
+ import { sanitizeAnsiForThemedOutput } from "./ansi-utils.ts";
3
+ import { normalizeCodeWhitespace, wrapToWidth } from "./diff-text.ts";
4
+ import { emphasis, stabilizeBackgroundResets, type DiffTheme } from "./diff-palette.ts";
5
+ import { clampDiffLineToWidth } from "./diff-limits.ts";
6
+ import type { DiffMetaEntry, DiffStats } from "./diff-parse.ts";
7
+ import type { DiffPresentationMode } from "./diff-presentation.ts";
8
+ import type { RenderedRow } from "./diff-layout.ts";
9
+
10
+ export function formatMetaEntryRows(
11
+ entry: DiffMetaEntry,
12
+ width: number,
13
+ theme: DiffTheme,
14
+ wordWrap: boolean,
15
+ ): RenderedRow[] {
16
+ const normalized = sanitizeAnsiForThemedOutput(normalizeCodeWhitespace(entry.raw));
17
+ const lines = wordWrap
18
+ ? wrapToWidth(normalized, width, true)
19
+ : [truncateToWidth(normalized, width)];
20
+
21
+ const mapColor = (line: string): string => {
22
+ if (entry.kind === "hunk") {
23
+ return stabilizeBackgroundResets(theme.fg("accent", line));
24
+ }
25
+ if (entry.kind === "file") {
26
+ return stabilizeBackgroundResets(theme.fg("muted", line));
27
+ }
28
+ return stabilizeBackgroundResets(theme.fg("toolDiffContext", line));
29
+ };
30
+
31
+ return lines.map((line) => ({
32
+ text: mapColor(line),
33
+ hunkIndex: entry.kind === "file" ? null : entry.hunkIndex || null,
34
+ }));
35
+ }
36
+
37
+ function renderDiffStatBar(stats: DiffStats, width: number, theme: DiffTheme): string | null {
38
+ const totalChanges = stats.added + stats.removed;
39
+ if (totalChanges === 0 || width < 20) {
40
+ return null;
41
+ }
42
+
43
+ const barSlots = Math.max(8, Math.min(24, Math.floor(width / 12)));
44
+ let addedSlots = Math.max(
45
+ 0,
46
+ Math.min(barSlots, Math.round((stats.added / totalChanges) * barSlots)),
47
+ );
48
+ if (stats.added > 0 && addedSlots === 0) {
49
+ addedSlots = 1;
50
+ }
51
+ if (stats.removed > 0 && addedSlots >= barSlots) {
52
+ addedSlots = barSlots - 1;
53
+ }
54
+ const removedSlots = Math.max(0, barSlots - addedSlots);
55
+
56
+ const addedBar = addedSlots > 0 ? theme.fg("toolDiffAdded", "━".repeat(addedSlots)) : "";
57
+ const removedBar = removedSlots > 0 ? theme.fg("toolDiffRemoved", "━".repeat(removedSlots)) : "";
58
+ return stabilizeBackgroundResets(
59
+ `${theme.fg("dim", "[")}${addedBar}${removedBar}${theme.fg("dim", "]")}`,
60
+ );
61
+ }
62
+
63
+ function buildDiffSummaryBasePieces(stats: DiffStats, theme: DiffTheme): string[] {
64
+ return [
65
+ theme.fg("toolOutput", `↳ ${emphasis(theme, "diff")}`),
66
+ theme.fg("toolDiffAdded", `+${stats.added}`),
67
+ theme.fg("toolDiffRemoved", `-${stats.removed}`),
68
+ ];
69
+ }
70
+
71
+ export function renderHeaderRows(
72
+ stats: DiffStats,
73
+ mode: Exclude<DiffPresentationMode, "summary">,
74
+ width: number,
75
+ theme: DiffTheme,
76
+ ): RenderedRow[] {
77
+ if (mode === "compact") {
78
+ const summary = buildDiffSummaryBasePieces(stats, theme).join(" ");
79
+ return [{ text: stabilizeBackgroundResets(truncateToWidth(summary, width)), hunkIndex: null }];
80
+ }
81
+
82
+ const summaryPieces = [...buildDiffSummaryBasePieces(stats, theme), theme.fg("muted", mode)];
83
+
84
+ const summary = summaryPieces.join(mode === "split" ? " " : theme.fg("muted", " • "));
85
+ const meter = renderDiffStatBar(stats, width, theme);
86
+ if (!meter) {
87
+ return [{ text: stabilizeBackgroundResets(truncateToWidth(summary, width)), hunkIndex: null }];
88
+ }
89
+
90
+ const meterSeparator = " ";
91
+ const meterWidth = visibleWidth(meterSeparator) + visibleWidth(meter);
92
+ if (meterWidth >= width) {
93
+ return [{ text: stabilizeBackgroundResets(truncateToWidth(summary, width)), hunkIndex: null }];
94
+ }
95
+
96
+ const summaryWidth = Math.max(0, width - meterWidth);
97
+ const fittedSummary = truncateToWidth(summary, summaryWidth);
98
+ return [
99
+ {
100
+ text: stabilizeBackgroundResets(`${fittedSummary}${meterSeparator}${meter}`),
101
+ hunkIndex: null,
102
+ },
103
+ ];
104
+ }
105
+
106
+ export function renderDiffFrameLine(width: number, theme: DiffTheme): string {
107
+ const frameWidth = Math.max(0, width);
108
+ if (frameWidth === 0) {
109
+ return "";
110
+ }
111
+ return stabilizeBackgroundResets(theme.fg("dim", "─".repeat(frameWidth)));
112
+ }
113
+
114
+ export function renderSingleDiffRow(
115
+ text: string,
116
+ color: string,
117
+ width: number,
118
+ theme: DiffTheme,
119
+ ): string[] {
120
+ if (width <= 0) {
121
+ return [""];
122
+ }
123
+ return [clampDiffLineToWidth(stabilizeBackgroundResets(theme.fg(color, text)), width)];
124
+ }
125
+
126
+ export function renderWriteHeader(
127
+ wasOverwrite: boolean,
128
+ width: number,
129
+ theme: DiffTheme,
130
+ headerLabel?: string,
131
+ ): string {
132
+ const actionLabel = headerLabel?.trim() || (wasOverwrite ? "overwritten" : "created");
133
+ return stabilizeBackgroundResets(
134
+ truncateToWidth(theme.fg("toolOutput", `↳ ${emphasis(theme, actionLabel)}`), width),
135
+ );
136
+ }
@@ -0,0 +1,115 @@
1
+ import { getLanguageFromPath, highlightCode } from "@earendil-works/pi-coding-agent";
2
+ import { sanitizeAnsiForThemedOutput } from "./ansi-utils.ts";
3
+ import { sanitizeToolResultText } from "../../../utils/tool-result-sanitize.ts";
4
+ import { MAX_HL_CHARS, shikiHighlightCache } from "./shiki-highlight.ts";
5
+ import { normalizeCodeWhitespace } from "./diff-text.ts";
6
+ import {
7
+ applyInlineSpanHighlight,
8
+ getLineBackground,
9
+ resolveShikiTheme,
10
+ type DiffPalette,
11
+ type DiffTheme,
12
+ } from "./diff-palette.ts";
13
+ import type { DiffLineEntry, ParsedDiffEntry } from "./diff-parse.ts";
14
+ import type { DiffSpan } from "./diff-inline.ts";
15
+
16
+ export type CodeLineHighlighter = (line: string, entry: DiffLineEntry) => string;
17
+
18
+ function cleanCodeLine(line: string): string {
19
+ return sanitizeToolResultText(line).replace(/\n/g, "");
20
+ }
21
+
22
+ export function shouldHighlightCodeBlock(code: string): boolean {
23
+ return code.length <= MAX_HL_CHARS;
24
+ }
25
+
26
+ export function resolveLanguageFromPath(rawPath: string | undefined): string | undefined {
27
+ if (!rawPath || !rawPath.trim()) {
28
+ return undefined;
29
+ }
30
+ const normalizedPath = rawPath.replace(/^@/, "").trim();
31
+ if (!normalizedPath) {
32
+ return undefined;
33
+ }
34
+ try {
35
+ return getLanguageFromPath(normalizedPath);
36
+ } catch {
37
+ return undefined;
38
+ }
39
+ }
40
+
41
+ export function createCodeLineHighlighter(
42
+ language: string | undefined,
43
+ theme: DiffTheme,
44
+ entries: readonly ParsedDiffEntry[],
45
+ invalidate?: () => void,
46
+ ): CodeLineHighlighter {
47
+ const codeEntries = entries.filter((entry): entry is DiffLineEntry => entry.kind === "line");
48
+ const cleanLines = codeEntries.map((entry) =>
49
+ cleanCodeLine(normalizeCodeWhitespace(entry.content)),
50
+ );
51
+ const code = cleanLines.join("\n");
52
+ const shouldHighlight = !!language && shouldHighlightCodeBlock(code);
53
+ const fallbackLines = shouldHighlight
54
+ ? (() => {
55
+ try {
56
+ return highlightCode(code, language).map(sanitizeAnsiForThemedOutput);
57
+ } catch {
58
+ return cleanLines.map(sanitizeAnsiForThemedOutput);
59
+ }
60
+ })()
61
+ : cleanLines.map(sanitizeAnsiForThemedOutput);
62
+ const fallbackByEntry = new WeakMap(
63
+ codeEntries.map((entry, index) => [entry, fallbackLines[index] ?? cleanLines[index] ?? ""]),
64
+ );
65
+ if (!shouldHighlight) return (_line, entry) => fallbackByEntry.get(entry) ?? "";
66
+
67
+ const shikiTheme = resolveShikiTheme(theme);
68
+ let highlightedByEntry: WeakMap<DiffLineEntry, string> | undefined;
69
+ const resolveHighlighted = () => {
70
+ if (highlightedByEntry) return;
71
+ const highlighted = shikiHighlightCache.get(
72
+ code,
73
+ language,
74
+ shikiTheme,
75
+ fallbackLines,
76
+ invalidate,
77
+ );
78
+ if (highlighted) {
79
+ highlightedByEntry = new WeakMap(
80
+ codeEntries.map((entry, index) => [
81
+ entry,
82
+ sanitizeAnsiForThemedOutput(highlighted[index] ?? fallbackByEntry.get(entry) ?? ""),
83
+ ]),
84
+ );
85
+ }
86
+ };
87
+ resolveHighlighted();
88
+ return (_line, entry) => {
89
+ resolveHighlighted();
90
+ return highlightedByEntry?.get(entry) ?? fallbackByEntry.get(entry) ?? "";
91
+ };
92
+ }
93
+
94
+ export function highlightDiffLine(
95
+ codeText: string,
96
+ entry: DiffLineEntry,
97
+ inlineHighlights: WeakMap<DiffLineEntry, DiffSpan[]>,
98
+ palette: DiffPalette,
99
+ highlightLine: CodeLineHighlighter,
100
+ containerBgAnsi: string | undefined,
101
+ ): { highlighted: string; rowBg: string | undefined } {
102
+ const syntaxHighlighted = highlightLine(codeText, entry);
103
+ const rowBg = getLineBackground(entry.lineKind, palette, false);
104
+ const emphasisBg = getLineBackground(entry.lineKind, palette, true);
105
+ const inlineSpans = inlineHighlights.get(entry) ?? [];
106
+ const highlighted = applyInlineSpanHighlight(
107
+ codeText,
108
+ syntaxHighlighted,
109
+ inlineSpans,
110
+ emphasisBg,
111
+ rowBg,
112
+ containerBgAnsi,
113
+ );
114
+ return { highlighted, rowBg };
115
+ }