@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,44 @@
1
+ import { truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
2
+
3
+ export function pluralize(count: number, singular: string, plural = `${singular}s`): string {
4
+ return count === 1 ? singular : plural;
5
+ }
6
+
7
+ export function splitWriteContentLines(content: string): string[] {
8
+ if (!content) {
9
+ return [];
10
+ }
11
+
12
+ const lines = content.split(/\r\n|\n|\r/);
13
+ if (lines.length > 0 && lines[lines.length - 1] === "") {
14
+ lines.pop();
15
+ }
16
+ return lines;
17
+ }
18
+
19
+ export function normalizeCodeWhitespace(text: string): string {
20
+ return text.replace(/\t/g, " ");
21
+ }
22
+
23
+ export function fitToWidth(text: string, width: number): string {
24
+ const trimmed = truncateToWidth(text, width, "");
25
+ const gap = Math.max(0, width - visibleWidth(trimmed));
26
+ return gap > 0 ? `${trimmed}${" ".repeat(gap)}` : trimmed;
27
+ }
28
+
29
+ export function wrapToWidth(text: string, width: number, wordWrap: boolean): string[] {
30
+ if (width <= 0) {
31
+ return [""];
32
+ }
33
+
34
+ if (!wordWrap) {
35
+ return [fitToWidth(text, width)];
36
+ }
37
+
38
+ const wrapped = wrapTextWithAnsi(text, width);
39
+ if (wrapped.length === 0) {
40
+ return [fitToWidth("", width)];
41
+ }
42
+
43
+ return wrapped.map((line) => fitToWidth(line, width));
44
+ }
@@ -0,0 +1,568 @@
1
+ import { Text, truncateToWidth, visibleWidth, type Component } from "@earendil-works/pi-tui";
2
+ import { sanitizeToolResultText } from "../../../utils/tool-result-sanitize.ts";
3
+ import { showMoreHintText } from "../show-more-hint.ts";
4
+ import { splitWriteContentLines } from "./diff-text.ts";
5
+ import {
6
+ collectDiffStats,
7
+ getLineNumberWidth,
8
+ type DiffLineEntry,
9
+ type DiffStats,
10
+ type ParsedDiffEntry,
11
+ } from "./diff-parse.ts";
12
+ import {
13
+ buildInlineHighlightMap,
14
+ buildSplitRows,
15
+ type DiffSpan,
16
+ type SplitDiffRow,
17
+ } from "./diff-inline.ts";
18
+ import { resolveDiffPalette, type DiffTheme } from "./diff-palette.ts";
19
+ import {
20
+ createCodeLineHighlighter,
21
+ resolveLanguageFromPath,
22
+ type CodeLineHighlighter,
23
+ } from "./diff-highlight.ts";
24
+ import {
25
+ canRenderSplitLayout,
26
+ renderCompact,
27
+ renderSplit,
28
+ renderUnified,
29
+ type DiffRenderContext,
30
+ type RenderedRow,
31
+ } from "./diff-layout.ts";
32
+ import { renderDiffFrameLine, renderSingleDiffRow, renderWriteHeader } from "./diff-header.ts";
33
+ import {
34
+ applyLineLimit,
35
+ clampDiffLineToWidth,
36
+ clampDiffLinesToWidth,
37
+ resolveDiffDisplayLimit,
38
+ resolveDiffProcessBudget,
39
+ resolveWriteCollapsedLimit,
40
+ takeEntriesForLineBudget,
41
+ takeSplitRowsForBudget,
42
+ } from "./diff-limits.ts";
43
+ import {
44
+ RICH_DIFF_COMPONENT,
45
+ createDiffRenderCache,
46
+ displayConfigCacheKey,
47
+ resolveDiffIndicatorMode,
48
+ resolveLiveDisplayConfig,
49
+ normalizeCollapsedHintLine,
50
+ type DiffRenderOptions,
51
+ type DisplayConfigInput,
52
+ } from "./diff-component.ts";
53
+ import {
54
+ buildDiffSummaryText,
55
+ normalizeDiffRenderWidth,
56
+ resolveDiffPresentationMode,
57
+ type DiffPresentationMode,
58
+ } from "./diff-presentation.ts";
59
+
60
+ type WriteDiffOperationKind = "context" | "remove" | "add";
61
+
62
+ interface WriteDiffOperation {
63
+ kind: WriteDiffOperationKind;
64
+ content: string;
65
+ }
66
+
67
+ function buildWriteDiffOperations(oldLines: string[], newLines: string[]): WriteDiffOperation[] {
68
+ const oldLength = oldLines.length;
69
+ const newLength = newLines.length;
70
+ const table: number[][] = Array.from({ length: oldLength + 1 }, () =>
71
+ Array<number>(newLength + 1).fill(0),
72
+ );
73
+
74
+ for (let oldIndex = 1; oldIndex <= oldLength; oldIndex++) {
75
+ for (let newIndex = 1; newIndex <= newLength; newIndex++) {
76
+ if ((oldLines[oldIndex - 1] ?? "") === (newLines[newIndex - 1] ?? "")) {
77
+ table[oldIndex]![newIndex] = (table[oldIndex - 1]?.[newIndex - 1] ?? 0) + 1;
78
+ continue;
79
+ }
80
+ const top = table[oldIndex - 1]?.[newIndex] ?? 0;
81
+ const left = table[oldIndex]?.[newIndex - 1] ?? 0;
82
+ table[oldIndex]![newIndex] = Math.max(top, left);
83
+ }
84
+ }
85
+
86
+ const operations: WriteDiffOperation[] = [];
87
+ let oldCursor = oldLength;
88
+ let newCursor = newLength;
89
+
90
+ while (oldCursor > 0 || newCursor > 0) {
91
+ const oldLine = oldCursor > 0 ? (oldLines[oldCursor - 1] ?? "") : undefined;
92
+ const newLine = newCursor > 0 ? (newLines[newCursor - 1] ?? "") : undefined;
93
+
94
+ if (oldCursor > 0 && newCursor > 0 && oldLine === newLine) {
95
+ operations.push({ kind: "context", content: oldLine ?? "" });
96
+ oldCursor--;
97
+ newCursor--;
98
+ continue;
99
+ }
100
+
101
+ const top = oldCursor > 0 ? (table[oldCursor - 1]?.[newCursor] ?? 0) : -1;
102
+ const left = newCursor > 0 ? (table[oldCursor]?.[newCursor - 1] ?? 0) : -1;
103
+
104
+ if (newCursor > 0 && left >= top) {
105
+ operations.push({ kind: "add", content: newLine ?? "" });
106
+ newCursor--;
107
+ continue;
108
+ }
109
+
110
+ if (oldCursor > 0) {
111
+ operations.push({ kind: "remove", content: oldLine ?? "" });
112
+ oldCursor--;
113
+ }
114
+ }
115
+
116
+ operations.reverse();
117
+ return operations;
118
+ }
119
+
120
+ function buildWriteEntries(lines: string[]): ParsedDiffEntry[] {
121
+ return lines.map((line, index) => ({
122
+ kind: "line",
123
+ lineKind: "add",
124
+ oldLineNumber: null,
125
+ newLineNumber: index + 1,
126
+ fallbackLineNumber: `${index + 1}`,
127
+ content: line,
128
+ raw: `+${line}`,
129
+ hunkIndex: 1,
130
+ }));
131
+ }
132
+
133
+ function buildWriteOverwriteEntries(oldLines: string[], newLines: string[]): ParsedDiffEntry[] {
134
+ const operations = buildWriteDiffOperations(oldLines, newLines);
135
+ const entries: ParsedDiffEntry[] = [];
136
+ let oldLineNumber = 1;
137
+ let newLineNumber = 1;
138
+
139
+ for (const operation of operations) {
140
+ if (operation.kind === "context") {
141
+ entries.push({
142
+ kind: "line",
143
+ lineKind: "context",
144
+ oldLineNumber,
145
+ newLineNumber,
146
+ fallbackLineNumber: `${newLineNumber}`,
147
+ content: operation.content,
148
+ raw: ` ${operation.content}`,
149
+ hunkIndex: 1,
150
+ });
151
+ oldLineNumber++;
152
+ newLineNumber++;
153
+ continue;
154
+ }
155
+
156
+ if (operation.kind === "remove") {
157
+ entries.push({
158
+ kind: "line",
159
+ lineKind: "remove",
160
+ oldLineNumber,
161
+ newLineNumber: null,
162
+ fallbackLineNumber: `${oldLineNumber}`,
163
+ content: operation.content,
164
+ raw: `-${operation.content}`,
165
+ hunkIndex: 1,
166
+ });
167
+ oldLineNumber++;
168
+ continue;
169
+ }
170
+
171
+ entries.push({
172
+ kind: "line",
173
+ lineKind: "add",
174
+ oldLineNumber: null,
175
+ newLineNumber,
176
+ fallbackLineNumber: `${newLineNumber}`,
177
+ content: operation.content,
178
+ raw: `+${operation.content}`,
179
+ hunkIndex: 1,
180
+ });
181
+ newLineNumber++;
182
+ }
183
+
184
+ return entries;
185
+ }
186
+
187
+ interface WriteDiffData {
188
+ entries: ParsedDiffEntry[];
189
+ splitRows: SplitDiffRow[];
190
+ inlineHighlights: WeakMap<DiffLineEntry, DiffSpan[]>;
191
+ lineNumberWidth: number;
192
+ stats: DiffStats;
193
+ hunkCount: number;
194
+ }
195
+
196
+ interface WriteOverwriteGuard {
197
+ previousLineCount: number;
198
+ nextLineCount: number;
199
+ }
200
+
201
+ const MAX_WRITE_OVERWRITE_DIFF_LINES = 4000;
202
+ const MAX_WRITE_OVERWRITE_DIFF_MATRIX_CELLS = 1_000_000;
203
+ /** Collapsed overwrite previews refuse larger LCS matrices to avoid TUI freezes. */
204
+ const MAX_COLLAPSED_WRITE_OVERWRITE_DIFF_MATRIX_CELLS = 200_000;
205
+
206
+ function buildApproximateWriteStats(
207
+ lineCount: number,
208
+ previousLineCount: number,
209
+ hasComparablePrevious: boolean,
210
+ ): DiffStats {
211
+ const removed = hasComparablePrevious ? previousLineCount : 0;
212
+ const added = lineCount;
213
+ const hasContent = lineCount > 0 || removed > 0;
214
+ return {
215
+ added,
216
+ removed,
217
+ context: 0,
218
+ hunks: hasContent ? 1 : 0,
219
+ files: 1,
220
+ lines: added + removed,
221
+ };
222
+ }
223
+
224
+ function buildWriteDiffData(entries: ParsedDiffEntry[]): WriteDiffData {
225
+ const splitRows = buildSplitRows(entries);
226
+ const inlineHighlights = buildInlineHighlightMap(splitRows);
227
+ const lineNumberWidth = getLineNumberWidth(entries);
228
+ const hunkCount = entries.length > 0 ? 1 : 0;
229
+ const stats = collectDiffStats(entries, hunkCount, 1);
230
+ return {
231
+ entries,
232
+ splitRows,
233
+ inlineHighlights,
234
+ lineNumberWidth,
235
+ stats,
236
+ hunkCount,
237
+ };
238
+ }
239
+
240
+ function resolveWriteOverwriteGuard(
241
+ previousLines: string[],
242
+ nextLines: string[],
243
+ expanded = true,
244
+ ): WriteOverwriteGuard | undefined {
245
+ const previousLineCount = previousLines.length;
246
+ const nextLineCount = nextLines.length;
247
+ if (
248
+ previousLineCount > MAX_WRITE_OVERWRITE_DIFF_LINES ||
249
+ nextLineCount > MAX_WRITE_OVERWRITE_DIFF_LINES
250
+ ) {
251
+ return { previousLineCount, nextLineCount };
252
+ }
253
+ if (previousLineCount === 0 || nextLineCount === 0) {
254
+ return undefined;
255
+ }
256
+ const cellLimit = expanded
257
+ ? MAX_WRITE_OVERWRITE_DIFF_MATRIX_CELLS
258
+ : MAX_COLLAPSED_WRITE_OVERWRITE_DIFF_MATRIX_CELLS;
259
+ return previousLineCount * nextLineCount > cellLimit
260
+ ? { previousLineCount, nextLineCount }
261
+ : undefined;
262
+ }
263
+
264
+ function buildWriteOverwriteGuardText(guard: WriteOverwriteGuard, width: number): string {
265
+ const safeWidth = normalizeDiffRenderWidth(width);
266
+ if (safeWidth === 0) {
267
+ return "";
268
+ }
269
+
270
+ const candidates = [
271
+ `↳ overwrite diff omitted (${guard.previousLineCount} → ${guard.nextLineCount} lines)`,
272
+ `↳ overwrite diff omitted (${guard.previousLineCount}→${guard.nextLineCount})`,
273
+ "↳ overwrite diff omitted",
274
+ "diff omitted",
275
+ "…",
276
+ ];
277
+ for (const candidate of candidates) {
278
+ if (visibleWidth(candidate) <= safeWidth) {
279
+ return candidate;
280
+ }
281
+ }
282
+ return truncateToWidth(candidates[candidates.length - 1] ?? "", safeWidth, "");
283
+ }
284
+
285
+ function renderWriteOverwriteGuardRows(
286
+ guard: WriteOverwriteGuard,
287
+ width: number,
288
+ theme: DiffTheme,
289
+ ): string[] {
290
+ return renderSingleDiffRow(buildWriteOverwriteGuardText(guard, width), "warning", width, theme);
291
+ }
292
+
293
+ /** Folded write with `writeDiffCollapsedLines: 0`: `↳ created • click to show more`. */
294
+ function renderWriteCollapsedHintLine(
295
+ wasOverwrite: boolean,
296
+ width: number,
297
+ theme: DiffTheme,
298
+ hovered: boolean,
299
+ headerLabel?: string,
300
+ ): string[] {
301
+ const safeWidth = normalizeDiffRenderWidth(width);
302
+ if (safeWidth === 0) {
303
+ return [""];
304
+ }
305
+ const actionLabel = headerLabel?.trim() || (wasOverwrite ? "overwritten" : "created");
306
+ const clickLabel = showMoreHintText();
307
+ const candidates = [`↳ ${actionLabel} • ${clickLabel}`, `↳ ${actionLabel}`, actionLabel, "…"];
308
+ let text = candidates[candidates.length - 1] ?? "…";
309
+ for (const candidate of candidates) {
310
+ if (visibleWidth(candidate) <= safeWidth) {
311
+ text = candidate;
312
+ break;
313
+ }
314
+ }
315
+ if (visibleWidth(text) > safeWidth) {
316
+ text = truncateToWidth(text, safeWidth, "");
317
+ }
318
+ const clickIndex = text.lastIndexOf(clickLabel);
319
+ const styled =
320
+ hovered && clickIndex >= 0
321
+ ? theme.fg("muted", text.slice(0, clickIndex)) +
322
+ theme.fg("text", clickLabel) +
323
+ theme.fg("muted", text.slice(clickIndex + clickLabel.length))
324
+ : theme.fg("muted", text);
325
+ return [clampDiffLineToWidth(styled, safeWidth)];
326
+ }
327
+
328
+ /**
329
+ * write 变更行统计(compact 单行用):复用当前 write diff 算法。
330
+ * 新文件 = 全部新内容为 added;覆盖已有文件用 LCS 精确统计。
331
+ * content 非字符串、元数据缺失或超过矩阵预算时返回 undefined(不显示计数)。
332
+ */
333
+ export function countWriteDiffStats(
334
+ content: string | undefined,
335
+ previousContent: string | undefined,
336
+ fileExistedBeforeWrite: boolean | undefined,
337
+ ): { added: number; removed: number } | undefined {
338
+ if (typeof content !== "string" || fileExistedBeforeWrite === undefined) return undefined;
339
+ const lines = splitWriteContentLines(sanitizeToolResultText(content));
340
+ if (fileExistedBeforeWrite === false) return { added: lines.length, removed: 0 };
341
+ if (typeof previousContent !== "string") return undefined;
342
+ const previousLines = splitWriteContentLines(sanitizeToolResultText(previousContent));
343
+ if (previousLines.length === 0 || lines.length === 0) {
344
+ return { added: lines.length, removed: previousLines.length };
345
+ }
346
+ if (previousLines.length * lines.length > MAX_COLLAPSED_WRITE_OVERWRITE_DIFF_MATRIX_CELLS) {
347
+ return undefined;
348
+ }
349
+ let added = 0;
350
+ let removed = 0;
351
+ for (const operation of buildWriteDiffOperations(previousLines, lines)) {
352
+ if (operation.kind === "add") added++;
353
+ else if (operation.kind === "remove") removed++;
354
+ }
355
+ return { added, removed };
356
+ }
357
+
358
+ export function renderWriteDiffResult(
359
+ content: string | undefined,
360
+ options: DiffRenderOptions,
361
+ config: DisplayConfigInput,
362
+ theme: DiffTheme,
363
+ fallbackText: string,
364
+ ): Component {
365
+ const safeFallbackText = sanitizeToolResultText(fallbackText);
366
+ if (typeof content !== "string") {
367
+ if (!safeFallbackText.trim()) {
368
+ return new Text(theme.fg("muted", "↳ write completed"), 0, 0);
369
+ }
370
+ return new Text(theme.fg("toolOutput", safeFallbackText), 0, 0);
371
+ }
372
+
373
+ const filePath = options.filePath?.trim() || "(unknown path)";
374
+ const lines = splitWriteContentLines(sanitizeToolResultText(content));
375
+ const previousLines =
376
+ typeof options.previousContent === "string"
377
+ ? splitWriteContentLines(sanitizeToolResultText(options.previousContent))
378
+ : [];
379
+ const hasComparablePrevious =
380
+ options.fileExistedBeforeWrite === true && typeof options.previousContent === "string";
381
+ const approximateStats = buildApproximateWriteStats(
382
+ lines.length,
383
+ previousLines.length,
384
+ hasComparablePrevious,
385
+ );
386
+ const palette = resolveDiffPalette(theme);
387
+ // Keep the panel transparent; changed rows and inline spans retain their own highlights.
388
+ const containerBgAnsi = undefined;
389
+ const language = resolveLanguageFromPath(filePath);
390
+ const cache = createDiffRenderCache();
391
+ let highlightLine: CodeLineHighlighter | undefined;
392
+ let detailedData: WriteDiffData | undefined;
393
+
394
+ function getDetailedData(): WriteDiffData {
395
+ if (detailedData) {
396
+ return detailedData;
397
+ }
398
+ const entries = hasComparablePrevious
399
+ ? buildWriteOverwriteEntries(previousLines, lines)
400
+ : buildWriteEntries(lines);
401
+ detailedData = buildWriteDiffData(entries);
402
+ highlightLine = createCodeLineHighlighter(language, theme, entries, () => {
403
+ cache.invalidate();
404
+ options.invalidate?.();
405
+ });
406
+ return detailedData;
407
+ }
408
+
409
+ return {
410
+ [RICH_DIFF_COMPONENT]: true,
411
+ /** 鼠标层只在真正的 remainder 行上提供展开入口。 */
412
+ isCollapsedHintLine(plainLine: string): boolean {
413
+ const hint = cache.getHintLine();
414
+ return hint !== undefined && normalizeCollapsedHintLine(plainLine) === hint;
415
+ },
416
+ render(width: number): string[] {
417
+ // Live config: panel can change indicator/wrap/limits after this component is created.
418
+ const live = resolveLiveDisplayConfig(config);
419
+ const wordWrap = live.diffWordWrap;
420
+ const indicatorMode = resolveDiffIndicatorMode(live);
421
+ const configKey = displayConfigCacheKey(live);
422
+ const safeWidth = normalizeDiffRenderWidth(width);
423
+ const resolvedMode = resolveDiffPresentationMode(
424
+ live,
425
+ safeWidth,
426
+ canRenderSplitLayout(safeWidth),
427
+ );
428
+ const mode: DiffPresentationMode = hasComparablePrevious
429
+ ? resolvedMode
430
+ : resolvedMode === "split"
431
+ ? "unified"
432
+ : resolvedMode;
433
+ const hovered = options.isHovered?.() ?? false;
434
+ const cached = cache.get(safeWidth, options.expanded, mode, configKey, hovered);
435
+ if (cached) {
436
+ return cached;
437
+ }
438
+
439
+ const writeCollapsedLimit = resolveWriteCollapsedLimit(live);
440
+ const statsOnlyCollapsed = !options.expanded && writeCollapsedLimit === 0;
441
+ if (statsOnlyCollapsed) {
442
+ const hintLines = renderWriteCollapsedHintLine(
443
+ options.fileExistedBeforeWrite === true,
444
+ safeWidth,
445
+ theme,
446
+ hovered,
447
+ options.headerLabel,
448
+ );
449
+ return cache.set(
450
+ safeWidth,
451
+ options.expanded,
452
+ mode,
453
+ configKey,
454
+ hovered,
455
+ clampDiffLinesToWidth(hintLines, safeWidth),
456
+ normalizeCollapsedHintLine(hintLines[0]),
457
+ );
458
+ }
459
+
460
+ const header = renderWriteHeader(
461
+ options.fileExistedBeforeWrite === true,
462
+ safeWidth,
463
+ theme,
464
+ options.headerLabel,
465
+ );
466
+ // Guard depends on expanded: collapsed previews use a stricter LCS cell budget.
467
+ const overwriteGuard = hasComparablePrevious
468
+ ? resolveWriteOverwriteGuard(previousLines, lines, options.expanded)
469
+ : undefined;
470
+ if (overwriteGuard) {
471
+ return cache.set(
472
+ safeWidth,
473
+ options.expanded,
474
+ mode,
475
+ configKey,
476
+ hovered,
477
+ clampDiffLinesToWidth(
478
+ [header, ...renderWriteOverwriteGuardRows(overwriteGuard, safeWidth, theme)],
479
+ safeWidth,
480
+ ),
481
+ );
482
+ }
483
+
484
+ if (mode === "summary") {
485
+ const summaryRows =
486
+ approximateStats.lines === 0
487
+ ? [header]
488
+ : [
489
+ header,
490
+ ...renderSingleDiffRow(
491
+ buildDiffSummaryText(approximateStats, safeWidth),
492
+ "toolOutput",
493
+ safeWidth,
494
+ theme,
495
+ ),
496
+ ];
497
+ return cache.set(
498
+ safeWidth,
499
+ options.expanded,
500
+ mode,
501
+ configKey,
502
+ hovered,
503
+ clampDiffLinesToWidth(summaryRows, safeWidth),
504
+ );
505
+ }
506
+
507
+ const data = getDetailedData();
508
+ const displayLimit = resolveDiffDisplayLimit(options.expanded, writeCollapsedLimit);
509
+ const processBudget = resolveDiffProcessBudget(displayLimit, wordWrap);
510
+ const entryBudget = takeEntriesForLineBudget(data.entries, processBudget);
511
+ const splitBudget = takeSplitRowsForBudget(data.splitRows, processBudget);
512
+ const inlineHighlights =
513
+ splitBudget.processedRows < splitBudget.totalRows
514
+ ? buildInlineHighlightMap(splitBudget.rows)
515
+ : data.inlineHighlights;
516
+ const renderCtx: DiffRenderContext = {
517
+ width: safeWidth,
518
+ theme,
519
+ inlineHighlights,
520
+ palette,
521
+ highlightLine: highlightLine!,
522
+ containerBgAnsi,
523
+ wordWrap,
524
+ indicatorMode,
525
+ showHashlineAnchors: false,
526
+ };
527
+ const bodyRows: RenderedRow[] =
528
+ entryBudget.entries.length === 0
529
+ ? [{ text: theme.fg("muted", "(empty file)"), hunkIndex: null }]
530
+ : mode === "split"
531
+ ? renderSplit(splitBudget.rows, renderCtx, data.lineNumberWidth)
532
+ : mode === "compact"
533
+ ? renderCompact(entryBudget.entries, renderCtx)
534
+ : renderUnified(entryBudget.entries, renderCtx, data.lineNumberWidth);
535
+ const unprocessedLogicalRows =
536
+ mode === "split"
537
+ ? Math.max(0, splitBudget.totalRows - splitBudget.processedRows)
538
+ : Math.max(0, entryBudget.totalLineEntries - entryBudget.processedLineEntries);
539
+
540
+ const bodyWithLimit = applyLineLimit(
541
+ bodyRows,
542
+ safeWidth,
543
+ options.expanded,
544
+ writeCollapsedLimit,
545
+ data.hunkCount,
546
+ theme,
547
+ unprocessedLogicalRows,
548
+ hovered,
549
+ );
550
+ const frame = renderDiffFrameLine(safeWidth, theme);
551
+ const renderedLines =
552
+ mode === "unified"
553
+ ? [header, frame, ...bodyWithLimit.lines, frame]
554
+ : [header, ...bodyWithLimit.lines];
555
+ const finalLines = clampDiffLinesToWidth(renderedLines, safeWidth);
556
+ return cache.set(
557
+ safeWidth,
558
+ options.expanded,
559
+ mode,
560
+ configKey,
561
+ hovered,
562
+ finalLines,
563
+ normalizeCollapsedHintLine(bodyWithLimit.hintLine),
564
+ );
565
+ },
566
+ invalidate: cache.invalidate,
567
+ } as Component;
568
+ }
@@ -5,7 +5,8 @@ import {
5
5
  renderWriteDiffResult,
6
6
  type DisplayConfigInput,
7
7
  } from "./diff-renderer.ts";
8
- import { DEFAULT_TOOL_DISPLAY_CONFIG } from "./types.ts";
8
+ import { DEFAULT_TOOL_DISPLAY_CONFIG } from "../../../config/config.ts";
9
+ import { patchRegistry, WRITE_OWNERSHIP_SLOT } from "../../../utils/patch-keys.ts";
9
10
  import { executeWriteWithMetadata, WriteExecutionMetadataStore } from "./write-execution.ts";
10
11
 
11
12
  function resultText(result: any): string {
@@ -59,7 +60,7 @@ export function renderRichToolResult(
59
60
  );
60
61
  }
61
62
  if (toolName !== "write") return undefined;
62
- // Yield point: when another extension owns write there is no execution metadata; fall back to the plain result row.
63
+ // 让位点:write 归其他扩展时不提供富 diff,交回普通结果行。
63
64
  if (!ownsWriteTool()) return undefined;
64
65
 
65
66
  const metadata = writeMetadata.get(context?.toolCallId);
@@ -85,24 +86,27 @@ export function renderRichToolResult(
85
86
  );
86
87
  }
87
88
 
88
- /** Extension currently owning write, reported in the conflict notice. */
89
+ /** write 被其他扩展占用时的来源,用于提示冲突。 */
89
90
  export type ExternalWriteOwner = { source: string; path: string };
90
91
 
91
- const WRITE_OWNERSHIP_SLOT = Symbol.for("pi.ccstyle.write-ownership");
92
- /** Parameters object of the write override registered by this module; identifies our own tool in getAllTools(). */
93
- let ownWriteParameters: unknown;
92
+ type WriteOwnershipState = {
93
+ /** write 是否由本插件执行;undefined 表示尚未确认,按拥有处理以保持既有行为。 */
94
+ owned?: boolean;
95
+ };
94
96
 
95
- function writeOwnership(): { owned?: boolean } {
96
- const slots = globalThis as any;
97
- slots[WRITE_OWNERSHIP_SLOT] ??= {};
98
- return slots[WRITE_OWNERSHIP_SLOT];
97
+ function writeOwnership(): WriteOwnershipState {
98
+ return patchRegistry.ensure<WriteOwnershipState>(WRITE_OWNERSHIP_SLOT, () => ({}));
99
99
  }
100
100
 
101
- /** Whether this package executes write; unknown ownership keeps the existing rich-diff behavior. */
101
+ /**
102
+ * write 是否由本插件执行。只有注册过 write override 才有执行元数据,
103
+ * 否则渲染层要放行给普通结果行,避免每张卡降级成 "diff unavailable"。
104
+ */
102
105
  export function ownsWriteTool(): boolean {
103
106
  return writeOwnership().owned !== false;
104
107
  }
105
108
 
109
+ /** 当前占用 write 的其他扩展;builtin 或未注册时返回 undefined。 */
106
110
  function findExternalWriteOwner(pi: ExtensionAPI): ExternalWriteOwner | undefined {
107
111
  try {
108
112
  const tools = pi.getAllTools() as any[];
@@ -110,12 +114,9 @@ function findExternalWriteOwner(pi: ExtensionAPI): ExternalWriteOwner | undefine
110
114
  const sourceInfo = write?.sourceInfo;
111
115
  const source = sourceInfo?.source;
112
116
  if (!write || typeof source !== "string" || source === "builtin") return undefined;
113
- // Our own override from an earlier session_start in this runtime.
114
- if (ownWriteParameters !== undefined && write.parameters === ownWriteParameters)
115
- return undefined;
116
117
  return { source, path: typeof sourceInfo?.path === "string" ? sourceInfo.path : "" };
117
118
  } catch {
118
- // getAllTools is unavailable before the extension runtime is bound.
119
+ // getAllTools 在扩展运行时绑定前不可用。
119
120
  return undefined;
120
121
  }
121
122
  }
@@ -123,20 +124,20 @@ function findExternalWriteOwner(pi: ExtensionAPI): ExternalWriteOwner | undefine
123
124
  export function installWriteOverride(
124
125
  pi: ExtensionAPI,
125
126
  store = new WriteExecutionMetadataStore(),
126
- /** Called when another extension already owns write. */
127
+ /** write 已被其他扩展占用时回调一次,调用方据此提示冲突。 */
127
128
  onExternalOwner?: (owner: ExternalWriteOwner) => void,
128
129
  ): WriteExecutionMetadataStore {
129
130
  if (typeof (pi as any).registerTool !== "function") return store;
130
131
  const state = writeOwnership();
131
132
  const external = findExternalWriteOwner(pi);
132
133
  if (external) {
134
+ // 让位:执行与 diff 都归对方,渲染层据 owned=false 走普通结果行。
133
135
  state.owned = false;
134
136
  onExternalOwner?.(external);
135
137
  return store;
136
138
  }
137
139
  state.owned = true;
138
140
  const nativeWrite = createWriteToolDefinition(process.cwd()) as any;
139
- ownWriteParameters = nativeWrite.parameters;
140
141
  pi.registerTool({
141
142
  ...nativeWrite,
142
143
  async execute(
@@ -157,6 +158,6 @@ export {
157
158
  type ToolDisplayConfig,
158
159
  type DiffViewMode,
159
160
  type DiffIndicatorMode,
160
- } from "./types.ts";
161
+ } from "../../../config/config.ts";
161
162
  export type { DisplayConfigInput } from "./diff-renderer.ts";
162
163
  export { WriteExecutionMetadataStore } from "./write-execution.ts";