@herbertgao/pi-extensions 2026.9.19 → 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 (186) hide show
  1. package/README.md +1 -8
  2. package/node_modules/@herbertgao/pi-cc-extensions/README.en.md +76 -59
  3. package/node_modules/@herbertgao/pi-cc-extensions/README.md +76 -59
  4. package/node_modules/@herbertgao/pi-cc-extensions/assets/readme/preview.webp +0 -0
  5. package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/config.ts +93 -34
  6. package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/panel.ts +346 -117
  7. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/agent-summary/core.ts +6 -36
  8. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/agent-summary/index.ts +2 -2
  9. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/compact-thinking.ts +24 -29
  10. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/context.ts +330 -360
  11. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/reference/index.ts +56 -108
  12. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/reference/session.ts +97 -15
  13. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/reference/subagent.ts +80 -45
  14. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/shell/flush-docked-bash.ts +36 -0
  15. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/shell/footer-layout.ts +229 -0
  16. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/shell/footer.ts +479 -0
  17. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/shell/startup-header.ts +92 -61
  18. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/shell/working-message.ts +58 -38
  19. package/node_modules/@herbertgao/pi-cc-extensions/extensions/index.ts +19 -3
  20. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/compact-mode.ts +1080 -229
  21. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/default-mode.ts +232 -259
  22. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/index.ts +38 -48
  23. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/markdown-enhance.ts +8 -2
  24. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/hover.ts +2 -7
  25. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/interaction.ts +277 -113
  26. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/layout.ts +15 -6
  27. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/packets.ts +18 -72
  28. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/scroll.ts +61 -26
  29. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/ansi-utils.ts +25 -57
  30. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-component.ts +125 -0
  31. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-edit-render.ts +227 -0
  32. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-header.ts +136 -0
  33. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-highlight.ts +115 -0
  34. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-inline.ts +324 -0
  35. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-layout.ts +762 -0
  36. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-limits.ts +152 -0
  37. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-palette.ts +410 -0
  38. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-parse.ts +535 -0
  39. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-presentation.ts +2 -2
  40. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-renderer.ts +21 -3219
  41. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-text.ts +44 -0
  42. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-write-render.ts +568 -0
  43. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/index.ts +19 -18
  44. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/line-width-safety.ts +2 -2
  45. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/write-execution.ts +1 -0
  46. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/grouping.ts +142 -192
  47. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/mcp-title.ts +39 -0
  48. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/{message-display.ts → tool/message-display.ts} +24 -90
  49. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/names.ts +426 -0
  50. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/result.ts +90 -132
  51. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/{show-more-hint.ts → tool/show-more-hint.ts} +10 -0
  52. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/transcript-refresh.ts +8 -26
  53. package/node_modules/@herbertgao/pi-cc-extensions/extensions/utils/ansi-color.ts +44 -0
  54. package/node_modules/@herbertgao/pi-cc-extensions/extensions/utils/ansi-text.ts +76 -0
  55. package/node_modules/@herbertgao/pi-cc-extensions/extensions/utils/component-tree.ts +34 -0
  56. package/node_modules/@herbertgao/pi-cc-extensions/extensions/utils/format.ts +28 -0
  57. package/node_modules/@herbertgao/pi-cc-extensions/extensions/utils/fullscreen-detect.ts +51 -0
  58. package/node_modules/@herbertgao/pi-cc-extensions/extensions/utils/patch-keys.ts +124 -0
  59. package/node_modules/@herbertgao/pi-cc-extensions/extensions/utils/sgr-mouse.ts +70 -0
  60. package/node_modules/@herbertgao/pi-cc-extensions/package.json +7 -7
  61. package/package.json +3 -7
  62. package/node_modules/@herbertgao/pi-cc-extensions/assets/readme/hero.en.svg +0 -101
  63. package/node_modules/@herbertgao/pi-cc-extensions/assets/readme/hero.svg +0 -101
  64. package/node_modules/@herbertgao/pi-cc-extensions/assets/readme/welcome.webp +0 -0
  65. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/render-utils.ts +0 -5
  66. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/types.ts +0 -30
  67. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/write-display-utils.ts +0 -12
  68. package/node_modules/pi-footer/CHANGELOG.md +0 -145
  69. package/node_modules/pi-footer/LICENSE +0 -21
  70. package/node_modules/pi-footer/README.md +0 -354
  71. package/node_modules/pi-footer/package.json +0 -68
  72. package/node_modules/pi-footer/src/cache.ts +0 -106
  73. package/node_modules/pi-footer/src/colors.ts +0 -237
  74. package/node_modules/pi-footer/src/config.ts +0 -202
  75. package/node_modules/pi-footer/src/event-widgets.ts +0 -41
  76. package/node_modules/pi-footer/src/extension-statuses.ts +0 -102
  77. package/node_modules/pi-footer/src/git.ts +0 -135
  78. package/node_modules/pi-footer/src/index.ts +0 -268
  79. package/node_modules/pi-footer/src/metrics.ts +0 -141
  80. package/node_modules/pi-footer/src/presets.ts +0 -383
  81. package/node_modules/pi-footer/src/render.ts +0 -114
  82. package/node_modules/pi-footer/src/separators.ts +0 -52
  83. package/node_modules/pi-footer/src/types.ts +0 -122
  84. package/node_modules/pi-footer/src/ui/color-level-confirm.ts +0 -22
  85. package/node_modules/pi-footer/src/ui/color-options.ts +0 -195
  86. package/node_modules/pi-footer/src/ui/config-lifecycle.ts +0 -61
  87. package/node_modules/pi-footer/src/ui/edit-colors.ts +0 -13
  88. package/node_modules/pi-footer/src/ui/events.ts +0 -35
  89. package/node_modules/pi-footer/src/ui/extension-status-picker.ts +0 -60
  90. package/node_modules/pi-footer/src/ui/extension-statuses.ts +0 -73
  91. package/node_modules/pi-footer/src/ui/fields.ts +0 -252
  92. package/node_modules/pi-footer/src/ui/global-menu.ts +0 -126
  93. package/node_modules/pi-footer/src/ui/helpers.ts +0 -44
  94. package/node_modules/pi-footer/src/ui/layout.ts +0 -17
  95. package/node_modules/pi-footer/src/ui/line-list.ts +0 -19
  96. package/node_modules/pi-footer/src/ui/model.ts +0 -64
  97. package/node_modules/pi-footer/src/ui/navigation.ts +0 -29
  98. package/node_modules/pi-footer/src/ui/option-edit.ts +0 -93
  99. package/node_modules/pi-footer/src/ui/overlay-render.ts +0 -103
  100. package/node_modules/pi-footer/src/ui/screen-context.ts +0 -18
  101. package/node_modules/pi-footer/src/ui/screen-controller.ts +0 -27
  102. package/node_modules/pi-footer/src/ui/screen-render.ts +0 -32
  103. package/node_modules/pi-footer/src/ui/screen-state.ts +0 -20
  104. package/node_modules/pi-footer/src/ui/screen.ts +0 -243
  105. package/node_modules/pi-footer/src/ui/screens/add-widget.ts +0 -92
  106. package/node_modules/pi-footer/src/ui/screens/confirm-exit.ts +0 -52
  107. package/node_modules/pi-footer/src/ui/screens/controller.ts +0 -12
  108. package/node_modules/pi-footer/src/ui/screens/edit-colors.ts +0 -68
  109. package/node_modules/pi-footer/src/ui/screens/edit-widget.ts +0 -115
  110. package/node_modules/pi-footer/src/ui/screens/extension-status-row.ts +0 -61
  111. package/node_modules/pi-footer/src/ui/screens/global.ts +0 -75
  112. package/node_modules/pi-footer/src/ui/screens/line-list.ts +0 -108
  113. package/node_modules/pi-footer/src/ui/screens/main.ts +0 -99
  114. package/node_modules/pi-footer/src/ui/screens/terminal.ts +0 -121
  115. package/node_modules/pi-footer/src/ui/screens/widget-list.ts +0 -154
  116. package/node_modules/pi-footer/src/ui/terminal-menu.ts +0 -32
  117. package/node_modules/pi-footer/src/ui/theme.ts +0 -48
  118. package/node_modules/pi-footer/src/ui/title-bar.ts +0 -100
  119. package/node_modules/pi-footer/src/ui/widget-actions.ts +0 -76
  120. package/node_modules/pi-footer/src/ui.ts +0 -66
  121. package/node_modules/pi-footer/src/widgets/context.ts +0 -28
  122. package/node_modules/pi-footer/src/widgets/core/active-tools.ts +0 -17
  123. package/node_modules/pi-footer/src/widgets/core/context-window.ts +0 -24
  124. package/node_modules/pi-footer/src/widgets/core/cwd-basename.ts +0 -19
  125. package/node_modules/pi-footer/src/widgets/core/cwd.ts +0 -108
  126. package/node_modules/pi-footer/src/widgets/core/event.ts +0 -34
  127. package/node_modules/pi-footer/src/widgets/core/external-status.ts +0 -102
  128. package/node_modules/pi-footer/src/widgets/core/model-provider.ts +0 -20
  129. package/node_modules/pi-footer/src/widgets/core/model.ts +0 -35
  130. package/node_modules/pi-footer/src/widgets/core/provider.ts +0 -17
  131. package/node_modules/pi-footer/src/widgets/core/session-name.ts +0 -17
  132. package/node_modules/pi-footer/src/widgets/core/text-verbosity.ts +0 -17
  133. package/node_modules/pi-footer/src/widgets/core/thinking-level.ts +0 -17
  134. package/node_modules/pi-footer/src/widgets/git/ahead-behind.ts +0 -17
  135. package/node_modules/pi-footer/src/widgets/git/branch.ts +0 -81
  136. package/node_modules/pi-footer/src/widgets/git/clean.ts +0 -19
  137. package/node_modules/pi-footer/src/widgets/git/deletions.ts +0 -17
  138. package/node_modules/pi-footer/src/widgets/git/diff.ts +0 -38
  139. package/node_modules/pi-footer/src/widgets/git/insertions.ts +0 -17
  140. package/node_modules/pi-footer/src/widgets/git/remote.ts +0 -17
  141. package/node_modules/pi-footer/src/widgets/git/root.ts +0 -17
  142. package/node_modules/pi-footer/src/widgets/git/sha.ts +0 -17
  143. package/node_modules/pi-footer/src/widgets/git/staged.ts +0 -17
  144. package/node_modules/pi-footer/src/widgets/git/status.ts +0 -19
  145. package/node_modules/pi-footer/src/widgets/git/unstaged.ts +0 -17
  146. package/node_modules/pi-footer/src/widgets/git/untracked.ts +0 -17
  147. package/node_modules/pi-footer/src/widgets/instance.ts +0 -118
  148. package/node_modules/pi-footer/src/widgets/layout/custom-text.ts +0 -18
  149. package/node_modules/pi-footer/src/widgets/layout/flex-separator.ts +0 -32
  150. package/node_modules/pi-footer/src/widgets/layout/separator.ts +0 -57
  151. package/node_modules/pi-footer/src/widgets/layout/spacer.ts +0 -31
  152. package/node_modules/pi-footer/src/widgets/options.ts +0 -122
  153. package/node_modules/pi-footer/src/widgets/project/runtime.ts +0 -361
  154. package/node_modules/pi-footer/src/widgets/registry.ts +0 -223
  155. package/node_modules/pi-footer/src/widgets/session/assistant-messages.ts +0 -17
  156. package/node_modules/pi-footer/src/widgets/session/compactions.ts +0 -17
  157. package/node_modules/pi-footer/src/widgets/session/elapsed.ts +0 -18
  158. package/node_modules/pi-footer/src/widgets/session/last-activity.ts +0 -18
  159. package/node_modules/pi-footer/src/widgets/session/messages.ts +0 -19
  160. package/node_modules/pi-footer/src/widgets/session/session-id.ts +0 -17
  161. package/node_modules/pi-footer/src/widgets/session/session-start.ts +0 -18
  162. package/node_modules/pi-footer/src/widgets/session/tool-results.ts +0 -17
  163. package/node_modules/pi-footer/src/widgets/session/total-messages.ts +0 -19
  164. package/node_modules/pi-footer/src/widgets/session/total-time.ts +0 -18
  165. package/node_modules/pi-footer/src/widgets/session/user-messages.ts +0 -17
  166. package/node_modules/pi-footer/src/widgets/store.ts +0 -26
  167. package/node_modules/pi-footer/src/widgets/tokens/cache-hit-rate.ts +0 -55
  168. package/node_modules/pi-footer/src/widgets/tokens/cache-read.ts +0 -18
  169. package/node_modules/pi-footer/src/widgets/tokens/cache-write.ts +0 -18
  170. package/node_modules/pi-footer/src/widgets/tokens/context-bar.ts +0 -108
  171. package/node_modules/pi-footer/src/widgets/tokens/context-length.ts +0 -26
  172. package/node_modules/pi-footer/src/widgets/tokens/context-remaining.ts +0 -29
  173. package/node_modules/pi-footer/src/widgets/tokens/context.ts +0 -29
  174. package/node_modules/pi-footer/src/widgets/tokens/cost.ts +0 -51
  175. package/node_modules/pi-footer/src/widgets/tokens/input-speed.ts +0 -25
  176. package/node_modules/pi-footer/src/widgets/tokens/input-tokens.ts +0 -18
  177. package/node_modules/pi-footer/src/widgets/tokens/output-speed.ts +0 -25
  178. package/node_modules/pi-footer/src/widgets/tokens/output-tokens.ts +0 -18
  179. package/node_modules/pi-footer/src/widgets/tokens/tokens.ts +0 -20
  180. package/node_modules/pi-footer/src/widgets/tokens/total-speed.ts +0 -25
  181. package/node_modules/pi-footer/src/widgets/tokens/total-tokens.ts +0 -18
  182. package/node_modules/pi-footer/src/widgets/types.ts +0 -181
  183. package/node_modules/pi-footer/src/widgets/utils/colors.ts +0 -11
  184. package/node_modules/pi-footer/src/widgets/utils/context.ts +0 -112
  185. package/node_modules/pi-footer/src/widgets/utils/session.ts +0 -19
  186. package/node_modules/pi-footer/src/widgets/utils/token-format.ts +0 -78
@@ -0,0 +1,762 @@
1
+ import { visibleWidth } from "@earendil-works/pi-tui";
2
+ import { fitToWidth, normalizeCodeWhitespace, wrapToWidth } from "./diff-text.ts";
3
+ import {
4
+ ANSI_BG_RESET,
5
+ applyLineBackgroundToWrappedRows,
6
+ colorizeSegment,
7
+ readThemeAnsi,
8
+ stabilizeBackgroundResets,
9
+ type DiffPalette,
10
+ type DiffTheme,
11
+ } from "./diff-palette.ts";
12
+ import { highlightDiffLine, type CodeLineHighlighter } from "./diff-highlight.ts";
13
+ import {
14
+ formatLineNumberLabel,
15
+ getCompactLineRenderContent,
16
+ type DiffLineEntry,
17
+ type DiffLineKind,
18
+ type DiffMetaEntry,
19
+ type DiffOmissionEntry,
20
+ type ParsedDiffEntry,
21
+ } from "./diff-parse.ts";
22
+ import { getCellLineNumber, type DiffSpan, type SplitDiffRow } from "./diff-inline.ts";
23
+ import { formatMetaEntryRows } from "./diff-header.ts";
24
+ import type { DiffIndicatorMode } from "../../../config/config.ts";
25
+
26
+ export interface RenderedRow {
27
+ text: string;
28
+ hunkIndex: number | null;
29
+ }
30
+
31
+ type MaterializedDiffEntry = DiffLineEntry | DiffMetaEntry;
32
+
33
+ interface LineCellRenderParams {
34
+ kind: DiffLineKind;
35
+ code: string;
36
+ width: number;
37
+ rowBg: string | undefined;
38
+ restoreBgAnsi: string | undefined;
39
+ theme: DiffTheme;
40
+ wordWrap: boolean;
41
+ indicatorMode: DiffIndicatorMode;
42
+ }
43
+
44
+ export interface DiffRenderContext {
45
+ width: number;
46
+ theme: DiffTheme;
47
+ inlineHighlights: WeakMap<DiffLineEntry, DiffSpan[]>;
48
+ palette: DiffPalette;
49
+ highlightLine: CodeLineHighlighter;
50
+ containerBgAnsi: string | undefined;
51
+ wordWrap: boolean;
52
+ indicatorMode: DiffIndicatorMode;
53
+ showHashlineAnchors: boolean;
54
+ }
55
+
56
+ const SPLIT_SEPARATOR = " │ ";
57
+ const OMISSION_GLYPH = "⋮";
58
+ const MIN_SPLIT_COLUMN_WIDTH = 24;
59
+
60
+ function resolveIndicatorGlyph(
61
+ kind: DiffLineKind,
62
+ indicatorMode: DiffIndicatorMode,
63
+ continuation: boolean,
64
+ ): string {
65
+ if (kind === "context") {
66
+ return " ";
67
+ }
68
+
69
+ switch (indicatorMode) {
70
+ case "bars":
71
+ return "▌";
72
+ case "classic":
73
+ if (continuation) {
74
+ return " ";
75
+ }
76
+ return kind === "add" ? "+" : "-";
77
+ case "none":
78
+ default:
79
+ return " ";
80
+ }
81
+ }
82
+
83
+ function renderChangeMarker(
84
+ kind: DiffLineKind,
85
+ theme: DiffTheme,
86
+ rowBg: string | undefined,
87
+ indicatorMode: DiffIndicatorMode,
88
+ continuation = false,
89
+ ): string {
90
+ const glyph = resolveIndicatorGlyph(kind, indicatorMode, continuation);
91
+ if (glyph === " ") {
92
+ return rowBg ? `${rowBg} ${rowBg}` : " ";
93
+ }
94
+ if (kind === "add") {
95
+ return colorizeSegment(theme, "toolDiffAdded", glyph, rowBg);
96
+ }
97
+ if (kind === "remove") {
98
+ return colorizeSegment(theme, "toolDiffRemoved", glyph, rowBg);
99
+ }
100
+ return colorizeSegment(theme, "dim", glyph, rowBg);
101
+ }
102
+
103
+ function usesHashlineGutter(showHashlineAnchors: boolean): boolean {
104
+ return showHashlineAnchors;
105
+ }
106
+
107
+ function getHashlineGutterMarkerWidth(_indicatorMode: DiffIndicatorMode): number {
108
+ return 0;
109
+ }
110
+
111
+ function getLineDividerPlainWidth(
112
+ indicatorMode: DiffIndicatorMode,
113
+ hashlineGutter = false,
114
+ ): number {
115
+ if (hashlineGutter) {
116
+ return 2;
117
+ }
118
+ return indicatorMode === "classic" ? 1 : 2;
119
+ }
120
+
121
+ function renderCodeDivider(
122
+ theme: DiffTheme,
123
+ rowBg: string | undefined,
124
+ indicatorMode: DiffIndicatorMode,
125
+ hashlineGutter = false,
126
+ ): string {
127
+ return colorizeSegment(
128
+ theme,
129
+ "dim",
130
+ hashlineGutter || indicatorMode !== "classic" ? "│ " : "│",
131
+ rowBg,
132
+ );
133
+ }
134
+
135
+ function getLineNumberColor(kind: DiffLineKind): "dim" | "toolDiffAdded" | "toolDiffRemoved" {
136
+ if (kind === "add") {
137
+ return "toolDiffAdded";
138
+ }
139
+ if (kind === "remove") {
140
+ return "toolDiffRemoved";
141
+ }
142
+ return "dim";
143
+ }
144
+
145
+ function renderLineNumberSegment(
146
+ kind: DiffLineKind,
147
+ lineNumber: string,
148
+ theme: DiffTheme,
149
+ rowBg: string | undefined,
150
+ ): string {
151
+ return colorizeSegment(theme, getLineNumberColor(kind), lineNumber, rowBg);
152
+ }
153
+
154
+ function getLinePrefixPlainWidth(
155
+ lineNumberWidth: number,
156
+ indicatorMode: DiffIndicatorMode,
157
+ hashlineGutter = false,
158
+ ): number {
159
+ if (hashlineGutter) {
160
+ return getHashlineGutterMarkerWidth(indicatorMode) + lineNumberWidth;
161
+ }
162
+ return indicatorMode === "bars"
163
+ ? visibleWidth(`▌ ${" ".repeat(lineNumberWidth)} `)
164
+ : visibleWidth(`${" ".repeat(lineNumberWidth)} `);
165
+ }
166
+
167
+ function getLineContentIndicatorPrefixPlainWidth(indicatorMode: DiffIndicatorMode): number {
168
+ return indicatorMode === "classic" ? 2 : 0;
169
+ }
170
+
171
+ function renderClassicContentPrefix(
172
+ kind: DiffLineKind,
173
+ theme: DiffTheme,
174
+ rowBg: string | undefined,
175
+ continuation = false,
176
+ ): string {
177
+ if (kind === "context" || continuation) {
178
+ return rowBg ? `${rowBg} ${rowBg}` : " ";
179
+ }
180
+
181
+ const glyph = kind === "add" ? "+" : "-";
182
+ const glyphColor = kind === "add" ? "toolDiffAdded" : "toolDiffRemoved";
183
+ const spacer = rowBg ? `${rowBg} ` : " ";
184
+ return `${colorizeSegment(theme, glyphColor, glyph, rowBg)}${spacer}`;
185
+ }
186
+
187
+ function renderLinePrefix(
188
+ kind: DiffLineKind,
189
+ lineNumber: string,
190
+ theme: DiffTheme,
191
+ rowBg: string | undefined,
192
+ indicatorMode: DiffIndicatorMode,
193
+ continuation = false,
194
+ hashlineGutter = false,
195
+ ): string {
196
+ const number = renderLineNumberSegment(kind, lineNumber, theme, rowBg);
197
+ if (hashlineGutter) {
198
+ return number;
199
+ }
200
+ const spacer = rowBg ? `${rowBg} ` : " ";
201
+ if (indicatorMode !== "bars") {
202
+ return `${number}${spacer}`;
203
+ }
204
+ const marker = renderChangeMarker(kind, theme, rowBg, indicatorMode, continuation);
205
+ return `${marker}${spacer}${number}${spacer}`;
206
+ }
207
+
208
+ function renderLineContinuationPrefix(
209
+ kind: DiffLineKind,
210
+ lineNumberWidth: number,
211
+ rowBg: string | undefined,
212
+ theme: DiffTheme,
213
+ indicatorMode: DiffIndicatorMode,
214
+ hashlineGutter = false,
215
+ ): string {
216
+ const blankLineNumber = " ".repeat(lineNumberWidth);
217
+ return renderLinePrefix(kind, blankLineNumber, theme, rowBg, indicatorMode, true, hashlineGutter);
218
+ }
219
+
220
+ function renderLineContentIndicatorPrefix(
221
+ kind: DiffLineKind,
222
+ theme: DiffTheme,
223
+ rowBg: string | undefined,
224
+ indicatorMode: DiffIndicatorMode,
225
+ continuation = false,
226
+ ): string {
227
+ return indicatorMode === "classic"
228
+ ? renderClassicContentPrefix(kind, theme, rowBg, continuation)
229
+ : "";
230
+ }
231
+
232
+ function renderCompactLinePrefix(
233
+ kind: DiffLineKind,
234
+ theme: DiffTheme,
235
+ rowBg: string | undefined,
236
+ indicatorMode: DiffIndicatorMode,
237
+ continuation = false,
238
+ ): string {
239
+ const marker = renderChangeMarker(kind, theme, rowBg, indicatorMode, continuation);
240
+ const spacer = rowBg ? `${rowBg} ` : " ";
241
+ return `${marker}${spacer}`;
242
+ }
243
+
244
+ function renderWrappedRowsWithOptionalBackground(
245
+ wrappedCodeLines: string[],
246
+ buildRow: (index: number, wrappedCodeLine: string) => string,
247
+ width: number,
248
+ rowBg: string | undefined,
249
+ restoreBgAnsi: string | undefined,
250
+ ): string[] {
251
+ if (!rowBg) {
252
+ return wrappedCodeLines.map((wrappedCodeLine, index) =>
253
+ stabilizeBackgroundResets(buildRow(index, wrappedCodeLine)),
254
+ );
255
+ }
256
+ const safeRestoreBgAnsi = restoreBgAnsi ?? rowBg ?? ANSI_BG_RESET;
257
+ const visualRows = wrappedCodeLines.map((wrappedCodeLine, index) =>
258
+ buildRow(index, wrappedCodeLine),
259
+ );
260
+ return applyLineBackgroundToWrappedRows(visualRows, width, rowBg, safeRestoreBgAnsi);
261
+ }
262
+
263
+ function computeLineCellCodeWidth(
264
+ width: number,
265
+ lineNumberWidth: number,
266
+ indicatorMode: DiffIndicatorMode,
267
+ hashlineGutter: boolean,
268
+ ): number {
269
+ const prefixPlainWidth = getLinePrefixPlainWidth(lineNumberWidth, indicatorMode, hashlineGutter);
270
+ const dividerPlainWidth = getLineDividerPlainWidth(indicatorMode, hashlineGutter);
271
+ const contentIndicatorWidth = hashlineGutter
272
+ ? 0
273
+ : getLineContentIndicatorPrefixPlainWidth(indicatorMode);
274
+ return Math.max(0, width - prefixPlainWidth - dividerPlainWidth - contentIndicatorWidth);
275
+ }
276
+
277
+ function buildLineCellParams(
278
+ kind: DiffLineKind,
279
+ code: string,
280
+ width: number,
281
+ rowBg: string | undefined,
282
+ restoreBgAnsi: string | undefined,
283
+ theme: DiffTheme,
284
+ wordWrap: boolean,
285
+ indicatorMode: DiffIndicatorMode,
286
+ ): LineCellRenderParams {
287
+ return { kind, code, width, rowBg, restoreBgAnsi, theme, wordWrap, indicatorMode };
288
+ }
289
+
290
+ function renderCompactLineCell({
291
+ kind,
292
+ code,
293
+ width,
294
+ rowBg,
295
+ restoreBgAnsi,
296
+ theme,
297
+ wordWrap,
298
+ indicatorMode,
299
+ }: LineCellRenderParams): string[] {
300
+ if (width <= 0) {
301
+ return [""];
302
+ }
303
+
304
+ const prefix = renderCompactLinePrefix(kind, theme, undefined, indicatorMode);
305
+ const continuationPrefix = renderCompactLinePrefix(kind, theme, undefined, indicatorMode, true);
306
+ const prefixPlainWidth = 2;
307
+ const codeWidth = Math.max(0, width - prefixPlainWidth);
308
+ const wrappedCodeLines = wrapToWidth(code, codeWidth, wordWrap);
309
+ return renderWrappedRowsWithOptionalBackground(
310
+ wrappedCodeLines,
311
+ (index, line) => `${index === 0 ? prefix : continuationPrefix}${line}`,
312
+ width,
313
+ rowBg,
314
+ restoreBgAnsi,
315
+ );
316
+ }
317
+
318
+ function renderLineCell(
319
+ { kind, code, width, rowBg, restoreBgAnsi, theme, wordWrap, indicatorMode }: LineCellRenderParams,
320
+ lineNumber: string,
321
+ hashlineGutter = false,
322
+ ): string[] {
323
+ if (width <= 0) {
324
+ return [""];
325
+ }
326
+
327
+ const codeWidth = computeLineCellCodeWidth(
328
+ width,
329
+ lineNumber.length,
330
+ indicatorMode,
331
+ hashlineGutter,
332
+ );
333
+ const prefix = renderLinePrefix(
334
+ kind,
335
+ lineNumber,
336
+ theme,
337
+ undefined,
338
+ indicatorMode,
339
+ false,
340
+ hashlineGutter,
341
+ );
342
+ const continuationPrefix = renderLineContinuationPrefix(
343
+ kind,
344
+ lineNumber.length,
345
+ undefined,
346
+ theme,
347
+ indicatorMode,
348
+ hashlineGutter,
349
+ );
350
+ const divider = renderCodeDivider(theme, undefined, indicatorMode, hashlineGutter);
351
+ const firstContentPrefix = hashlineGutter
352
+ ? ""
353
+ : renderLineContentIndicatorPrefix(kind, theme, undefined, indicatorMode);
354
+ const continuationContentPrefix = hashlineGutter
355
+ ? ""
356
+ : renderLineContentIndicatorPrefix(kind, theme, undefined, indicatorMode, true);
357
+ const wrappedCodeLines = wrapToWidth(code, codeWidth, wordWrap);
358
+ return renderWrappedRowsWithOptionalBackground(
359
+ wrappedCodeLines,
360
+ (index, line) => {
361
+ const linePrefix = index === 0 ? prefix : continuationPrefix;
362
+ const contentPrefix = index === 0 ? firstContentPrefix : continuationContentPrefix;
363
+ return `${linePrefix}${divider}${contentPrefix}${line}`;
364
+ },
365
+ width,
366
+ rowBg,
367
+ restoreBgAnsi,
368
+ );
369
+ }
370
+
371
+ // Reuse line-cell layout without treating omissions as parsed source lines.
372
+ function createOmissionRenderLine(entry: DiffOmissionEntry, compact = false): DiffLineEntry {
373
+ return {
374
+ kind: "line",
375
+ lineKind: "context",
376
+ oldLineNumber: null,
377
+ newLineNumber: null,
378
+ fallbackLineNumber: compact ? "" : OMISSION_GLYPH,
379
+ content: compact ? OMISSION_GLYPH : "",
380
+ raw: entry.raw,
381
+ hunkIndex: entry.hunkIndex,
382
+ };
383
+ }
384
+
385
+ function materializeOmissionEntries(
386
+ entries: ParsedDiffEntry[],
387
+ compact = false,
388
+ ): MaterializedDiffEntry[] {
389
+ return entries.map((entry) =>
390
+ entry.kind === "omission" ? createOmissionRenderLine(entry, compact) : entry,
391
+ );
392
+ }
393
+
394
+ function pushDiffLineRows(rows: RenderedRow[], lines: string[], entry: DiffLineEntry): void {
395
+ rows.push(
396
+ ...lines.map((text) => ({
397
+ text,
398
+ hunkIndex: entry.hunkIndex || null,
399
+ })),
400
+ );
401
+ }
402
+
403
+ function processDiffEntries(
404
+ entries: MaterializedDiffEntry[],
405
+ ctx: DiffRenderContext,
406
+ processLine: (entry: DiffLineEntry) => string[],
407
+ ): RenderedRow[] {
408
+ const { width, theme, wordWrap } = ctx;
409
+ const rows: RenderedRow[] = [];
410
+ for (const entry of entries) {
411
+ if (entry.kind !== "line") {
412
+ rows.push(...formatMetaEntryRows(entry, width, theme, wordWrap));
413
+ continue;
414
+ }
415
+ pushDiffLineRows(rows, processLine(entry), entry);
416
+ }
417
+ return rows;
418
+ }
419
+
420
+ export function renderUnified(
421
+ entries: ParsedDiffEntry[],
422
+ ctx: DiffRenderContext,
423
+ lineNumberWidth: number,
424
+ ): RenderedRow[] {
425
+ return processDiffEntries(materializeOmissionEntries(entries), ctx, (entry) => {
426
+ const lineNumber =
427
+ entry.lineKind === "add"
428
+ ? formatLineNumberLabel(
429
+ entry,
430
+ entry.newLineNumber,
431
+ entry.fallbackLineNumber,
432
+ lineNumberWidth,
433
+ ctx.showHashlineAnchors,
434
+ )
435
+ : formatLineNumberLabel(
436
+ entry,
437
+ entry.oldLineNumber,
438
+ entry.fallbackLineNumber,
439
+ lineNumberWidth,
440
+ ctx.showHashlineAnchors,
441
+ );
442
+ const codeText = normalizeCodeWhitespace(entry.content);
443
+ const { highlighted, rowBg } = highlightDiffLine(
444
+ codeText,
445
+ entry,
446
+ ctx.inlineHighlights,
447
+ ctx.palette,
448
+ ctx.highlightLine,
449
+ ctx.containerBgAnsi,
450
+ );
451
+ return renderLineCell(
452
+ buildLineCellParams(
453
+ entry.lineKind,
454
+ highlighted,
455
+ ctx.width,
456
+ rowBg,
457
+ ctx.containerBgAnsi,
458
+ ctx.theme,
459
+ ctx.wordWrap,
460
+ ctx.indicatorMode,
461
+ ),
462
+ lineNumber,
463
+ usesHashlineGutter(ctx.showHashlineAnchors),
464
+ );
465
+ });
466
+ }
467
+
468
+ function toUnifiedFallbackRows(
469
+ rows: SplitDiffRow[],
470
+ ctx: DiffRenderContext,
471
+ lineNumberWidth: number,
472
+ ): RenderedRow[] {
473
+ const flattened: ParsedDiffEntry[] = [];
474
+ for (const row of rows) {
475
+ if (row.omission) {
476
+ flattened.push(row.omission);
477
+ continue;
478
+ }
479
+ if (row.meta) {
480
+ flattened.push(row.meta);
481
+ continue;
482
+ }
483
+ if (row.left) {
484
+ flattened.push(row.left);
485
+ }
486
+ if (row.right && row.right !== row.left) {
487
+ flattened.push(row.right);
488
+ }
489
+ }
490
+ return renderUnified(flattened, ctx, lineNumberWidth);
491
+ }
492
+
493
+ export function renderCompact(entries: ParsedDiffEntry[], ctx: DiffRenderContext): RenderedRow[] {
494
+ return processDiffEntries(materializeOmissionEntries(entries, true), ctx, (entry) => {
495
+ const codeText = normalizeCodeWhitespace(
496
+ getCompactLineRenderContent(entry, ctx.showHashlineAnchors),
497
+ );
498
+ const { highlighted, rowBg } = highlightDiffLine(
499
+ codeText,
500
+ entry,
501
+ ctx.inlineHighlights,
502
+ ctx.palette,
503
+ ctx.highlightLine,
504
+ ctx.containerBgAnsi,
505
+ );
506
+ return renderCompactLineCell(
507
+ buildLineCellParams(
508
+ entry.lineKind,
509
+ highlighted,
510
+ ctx.width,
511
+ rowBg,
512
+ ctx.containerBgAnsi,
513
+ ctx.theme,
514
+ ctx.wordWrap,
515
+ ctx.indicatorMode,
516
+ ),
517
+ );
518
+ });
519
+ }
520
+
521
+ function renderSplitBlankCell(
522
+ columnWidth: number,
523
+ lineNumberWidth: number,
524
+ theme: DiffTheme,
525
+ indicatorMode: DiffIndicatorMode,
526
+ hashlineGutter = false,
527
+ ): string {
528
+ const codeWidth = computeLineCellCodeWidth(
529
+ columnWidth,
530
+ lineNumberWidth,
531
+ indicatorMode,
532
+ hashlineGutter,
533
+ );
534
+ const prefix = renderLinePrefix(
535
+ "context",
536
+ " ".repeat(lineNumberWidth),
537
+ theme,
538
+ undefined,
539
+ indicatorMode,
540
+ true,
541
+ hashlineGutter,
542
+ );
543
+ const divider = renderCodeDivider(theme, undefined, indicatorMode, hashlineGutter);
544
+ const contentPrefix = hashlineGutter
545
+ ? ""
546
+ : renderLineContentIndicatorPrefix("context", theme, undefined, indicatorMode, true);
547
+ return stabilizeBackgroundResets(`${prefix}${divider}${contentPrefix}${" ".repeat(codeWidth)}`);
548
+ }
549
+
550
+ function renderSplitCell(
551
+ line: DiffLineEntry | undefined,
552
+ side: "left" | "right",
553
+ columnWidth: number,
554
+ lineNumberWidth: number,
555
+ theme: DiffTheme,
556
+ inlineHighlights: WeakMap<DiffLineEntry, DiffSpan[]>,
557
+ palette: DiffPalette,
558
+ highlightLine: CodeLineHighlighter,
559
+ containerBgAnsi: string | undefined,
560
+ wordWrap: boolean,
561
+ indicatorMode: DiffIndicatorMode,
562
+ showHashlineAnchors: boolean,
563
+ ): string[] {
564
+ const hashlineGutter = usesHashlineGutter(showHashlineAnchors);
565
+ if (!line) {
566
+ return [
567
+ renderSplitBlankCell(columnWidth, lineNumberWidth, theme, indicatorMode, hashlineGutter),
568
+ ];
569
+ }
570
+
571
+ const lineNumber = formatLineNumberLabel(
572
+ line,
573
+ getCellLineNumber(line, side),
574
+ line.fallbackLineNumber,
575
+ lineNumberWidth,
576
+ showHashlineAnchors,
577
+ );
578
+ const codeText = normalizeCodeWhitespace(line.content);
579
+ const { highlighted, rowBg } = highlightDiffLine(
580
+ codeText,
581
+ line,
582
+ inlineHighlights,
583
+ palette,
584
+ highlightLine,
585
+ containerBgAnsi,
586
+ );
587
+ return renderLineCell(
588
+ buildLineCellParams(
589
+ line.lineKind,
590
+ highlighted,
591
+ columnWidth,
592
+ rowBg,
593
+ containerBgAnsi,
594
+ theme,
595
+ wordWrap,
596
+ indicatorMode,
597
+ ),
598
+ lineNumber,
599
+ hashlineGutter,
600
+ );
601
+ }
602
+
603
+ function renderSplitDivider(
604
+ theme: DiffTheme,
605
+ containerBgAnsi: string | undefined,
606
+ separatorText: string = SPLIT_SEPARATOR,
607
+ ): string {
608
+ const dimAnsi = readThemeAnsi(theme, "fg", "dim");
609
+ if (!containerBgAnsi) {
610
+ return stabilizeBackgroundResets(theme.fg("dim", separatorText));
611
+ }
612
+ if (!dimAnsi) {
613
+ return stabilizeBackgroundResets(
614
+ `${containerBgAnsi}${theme.fg("dim", separatorText)}${containerBgAnsi}`,
615
+ );
616
+ }
617
+ return stabilizeBackgroundResets(
618
+ `${containerBgAnsi}${dimAnsi}${separatorText}\x1b[39m${containerBgAnsi}`,
619
+ );
620
+ }
621
+
622
+ function renderSplitTopBorderCell(
623
+ columnWidth: number,
624
+ lineNumberWidth: number,
625
+ theme: DiffTheme,
626
+ indicatorMode: DiffIndicatorMode,
627
+ hashlineGutter = false,
628
+ ): string {
629
+ const safeColumnWidth = Math.max(1, columnWidth);
630
+ const chars = "─".repeat(safeColumnWidth).split("");
631
+ const dividerIndex = getLinePrefixPlainWidth(lineNumberWidth, indicatorMode, hashlineGutter);
632
+ if (dividerIndex >= 0 && dividerIndex < chars.length) {
633
+ chars[dividerIndex] = "┬";
634
+ }
635
+ return stabilizeBackgroundResets(theme.fg("dim", chars.join("")));
636
+ }
637
+
638
+ function renderSplitHeaderCell(
639
+ label: string,
640
+ columnWidth: number,
641
+ lineNumberWidth: number,
642
+ theme: DiffTheme,
643
+ indicatorMode: DiffIndicatorMode,
644
+ hashlineGutter = false,
645
+ ): string {
646
+ const markerPad = hashlineGutter
647
+ ? " ".repeat(getHashlineGutterMarkerWidth(indicatorMode))
648
+ : indicatorMode === "bars"
649
+ ? " "
650
+ : "";
651
+ const lineNumberLabel = fitToWidth(label, lineNumberWidth);
652
+ const lineNumberSpacer = hashlineGutter ? "" : " ";
653
+ const divider = hashlineGutter || indicatorMode !== "classic" ? "│ " : "│";
654
+ const prefix = `${theme.fg("dim", markerPad)}${theme.fg("muted", lineNumberLabel)}${theme.fg("dim", lineNumberSpacer)}${theme.fg("dim", divider)}`;
655
+ const prefixWidth = visibleWidth(`${markerPad}${lineNumberLabel}${lineNumberSpacer}${divider}`);
656
+ const contentIndicatorWidth = hashlineGutter
657
+ ? 0
658
+ : getLineContentIndicatorPrefixPlainWidth(indicatorMode);
659
+ const codeWidth = Math.max(0, columnWidth - prefixWidth - contentIndicatorWidth);
660
+ const contentPad = !hashlineGutter && indicatorMode === "classic" ? " " : "";
661
+ return stabilizeBackgroundResets(`${prefix}${contentPad}${" ".repeat(codeWidth)}`);
662
+ }
663
+
664
+ export function canRenderSplitLayout(width: number): boolean {
665
+ const separatorWidth = visibleWidth(SPLIT_SEPARATOR);
666
+ const minimumSplitWidth = MIN_SPLIT_COLUMN_WIDTH * 2 + separatorWidth;
667
+ return width >= minimumSplitWidth;
668
+ }
669
+
670
+ export function renderSplit(
671
+ rows: SplitDiffRow[],
672
+ ctx: DiffRenderContext,
673
+ lineNumberWidth: number,
674
+ ): RenderedRow[] {
675
+ const {
676
+ width,
677
+ theme,
678
+ inlineHighlights,
679
+ palette,
680
+ highlightLine,
681
+ containerBgAnsi,
682
+ wordWrap,
683
+ indicatorMode,
684
+ showHashlineAnchors,
685
+ } = ctx;
686
+ if (!canRenderSplitLayout(width)) {
687
+ return toUnifiedFallbackRows(rows, ctx, lineNumberWidth);
688
+ }
689
+
690
+ const separatorWidth = visibleWidth(SPLIT_SEPARATOR);
691
+ const leftWidth = Math.max(MIN_SPLIT_COLUMN_WIDTH, Math.floor((width - separatorWidth) / 2));
692
+ const rightWidth = Math.max(MIN_SPLIT_COLUMN_WIDTH, width - separatorWidth - leftWidth);
693
+ const splitLineNumberWidth = Math.max(3, lineNumberWidth);
694
+ const hashlineGutter = usesHashlineGutter(showHashlineAnchors);
695
+ const separator = renderSplitDivider(theme, containerBgAnsi);
696
+ const topSeparator = renderSplitDivider(theme, containerBgAnsi, "─┬─");
697
+ const output: RenderedRow[] = [];
698
+ output.push({
699
+ text: `${renderSplitTopBorderCell(leftWidth, splitLineNumberWidth, theme, indicatorMode, hashlineGutter)}${topSeparator}${renderSplitTopBorderCell(rightWidth, splitLineNumberWidth, theme, indicatorMode, hashlineGutter)}`,
700
+ hunkIndex: null,
701
+ });
702
+ output.push({
703
+ text: `${renderSplitHeaderCell("old", leftWidth, splitLineNumberWidth, theme, indicatorMode, hashlineGutter)}${separator}${renderSplitHeaderCell("new", rightWidth, splitLineNumberWidth, theme, indicatorMode, hashlineGutter)}`,
704
+ hunkIndex: null,
705
+ });
706
+
707
+ for (const row of rows) {
708
+ if (row.meta) {
709
+ output.push(...formatMetaEntryRows(row.meta, width, theme, wordWrap));
710
+ continue;
711
+ }
712
+
713
+ const omissionLine = row.omission ? createOmissionRenderLine(row.omission) : undefined;
714
+ const leftCells = renderSplitCell(
715
+ omissionLine ?? row.left,
716
+ "left",
717
+ leftWidth,
718
+ splitLineNumberWidth,
719
+ theme,
720
+ inlineHighlights,
721
+ palette,
722
+ highlightLine,
723
+ containerBgAnsi,
724
+ wordWrap,
725
+ indicatorMode,
726
+ showHashlineAnchors,
727
+ );
728
+ const rightCells = renderSplitCell(
729
+ omissionLine ?? row.right,
730
+ "right",
731
+ rightWidth,
732
+ splitLineNumberWidth,
733
+ theme,
734
+ inlineHighlights,
735
+ palette,
736
+ highlightLine,
737
+ containerBgAnsi,
738
+ wordWrap,
739
+ indicatorMode,
740
+ showHashlineAnchors,
741
+ );
742
+
743
+ const rowCount = Math.max(leftCells.length, rightCells.length);
744
+ for (let index = 0; index < rowCount; index++) {
745
+ const leftCell =
746
+ leftCells[index] ??
747
+ renderSplitBlankCell(leftWidth, splitLineNumberWidth, theme, indicatorMode, hashlineGutter);
748
+ const rightCell =
749
+ rightCells[index] ??
750
+ renderSplitBlankCell(
751
+ rightWidth,
752
+ splitLineNumberWidth,
753
+ theme,
754
+ indicatorMode,
755
+ hashlineGutter,
756
+ );
757
+ output.push({ text: `${leftCell}${separator}${rightCell}`, hunkIndex: row.hunkIndex });
758
+ }
759
+ }
760
+
761
+ return output;
762
+ }