@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,535 @@
1
+ import { visibleWidth } from "@earendil-works/pi-tui";
2
+ import { fitToWidth } from "./diff-text.ts";
3
+
4
+ export type DiffLineKind = "add" | "remove" | "context";
5
+ export type DiffEntryKind = "line" | "omission" | "meta" | "hunk" | "file";
6
+
7
+ export interface DiffLineEntry {
8
+ kind: "line";
9
+ lineKind: DiffLineKind;
10
+ oldLineNumber: number | null;
11
+ newLineNumber: number | null;
12
+ fallbackLineNumber: string;
13
+ content: string;
14
+ hashlineAnchorContent?: string;
15
+ raw: string;
16
+ hunkIndex: number;
17
+ }
18
+
19
+ export interface DiffOmissionEntry {
20
+ kind: "omission";
21
+ raw: string;
22
+ hunkIndex: number;
23
+ }
24
+
25
+ export interface DiffMetaEntry {
26
+ kind: Exclude<DiffEntryKind, "line" | "omission">;
27
+ raw: string;
28
+ hunkIndex: number;
29
+ }
30
+
31
+ export type ParsedDiffEntry = DiffLineEntry | DiffOmissionEntry | DiffMetaEntry;
32
+
33
+ export interface ParsedDiff {
34
+ entries: ParsedDiffEntry[];
35
+ stats: DiffStats;
36
+ }
37
+
38
+ export interface DiffStats {
39
+ added: number;
40
+ removed: number;
41
+ context: number;
42
+ hunks: number;
43
+ files: number;
44
+ lines: number;
45
+ }
46
+
47
+ const CANONICAL_LINE_PATTERN = /^([+\- ])(\s*\d+)\|(.*)$/;
48
+ const HASHLINE_ANCHOR_LINE_PATTERN = /^([+\- ])(\s*\d+)#([A-Za-z0-9]+| {2}):(.*)$/;
49
+ // Pi still emits space-separated numbered rows, unlike OMP's pipe-delimited format.
50
+ const PI_LINE_PATTERN = /^([+\- ])(\s*\d+)\s(.*)$/;
51
+ const PI_OMISSION_LINE_PATTERN = /^ {3,}\.\.\.$/;
52
+ const HUNK_HEADER_PATTERN = /^@@\s+-(\d+)(?:,(\d+))?\s+\+(\d+)(?:,(\d+))?\s+@@(.*)$/;
53
+ const MIN_LINE_NUMBER_WIDTH = 2;
54
+
55
+ function toParsedDiffLine(
56
+ prefix: string,
57
+ lineNumber: string,
58
+ content: string,
59
+ ): {
60
+ lineKind: DiffLineKind;
61
+ lineNumber: string;
62
+ content: string;
63
+ } {
64
+ const normalizedLineNumber = lineNumber.trim();
65
+ if (prefix === "+") {
66
+ return { lineKind: "add", lineNumber: normalizedLineNumber, content };
67
+ }
68
+ if (prefix === "-") {
69
+ return { lineKind: "remove", lineNumber: normalizedLineNumber, content };
70
+ }
71
+ return { lineKind: "context", lineNumber: normalizedLineNumber, content };
72
+ }
73
+
74
+ function parseCanonicalDiffLine(
75
+ line: string,
76
+ allowPiFormat: boolean,
77
+ ): {
78
+ lineKind: DiffLineKind;
79
+ lineNumber: string;
80
+ content: string;
81
+ hashlineAnchorContent?: string;
82
+ } | null {
83
+ const hashlineAnchorMatch = line.match(HASHLINE_ANCHOR_LINE_PATTERN);
84
+ if (hashlineAnchorMatch) {
85
+ const lineNumber = hashlineAnchorMatch[2] ?? "";
86
+ const hash = hashlineAnchorMatch[3] ?? "";
87
+ const content = hashlineAnchorMatch[4] ?? "";
88
+ const parsed = toParsedDiffLine(hashlineAnchorMatch[1] ?? " ", lineNumber, content);
89
+ return {
90
+ ...parsed,
91
+ hashlineAnchorContent: `${lineNumber.trim()}#${hash}:${content}`,
92
+ };
93
+ }
94
+
95
+ const match =
96
+ line.match(CANONICAL_LINE_PATTERN) ?? (allowPiFormat ? line.match(PI_LINE_PATTERN) : null);
97
+ return match ? toParsedDiffLine(match[1] ?? " ", match[2] ?? "", match[3] ?? "") : null;
98
+ }
99
+
100
+ function toNumber(value: string | undefined): number | null {
101
+ if (!value) {
102
+ return null;
103
+ }
104
+ const parsed = Number.parseInt(value, 10);
105
+ return Number.isNaN(parsed) ? null : parsed;
106
+ }
107
+
108
+ function anchorCanonicalLineCursors(
109
+ kind: DiffLineKind,
110
+ parsedNumber: number | null,
111
+ oldLineCursor: number | null,
112
+ newLineCursor: number | null,
113
+ lineNumberDelta: number,
114
+ ): { oldLineCursor: number | null; newLineCursor: number | null } {
115
+ if (parsedNumber === null) {
116
+ return { oldLineCursor, newLineCursor };
117
+ }
118
+
119
+ if (kind === "add") {
120
+ return {
121
+ oldLineCursor,
122
+ newLineCursor: newLineCursor ?? parsedNumber,
123
+ };
124
+ }
125
+
126
+ return {
127
+ oldLineCursor: parsedNumber,
128
+ newLineCursor: parsedNumber + lineNumberDelta,
129
+ };
130
+ }
131
+
132
+ function classifyMetaLine(raw: string): DiffMetaEntry["kind"] {
133
+ if (raw.startsWith("@@")) {
134
+ return "hunk";
135
+ }
136
+ if (
137
+ raw.startsWith("diff --git") ||
138
+ raw.startsWith("index ") ||
139
+ raw.startsWith("--- ") ||
140
+ raw.startsWith("+++ ") ||
141
+ raw.startsWith("rename from ") ||
142
+ raw.startsWith("rename to ") ||
143
+ raw.startsWith("new file mode ") ||
144
+ raw.startsWith("deleted file mode ")
145
+ ) {
146
+ return "file";
147
+ }
148
+ return "meta";
149
+ }
150
+
151
+ function pushParsedLineEntry(
152
+ entries: ParsedDiffEntry[],
153
+ lineKind: DiffLineKind,
154
+ oldLineNumber: number | null,
155
+ newLineNumber: number | null,
156
+ fallbackLineNumber: string,
157
+ rawLine: string,
158
+ hunkIndex: number,
159
+ ): void {
160
+ entries.push({
161
+ kind: "line",
162
+ lineKind,
163
+ oldLineNumber,
164
+ newLineNumber,
165
+ fallbackLineNumber,
166
+ content: rawLine.slice(1),
167
+ raw: rawLine,
168
+ hunkIndex,
169
+ });
170
+ }
171
+
172
+ function createOmissionEntry(raw: string, hunkIndex: number): DiffOmissionEntry {
173
+ return {
174
+ kind: "omission",
175
+ raw,
176
+ hunkIndex,
177
+ };
178
+ }
179
+
180
+ function createMetaEntry(raw: string, hunkIndex: number): DiffMetaEntry {
181
+ return {
182
+ kind: classifyMetaLine(raw),
183
+ raw,
184
+ hunkIndex,
185
+ };
186
+ }
187
+
188
+ function ensureImplicitHunk(currentHunk: number): number {
189
+ return currentHunk > 0 ? currentHunk : 1;
190
+ }
191
+
192
+ export function parseDiff(diffText: string): ParsedDiff {
193
+ const stats: DiffStats = {
194
+ added: 0,
195
+ removed: 0,
196
+ context: 0,
197
+ hunks: 0,
198
+ files: 0,
199
+ lines: 0,
200
+ };
201
+ const entries: ParsedDiffEntry[] = [];
202
+
203
+ if (!diffText.trim()) {
204
+ return { entries, stats };
205
+ }
206
+
207
+ let hunkIndex = 0;
208
+ let oldLineCursor: number | null = null;
209
+ let newLineCursor: number | null = null;
210
+ let lineNumberDelta = 0;
211
+ let hasHunkHeader = false;
212
+
213
+ for (const rawLine of diffText.replace(/\r/g, "").split("\n")) {
214
+ stats.lines++;
215
+
216
+ const hunkMatch = rawLine.match(HUNK_HEADER_PATTERN);
217
+ if (hunkMatch) {
218
+ hasHunkHeader = true;
219
+ hunkIndex++;
220
+ stats.hunks = Math.max(stats.hunks, hunkIndex);
221
+ oldLineCursor = toNumber(hunkMatch[1]);
222
+ newLineCursor = toNumber(hunkMatch[3]);
223
+ lineNumberDelta = (newLineCursor ?? 0) - (oldLineCursor ?? 0);
224
+ entries.push({ kind: "hunk", raw: rawLine, hunkIndex });
225
+ continue;
226
+ }
227
+
228
+ if (rawLine.startsWith("diff --git ")) {
229
+ stats.files++;
230
+ oldLineCursor = null;
231
+ newLineCursor = null;
232
+ lineNumberDelta = 0;
233
+ entries.push({ kind: "file", raw: rawLine, hunkIndex });
234
+ continue;
235
+ }
236
+
237
+ if (rawLine.startsWith("--- ") || rawLine.startsWith("+++ ")) {
238
+ oldLineCursor = null;
239
+ newLineCursor = null;
240
+ lineNumberDelta = 0;
241
+ }
242
+
243
+ // Pi pads omitted context with a blank line number; it is not a source row.
244
+ if (!hasHunkHeader && PI_OMISSION_LINE_PATTERN.test(rawLine)) {
245
+ entries.push(createOmissionEntry(rawLine, hunkIndex));
246
+ continue;
247
+ }
248
+
249
+ // Pi's headerless format is ambiguous with numeric source text in unified hunks.
250
+ const canonical = parseCanonicalDiffLine(rawLine, !hasHunkHeader);
251
+ if (canonical) {
252
+ hunkIndex = ensureImplicitHunk(hunkIndex);
253
+ stats.hunks = Math.max(stats.hunks, hunkIndex);
254
+
255
+ const parsedNumber = toNumber(canonical.lineNumber);
256
+ const anchoredCursors = anchorCanonicalLineCursors(
257
+ canonical.lineKind,
258
+ parsedNumber,
259
+ oldLineCursor,
260
+ newLineCursor,
261
+ lineNumberDelta,
262
+ );
263
+ oldLineCursor = anchoredCursors.oldLineCursor;
264
+ newLineCursor = anchoredCursors.newLineCursor;
265
+
266
+ const oldLineNumber = canonical.lineKind === "add" ? null : oldLineCursor;
267
+ const newLineNumber = canonical.lineKind === "remove" ? null : newLineCursor;
268
+
269
+ if (canonical.lineKind === "add") {
270
+ stats.added++;
271
+ if (newLineCursor !== null) {
272
+ newLineCursor++;
273
+ }
274
+ lineNumberDelta++;
275
+ } else if (canonical.lineKind === "remove") {
276
+ stats.removed++;
277
+ if (oldLineCursor !== null) {
278
+ oldLineCursor++;
279
+ }
280
+ lineNumberDelta--;
281
+ } else {
282
+ stats.context++;
283
+ if (oldLineCursor !== null) {
284
+ oldLineCursor++;
285
+ }
286
+ if (newLineCursor !== null) {
287
+ newLineCursor++;
288
+ }
289
+ }
290
+
291
+ entries.push({
292
+ kind: "line",
293
+ lineKind: canonical.lineKind,
294
+ oldLineNumber,
295
+ newLineNumber,
296
+ fallbackLineNumber: canonical.lineNumber,
297
+ content: canonical.content,
298
+ hashlineAnchorContent: canonical.hashlineAnchorContent,
299
+ raw: rawLine,
300
+ hunkIndex,
301
+ });
302
+ continue;
303
+ }
304
+
305
+ if (rawLine.startsWith("-") && !rawLine.startsWith("---")) {
306
+ hunkIndex = ensureImplicitHunk(hunkIndex);
307
+ stats.hunks = Math.max(stats.hunks, hunkIndex);
308
+ stats.removed++;
309
+ const oldLineNumber = oldLineCursor;
310
+ if (oldLineCursor !== null) {
311
+ oldLineCursor++;
312
+ }
313
+ lineNumberDelta--;
314
+ pushParsedLineEntry(
315
+ entries,
316
+ "remove",
317
+ oldLineNumber,
318
+ null,
319
+ oldLineNumber !== null ? `${oldLineNumber}` : "",
320
+ rawLine,
321
+ hunkIndex,
322
+ );
323
+ continue;
324
+ }
325
+
326
+ if (rawLine.startsWith("+") && !rawLine.startsWith("+++")) {
327
+ hunkIndex = ensureImplicitHunk(hunkIndex);
328
+ stats.hunks = Math.max(stats.hunks, hunkIndex);
329
+ stats.added++;
330
+ const newLineNumber = newLineCursor;
331
+ if (newLineCursor !== null) {
332
+ newLineCursor++;
333
+ }
334
+ lineNumberDelta++;
335
+ pushParsedLineEntry(
336
+ entries,
337
+ "add",
338
+ null,
339
+ newLineNumber,
340
+ newLineNumber !== null ? `${newLineNumber}` : "",
341
+ rawLine,
342
+ hunkIndex,
343
+ );
344
+ continue;
345
+ }
346
+
347
+ if (rawLine.startsWith(" ")) {
348
+ hunkIndex = ensureImplicitHunk(hunkIndex);
349
+ stats.hunks = Math.max(stats.hunks, hunkIndex);
350
+ stats.context++;
351
+ const oldLineNumber = oldLineCursor;
352
+ const newLineNumber = newLineCursor;
353
+ if (oldLineCursor !== null) {
354
+ oldLineCursor++;
355
+ }
356
+ if (newLineCursor !== null) {
357
+ newLineCursor++;
358
+ }
359
+ pushParsedLineEntry(
360
+ entries,
361
+ "context",
362
+ oldLineNumber,
363
+ newLineNumber,
364
+ oldLineNumber !== null
365
+ ? `${oldLineNumber}`
366
+ : newLineNumber !== null
367
+ ? `${newLineNumber}`
368
+ : "",
369
+ rawLine,
370
+ hunkIndex,
371
+ );
372
+ continue;
373
+ }
374
+
375
+ entries.push(createMetaEntry(rawLine, hunkIndex));
376
+ }
377
+
378
+ if (stats.hunks === 0 && (stats.added > 0 || stats.removed > 0 || stats.context > 0)) {
379
+ stats.hunks = 1;
380
+ }
381
+ if (stats.files === 0) {
382
+ const patchStyleFileHeaders = entries.filter(
383
+ (entry) => entry.kind === "file" && entry.raw.startsWith("+++ "),
384
+ ).length;
385
+ if (patchStyleFileHeaders > 0) {
386
+ stats.files = patchStyleFileHeaders;
387
+ } else if (stats.hunks > 0) {
388
+ stats.files = 1;
389
+ }
390
+ }
391
+
392
+ return { entries, stats };
393
+ }
394
+
395
+ export function getHashlineAnchorLabel(entry: DiffLineEntry): string | undefined {
396
+ if (!entry.hashlineAnchorContent) {
397
+ return undefined;
398
+ }
399
+ const separatorIndex = entry.hashlineAnchorContent.indexOf(":");
400
+ return separatorIndex >= 0
401
+ ? entry.hashlineAnchorContent.slice(0, separatorIndex)
402
+ : entry.hashlineAnchorContent;
403
+ }
404
+
405
+ export function getLineNumberWidth(
406
+ entries: ParsedDiffEntry[],
407
+ showHashlineAnchors = false,
408
+ ): number {
409
+ let maxWidth = MIN_LINE_NUMBER_WIDTH;
410
+
411
+ for (const entry of entries) {
412
+ if (entry.kind !== "line") {
413
+ continue;
414
+ }
415
+
416
+ if (showHashlineAnchors) {
417
+ const anchorLabel = getHashlineAnchorLabel(entry);
418
+ if (anchorLabel) {
419
+ maxWidth = Math.max(maxWidth, visibleWidth(anchorLabel));
420
+ continue;
421
+ }
422
+ }
423
+
424
+ const candidates = [
425
+ entry.oldLineNumber,
426
+ entry.newLineNumber,
427
+ toNumber(entry.fallbackLineNumber),
428
+ ].filter((value): value is number => value !== null);
429
+
430
+ for (const candidate of candidates) {
431
+ const digits = `${candidate}`.length;
432
+ if (digits > maxWidth) {
433
+ maxWidth = digits;
434
+ }
435
+ }
436
+ }
437
+
438
+ return maxWidth;
439
+ }
440
+
441
+ function formatLineNumber(value: number | null, fallback: string, width: number): string {
442
+ if (value !== null) {
443
+ return `${value}`.padStart(width, " ");
444
+ }
445
+ if (fallback.trim()) {
446
+ return fallback.trim().slice(-width).padStart(width, " ");
447
+ }
448
+ return " ".repeat(width);
449
+ }
450
+
451
+ export function formatLineNumberLabel(
452
+ entry: DiffLineEntry,
453
+ value: number | null,
454
+ fallback: string,
455
+ width: number,
456
+ showHashlineAnchors: boolean,
457
+ ): string {
458
+ const anchorLabel = showHashlineAnchors ? getHashlineAnchorLabel(entry) : undefined;
459
+ if (anchorLabel) {
460
+ return fitToWidth(anchorLabel, width);
461
+ }
462
+ return formatLineNumber(value, fallback, width);
463
+ }
464
+
465
+ export function getCompactLineRenderContent(
466
+ entry: DiffLineEntry,
467
+ showHashlineAnchors: boolean,
468
+ ): string {
469
+ return showHashlineAnchors && entry.hashlineAnchorContent
470
+ ? entry.hashlineAnchorContent
471
+ : entry.content;
472
+ }
473
+
474
+ export function countDiffLineEntries(entries: readonly ParsedDiffEntry[]): number {
475
+ let count = 0;
476
+ for (const entry of entries) {
477
+ if (entry.kind === "line") count++;
478
+ }
479
+ return count;
480
+ }
481
+
482
+ export function collectDiffStats(
483
+ entries: ParsedDiffEntry[],
484
+ fallbackHunks = 0,
485
+ fallbackFiles = 0,
486
+ ): DiffStats {
487
+ const stats: DiffStats = {
488
+ added: 0,
489
+ removed: 0,
490
+ context: 0,
491
+ hunks: fallbackHunks,
492
+ files: fallbackFiles,
493
+ lines: entries.length,
494
+ };
495
+
496
+ const hunkIndexes = new Set<number>();
497
+ let explicitFileCount = 0;
498
+
499
+ for (const entry of entries) {
500
+ if (entry.kind === "line") {
501
+ if (entry.lineKind === "add") {
502
+ stats.added++;
503
+ } else if (entry.lineKind === "remove") {
504
+ stats.removed++;
505
+ } else {
506
+ stats.context++;
507
+ }
508
+ if (entry.hunkIndex > 0) {
509
+ hunkIndexes.add(entry.hunkIndex);
510
+ }
511
+ continue;
512
+ }
513
+
514
+ if (entry.kind === "hunk" && entry.hunkIndex > 0) {
515
+ hunkIndexes.add(entry.hunkIndex);
516
+ }
517
+ if (entry.kind === "file") {
518
+ explicitFileCount++;
519
+ }
520
+ }
521
+
522
+ if (hunkIndexes.size > 0) {
523
+ stats.hunks = Math.max(stats.hunks, hunkIndexes.size);
524
+ }
525
+ if (explicitFileCount > 0) {
526
+ stats.files = Math.max(stats.files, explicitFileCount);
527
+ } else if (entries.length > 0) {
528
+ stats.files = Math.max(stats.files, 1);
529
+ }
530
+ if (stats.hunks === 0 && entries.some((entry) => entry.kind === "line")) {
531
+ stats.hunks = 1;
532
+ }
533
+
534
+ return stats;
535
+ }
@@ -1,6 +1,6 @@
1
1
  import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
2
- import { pluralize } from "./render-utils.ts";
3
- import type { ToolDisplayConfig } from "./types.ts";
2
+ import { pluralize } from "./diff-text.ts";
3
+ import type { ToolDisplayConfig } from "../../../config/config.ts";
4
4
 
5
5
  export interface DiffSummaryStats {
6
6
  added: number;