@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,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,6 +60,8 @@ export function renderRichToolResult(
59
60
  );
60
61
  }
61
62
  if (toolName !== "write") return undefined;
63
+ // 让位点:write 归其他扩展时不提供富 diff,交回普通结果行。
64
+ if (!ownsWriteTool()) return undefined;
62
65
 
63
66
  const metadata = writeMetadata.get(context?.toolCallId);
64
67
  if (!metadata) {
@@ -83,23 +86,57 @@ export function renderRichToolResult(
83
86
  );
84
87
  }
85
88
 
86
- function hasExternalWriteOwner(pi: ExtensionAPI): boolean {
89
+ /** write 被其他扩展占用时的来源,用于提示冲突。 */
90
+ export type ExternalWriteOwner = { source: string; path: string };
91
+
92
+ type WriteOwnershipState = {
93
+ /** write 是否由本插件执行;undefined 表示尚未确认,按拥有处理以保持既有行为。 */
94
+ owned?: boolean;
95
+ };
96
+
97
+ function writeOwnership(): WriteOwnershipState {
98
+ return patchRegistry.ensure<WriteOwnershipState>(WRITE_OWNERSHIP_SLOT, () => ({}));
99
+ }
100
+
101
+ /**
102
+ * write 是否由本插件执行。只有注册过 write override 才有执行元数据,
103
+ * 否则渲染层要放行给普通结果行,避免每张卡降级成 "diff unavailable"。
104
+ */
105
+ export function ownsWriteTool(): boolean {
106
+ return writeOwnership().owned !== false;
107
+ }
108
+
109
+ /** 当前占用 write 的其他扩展;builtin 或未注册时返回 undefined。 */
110
+ function findExternalWriteOwner(pi: ExtensionAPI): ExternalWriteOwner | undefined {
87
111
  try {
88
- const tools = pi.getAllTools();
89
- const write = tools.find((tool: any) => tool?.name === "write") as any;
90
- const source = write?.sourceInfo?.source;
91
- return Boolean(write && typeof source === "string" && source !== "builtin");
112
+ const tools = pi.getAllTools() as any[];
113
+ const write = tools?.find((tool: any) => tool?.name === "write") as any;
114
+ const sourceInfo = write?.sourceInfo;
115
+ const source = sourceInfo?.source;
116
+ if (!write || typeof source !== "string" || source === "builtin") return undefined;
117
+ return { source, path: typeof sourceInfo?.path === "string" ? sourceInfo.path : "" };
92
118
  } catch {
93
- // getAllTools is unavailable before the extension runtime is bound.
94
- return false;
119
+ // getAllTools 在扩展运行时绑定前不可用。
120
+ return undefined;
95
121
  }
96
122
  }
97
123
 
98
124
  export function installWriteOverride(
99
125
  pi: ExtensionAPI,
100
126
  store = new WriteExecutionMetadataStore(),
127
+ /** write 已被其他扩展占用时回调一次,调用方据此提示冲突。 */
128
+ onExternalOwner?: (owner: ExternalWriteOwner) => void,
101
129
  ): WriteExecutionMetadataStore {
102
- if (typeof (pi as any).registerTool !== "function" || hasExternalWriteOwner(pi)) return store;
130
+ if (typeof (pi as any).registerTool !== "function") return store;
131
+ const state = writeOwnership();
132
+ const external = findExternalWriteOwner(pi);
133
+ if (external) {
134
+ // 让位:执行与 diff 都归对方,渲染层据 owned=false 走普通结果行。
135
+ state.owned = false;
136
+ onExternalOwner?.(external);
137
+ return store;
138
+ }
139
+ state.owned = true;
103
140
  const nativeWrite = createWriteToolDefinition(process.cwd()) as any;
104
141
  pi.registerTool({
105
142
  ...nativeWrite,
@@ -121,6 +158,6 @@ export {
121
158
  type ToolDisplayConfig,
122
159
  type DiffViewMode,
123
160
  type DiffIndicatorMode,
124
- } from "./types.ts";
161
+ } from "../../../config/config.ts";
125
162
  export type { DisplayConfigInput } from "./diff-renderer.ts";
126
163
  export { WriteExecutionMetadataStore } from "./write-execution.ts";