@oh-my-pi/pi-coding-agent 13.18.0 → 14.0.2

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 (235) hide show
  1. package/CHANGELOG.md +316 -1
  2. package/package.json +86 -24
  3. package/scripts/format-prompts.ts +2 -2
  4. package/src/autoresearch/apply-contract-to-state.ts +24 -0
  5. package/src/autoresearch/contract.ts +0 -44
  6. package/src/autoresearch/dashboard.ts +1 -2
  7. package/src/autoresearch/git.ts +116 -30
  8. package/src/autoresearch/helpers.ts +49 -0
  9. package/src/autoresearch/index.ts +28 -187
  10. package/src/autoresearch/prompt.md +26 -9
  11. package/src/autoresearch/state.ts +0 -6
  12. package/src/autoresearch/tools/init-experiment.ts +202 -117
  13. package/src/autoresearch/tools/log-experiment.ts +123 -178
  14. package/src/autoresearch/tools/run-experiment.ts +48 -10
  15. package/src/autoresearch/types.ts +2 -2
  16. package/src/capability/index.ts +4 -2
  17. package/src/cli/file-processor.ts +3 -3
  18. package/src/cli/grep-cli.ts +8 -8
  19. package/src/cli/grievances-cli.ts +78 -0
  20. package/src/cli/read-cli.ts +67 -0
  21. package/src/cli/setup-cli.ts +4 -4
  22. package/src/cli/update-cli.ts +3 -3
  23. package/src/cli.ts +2 -0
  24. package/src/commands/grep.ts +6 -1
  25. package/src/commands/grievances.ts +20 -0
  26. package/src/commands/read.ts +33 -0
  27. package/src/commit/agentic/agent.ts +5 -8
  28. package/src/commit/agentic/index.ts +22 -26
  29. package/src/commit/agentic/tools/analyze-file.ts +3 -3
  30. package/src/commit/agentic/tools/git-file-diff.ts +3 -6
  31. package/src/commit/agentic/tools/git-hunk.ts +3 -3
  32. package/src/commit/agentic/tools/git-overview.ts +6 -9
  33. package/src/commit/agentic/tools/index.ts +6 -8
  34. package/src/commit/agentic/tools/propose-commit.ts +4 -7
  35. package/src/commit/agentic/tools/recent-commits.ts +3 -3
  36. package/src/commit/agentic/tools/split-commit.ts +4 -4
  37. package/src/commit/agentic/validation.ts +1 -1
  38. package/src/commit/analysis/conventional.ts +4 -4
  39. package/src/commit/analysis/summary.ts +3 -3
  40. package/src/commit/changelog/generate.ts +4 -4
  41. package/src/commit/changelog/index.ts +5 -9
  42. package/src/commit/map-reduce/map-phase.ts +4 -4
  43. package/src/commit/map-reduce/reduce-phase.ts +4 -4
  44. package/src/commit/pipeline.ts +13 -16
  45. package/src/config/keybindings.ts +7 -6
  46. package/src/config/prompt-templates.ts +44 -226
  47. package/src/config/resolve-config-value.ts +4 -2
  48. package/src/config/settings-schema.ts +98 -2
  49. package/src/config/settings.ts +25 -26
  50. package/src/dap/client.ts +674 -0
  51. package/src/dap/config.ts +150 -0
  52. package/src/dap/defaults.json +211 -0
  53. package/src/dap/index.ts +4 -0
  54. package/src/dap/session.ts +1255 -0
  55. package/src/dap/types.ts +600 -0
  56. package/src/debug/log-viewer.ts +3 -2
  57. package/src/discovery/builtin.ts +1 -2
  58. package/src/discovery/codex.ts +2 -2
  59. package/src/discovery/github.ts +2 -1
  60. package/src/discovery/helpers.ts +2 -2
  61. package/src/discovery/opencode.ts +2 -2
  62. package/src/edit/diff.ts +818 -0
  63. package/src/edit/index.ts +309 -0
  64. package/src/edit/line-hash.ts +67 -0
  65. package/src/edit/modes/chunk.ts +454 -0
  66. package/src/{patch → edit/modes}/hashline.ts +741 -361
  67. package/src/{patch/applicator.ts → edit/modes/patch.ts} +420 -117
  68. package/src/{patch/fuzzy.ts → edit/modes/replace.ts} +519 -197
  69. package/src/{patch → edit}/normalize.ts +97 -76
  70. package/src/{patch/shared.ts → edit/renderer.ts} +181 -108
  71. package/src/exec/bash-executor.ts +4 -2
  72. package/src/exec/idle-timeout-watchdog.ts +126 -0
  73. package/src/exec/non-interactive-env.ts +5 -0
  74. package/src/extensibility/custom-commands/bundled/ci-green/index.ts +6 -18
  75. package/src/extensibility/custom-commands/bundled/review/index.ts +45 -43
  76. package/src/extensibility/custom-commands/loader.ts +1 -2
  77. package/src/extensibility/custom-tools/loader.ts +34 -11
  78. package/src/extensibility/custom-tools/types.ts +1 -1
  79. package/src/extensibility/extensions/loader.ts +9 -4
  80. package/src/extensibility/extensions/runner.ts +24 -1
  81. package/src/extensibility/extensions/types.ts +4 -2
  82. package/src/extensibility/hooks/loader.ts +5 -6
  83. package/src/extensibility/hooks/types.ts +2 -2
  84. package/src/extensibility/plugins/doctor.ts +2 -1
  85. package/src/extensibility/plugins/marketplace/fetcher.ts +2 -57
  86. package/src/extensibility/plugins/marketplace/source-resolver.ts +4 -4
  87. package/src/extensibility/slash-commands.ts +3 -7
  88. package/src/index.ts +3 -1
  89. package/src/internal-urls/docs-index.generated.ts +11 -11
  90. package/src/ipy/executor.ts +58 -17
  91. package/src/ipy/gateway-coordinator.ts +6 -4
  92. package/src/ipy/kernel.ts +45 -22
  93. package/src/ipy/runtime.ts +2 -2
  94. package/src/lsp/client.ts +7 -4
  95. package/src/lsp/clients/lsp-linter-client.ts +4 -4
  96. package/src/lsp/config.ts +2 -2
  97. package/src/lsp/defaults.json +688 -154
  98. package/src/lsp/index.ts +234 -45
  99. package/src/lsp/lspmux.ts +2 -2
  100. package/src/lsp/startup-events.ts +13 -0
  101. package/src/lsp/types.ts +12 -1
  102. package/src/lsp/utils.ts +8 -1
  103. package/src/main.ts +125 -47
  104. package/src/memories/index.ts +4 -5
  105. package/src/modes/acp/acp-agent.ts +563 -163
  106. package/src/modes/acp/acp-event-mapper.ts +9 -1
  107. package/src/modes/acp/acp-mode.ts +4 -2
  108. package/src/modes/components/agent-dashboard.ts +3 -4
  109. package/src/modes/components/diff.ts +6 -7
  110. package/src/modes/components/footer.ts +9 -29
  111. package/src/modes/components/hook-editor.ts +3 -3
  112. package/src/modes/components/hook-selector.ts +6 -1
  113. package/src/modes/components/read-tool-group.ts +6 -12
  114. package/src/modes/components/session-observer-overlay.ts +472 -0
  115. package/src/modes/components/settings-defs.ts +24 -0
  116. package/src/modes/components/status-line.ts +15 -61
  117. package/src/modes/components/tool-execution.ts +1 -1
  118. package/src/modes/components/welcome.ts +1 -1
  119. package/src/modes/controllers/btw-controller.ts +2 -2
  120. package/src/modes/controllers/command-controller.ts +4 -2
  121. package/src/modes/controllers/event-controller.ts +59 -2
  122. package/src/modes/controllers/extension-ui-controller.ts +1 -0
  123. package/src/modes/controllers/input-controller.ts +15 -8
  124. package/src/modes/controllers/selector-controller.ts +26 -0
  125. package/src/modes/index.ts +20 -2
  126. package/src/modes/interactive-mode.ts +278 -69
  127. package/src/modes/rpc/host-tools.ts +186 -0
  128. package/src/modes/rpc/rpc-client.ts +178 -13
  129. package/src/modes/rpc/rpc-mode.ts +73 -3
  130. package/src/modes/rpc/rpc-types.ts +53 -1
  131. package/src/modes/session-observer-registry.ts +146 -0
  132. package/src/modes/shared.ts +0 -42
  133. package/src/modes/theme/theme.ts +80 -8
  134. package/src/modes/types.ts +4 -2
  135. package/src/modes/utils/keybinding-matchers.ts +9 -0
  136. package/src/prompts/system/custom-system-prompt.md +5 -0
  137. package/src/prompts/system/system-prompt.md +8 -1
  138. package/src/prompts/tools/chunk-edit.md +219 -0
  139. package/src/prompts/tools/debug.md +43 -0
  140. package/src/prompts/tools/grep.md +3 -0
  141. package/src/prompts/tools/lsp.md +5 -5
  142. package/src/prompts/tools/read-chunk.md +17 -0
  143. package/src/prompts/tools/read.md +19 -5
  144. package/src/sdk.ts +216 -165
  145. package/src/secrets/index.ts +1 -1
  146. package/src/secrets/obfuscator.ts +25 -17
  147. package/src/session/agent-session.ts +381 -286
  148. package/src/session/agent-storage.ts +12 -12
  149. package/src/session/compaction/branch-summarization.ts +3 -3
  150. package/src/session/compaction/compaction.ts +5 -6
  151. package/src/session/compaction/utils.ts +3 -3
  152. package/src/session/history-storage.ts +62 -19
  153. package/src/session/messages.ts +3 -3
  154. package/src/session/session-dump-format.ts +203 -0
  155. package/src/session/session-manager.ts +15 -5
  156. package/src/session/session-storage.ts +4 -2
  157. package/src/session/streaming-output.ts +1 -1
  158. package/src/session/tool-choice-queue.ts +213 -0
  159. package/src/slash-commands/builtin-registry.ts +56 -8
  160. package/src/ssh/connection-manager.ts +2 -2
  161. package/src/ssh/sshfs-mount.ts +5 -5
  162. package/src/stt/downloader.ts +4 -4
  163. package/src/stt/recorder.ts +4 -4
  164. package/src/stt/transcriber.ts +2 -2
  165. package/src/system-prompt.ts +25 -13
  166. package/src/task/agents.ts +5 -6
  167. package/src/task/commands.ts +2 -5
  168. package/src/task/executor.ts +32 -4
  169. package/src/task/index.ts +91 -82
  170. package/src/task/template.ts +2 -2
  171. package/src/task/types.ts +25 -0
  172. package/src/task/worktree.ts +131 -149
  173. package/src/tools/ask.ts +2 -3
  174. package/src/tools/ast-edit.ts +7 -7
  175. package/src/tools/ast-grep.ts +7 -7
  176. package/src/tools/auto-generated-guard.ts +36 -41
  177. package/src/tools/await-tool.ts +2 -2
  178. package/src/tools/bash.ts +5 -23
  179. package/src/tools/browser.ts +4 -5
  180. package/src/tools/calculator.ts +2 -3
  181. package/src/tools/cancel-job.ts +2 -2
  182. package/src/tools/checkpoint.ts +3 -3
  183. package/src/tools/debug.ts +1007 -0
  184. package/src/tools/exit-plan-mode.ts +3 -3
  185. package/src/tools/fetch.ts +67 -3
  186. package/src/tools/find.ts +4 -5
  187. package/src/tools/fs-cache-invalidation.ts +5 -0
  188. package/src/tools/gemini-image.ts +13 -5
  189. package/src/tools/gh.ts +130 -308
  190. package/src/tools/grep.ts +57 -9
  191. package/src/tools/index.ts +44 -22
  192. package/src/tools/inspect-image.ts +4 -4
  193. package/src/tools/output-meta.ts +1 -1
  194. package/src/tools/python.ts +19 -6
  195. package/src/tools/read.ts +211 -146
  196. package/src/tools/render-mermaid.ts +2 -3
  197. package/src/tools/render-utils.ts +20 -6
  198. package/src/tools/renderers.ts +3 -1
  199. package/src/tools/report-tool-issue.ts +80 -0
  200. package/src/tools/resolve.ts +70 -39
  201. package/src/tools/search-tool-bm25.ts +2 -2
  202. package/src/tools/ssh.ts +2 -2
  203. package/src/tools/todo-write.ts +2 -2
  204. package/src/tools/tool-timeouts.ts +1 -0
  205. package/src/tools/write.ts +5 -6
  206. package/src/tui/tree-list.ts +3 -1
  207. package/src/utils/clipboard.ts +80 -0
  208. package/src/utils/commit-message-generator.ts +2 -3
  209. package/src/utils/edit-mode.ts +49 -0
  210. package/src/utils/external-editor.ts +11 -5
  211. package/src/utils/file-display-mode.ts +6 -5
  212. package/src/utils/file-mentions.ts +8 -7
  213. package/src/utils/git.ts +1400 -0
  214. package/src/utils/image-loading.ts +98 -0
  215. package/src/utils/title-generator.ts +2 -3
  216. package/src/utils/tools-manager.ts +6 -6
  217. package/src/web/scrapers/choosealicense.ts +1 -1
  218. package/src/web/search/index.ts +3 -3
  219. package/src/web/search/render.ts +6 -4
  220. package/src/autoresearch/command-initialize.md +0 -34
  221. package/src/commit/git/errors.ts +0 -9
  222. package/src/commit/git/index.ts +0 -210
  223. package/src/commit/git/operations.ts +0 -54
  224. package/src/patch/diff.ts +0 -433
  225. package/src/patch/index.ts +0 -888
  226. package/src/patch/parser.ts +0 -532
  227. package/src/patch/types.ts +0 -292
  228. package/src/prompts/agents/oracle.md +0 -77
  229. package/src/tools/gh-cli.ts +0 -125
  230. package/src/tools/pending-action.ts +0 -49
  231. package/src/utils/child-process.ts +0 -88
  232. package/src/utils/frontmatter.ts +0 -117
  233. package/src/utils/image-input.ts +0 -274
  234. package/src/utils/mime.ts +0 -53
  235. package/src/utils/prompt-format.ts +0 -170
@@ -0,0 +1,454 @@
1
+ import * as fs from "node:fs/promises";
2
+ import * as nodePath from "node:path";
3
+ import type { AgentToolResult } from "@oh-my-pi/pi-agent-core";
4
+ import { StringEnum } from "@oh-my-pi/pi-coding-agent";
5
+ import {
6
+ ChunkAnchorStyle,
7
+ ChunkEditOp,
8
+ type ChunkInfo,
9
+ ChunkReadStatus,
10
+ type ChunkReadTarget,
11
+ ChunkState,
12
+ type EditOperation as NativeEditOperation,
13
+ } from "@oh-my-pi/pi-natives";
14
+ import { type Static, Type } from "@sinclair/typebox";
15
+ import type { BunFile } from "bun";
16
+ import { LRUCache } from "lru-cache";
17
+ import type { Settings } from "../../config/settings";
18
+ import type { WritethroughCallback, WritethroughDeferredHandle } from "../../lsp";
19
+ import { getLanguageFromPath } from "../../modes/theme/theme";
20
+ import type { ToolSession } from "../../tools";
21
+ import { assertEditableFileContent } from "../../tools/auto-generated-guard";
22
+ import { invalidateFsScanAfterWrite } from "../../tools/fs-cache-invalidation";
23
+ import { outputMeta } from "../../tools/output-meta";
24
+ import { enforcePlanModeWrite, resolvePlanPath } from "../../tools/plan-mode-guard";
25
+ import { generateUnifiedDiffString } from "../diff";
26
+ import { detectLineEnding, normalizeToLF, restoreLineEndings, stripBom } from "../normalize";
27
+ import type { EditToolDetails, LspBatchRequest } from "../renderer";
28
+
29
+ export type { ChunkReadTarget };
30
+
31
+ export type ChunkEditOperation =
32
+ | { op: "replace"; sel?: string; content: string }
33
+ | { op: "before"; sel?: string; content: string }
34
+ | { op: "after"; sel?: string; content: string }
35
+ | { op: "prepend"; sel?: string; content: string }
36
+ | { op: "append"; sel?: string; content: string };
37
+
38
+ type ChunkEditResult = {
39
+ diffSourceBefore: string;
40
+ diffSourceAfter: string;
41
+ responseText: string;
42
+ changed: boolean;
43
+ parseValid: boolean;
44
+ touchedPaths: string[];
45
+ warnings: string[];
46
+ };
47
+
48
+ export type ParsedChunkReadPath = {
49
+ filePath: string;
50
+ selector?: string;
51
+ };
52
+
53
+ type ChunkCacheEntry = {
54
+ mtimeMs: number;
55
+ size: number;
56
+ source: string;
57
+ state: ChunkState;
58
+ };
59
+
60
+ const validAnchorStyles: Record<string, ChunkAnchorStyle> = {
61
+ full: ChunkAnchorStyle.Full,
62
+ kind: ChunkAnchorStyle.Kind,
63
+ bare: ChunkAnchorStyle.Bare,
64
+ };
65
+
66
+ export function resolveAnchorStyle(settings?: Settings): ChunkAnchorStyle {
67
+ const envStyle = Bun.env.PI_ANCHOR_STYLE;
68
+ return (
69
+ (envStyle && validAnchorStyles[envStyle]) ||
70
+ (settings?.get("read.anchorstyle") as ChunkAnchorStyle | undefined) ||
71
+ ChunkAnchorStyle.Full
72
+ );
73
+ }
74
+
75
+ const readEnvInt = (name: string, defaultValue: number): number => {
76
+ const value = Bun.env[name];
77
+ if (!value) return defaultValue;
78
+ const parsed = Number.parseInt(value, 10);
79
+ if (Number.isNaN(parsed) || parsed <= 0) return defaultValue;
80
+ return parsed;
81
+ };
82
+
83
+ const chunkStateCache = new LRUCache<string, ChunkCacheEntry>({
84
+ max: readEnvInt("PI_CHUNK_CACHE_MAX_ENTRIES", 200),
85
+ });
86
+
87
+ export function invalidateChunkCache(filePath: string): void {
88
+ chunkStateCache.delete(filePath);
89
+ }
90
+
91
+ type ChunkSourceContext = {
92
+ resolvedPath: string;
93
+ sourceFile: BunFile;
94
+ sourceExists: boolean;
95
+ rawContent: string;
96
+ chunkLanguage: string | undefined;
97
+ };
98
+
99
+ function normalizeLanguage(language: string | undefined): string {
100
+ return language?.trim().toLowerCase() || "";
101
+ }
102
+
103
+ function normalizeChunkSource(text: string): string {
104
+ return normalizeToLF(stripBom(text).text);
105
+ }
106
+
107
+ function displayPathForFile(filePath: string, cwd: string): string {
108
+ const relative = nodePath.relative(cwd, filePath).replace(/\\/g, "/");
109
+ return relative && !relative.startsWith("..") ? relative : filePath.replace(/\\/g, "/");
110
+ }
111
+
112
+ function fileLanguageTag(filePath: string, language?: string): string | undefined {
113
+ const normalizedLanguage = normalizeLanguage(language);
114
+ if (normalizedLanguage.length > 0) return normalizedLanguage;
115
+ const ext = nodePath.extname(filePath).replace(/^\./, "").toLowerCase();
116
+ return ext.length > 0 ? ext : undefined;
117
+ }
118
+
119
+ async function resolveChunkSourceContext(session: ToolSession, path: string): Promise<ChunkSourceContext> {
120
+ const resolvedPath = resolvePlanPath(session, path);
121
+ const sourceFile = Bun.file(resolvedPath);
122
+ const sourceExists = await sourceFile.exists();
123
+ enforcePlanModeWrite(session, path, { op: sourceExists ? "update" : "create" });
124
+
125
+ let rawContent = "";
126
+ if (sourceExists) {
127
+ rawContent = await sourceFile.text();
128
+ assertEditableFileContent(rawContent, path);
129
+ }
130
+
131
+ return {
132
+ resolvedPath,
133
+ sourceFile,
134
+ sourceExists,
135
+ rawContent,
136
+ chunkLanguage: getLanguageFromPath(resolvedPath),
137
+ };
138
+ }
139
+
140
+ function buildChunkEditResult(result: {
141
+ diffBefore: string;
142
+ diffAfter: string;
143
+ responseText: string;
144
+ changed: boolean;
145
+ parseValid: boolean;
146
+ touchedPaths: string[];
147
+ warnings: string[];
148
+ }): ChunkEditResult {
149
+ return {
150
+ diffSourceBefore: result.diffBefore,
151
+ diffSourceAfter: result.diffAfter,
152
+ responseText: result.responseText,
153
+ changed: result.changed,
154
+ parseValid: result.parseValid,
155
+ touchedPaths: result.touchedPaths,
156
+ warnings: result.warnings,
157
+ };
158
+ }
159
+
160
+ function chunkReadPathSeparatorIndex(readPath: string): number {
161
+ if (/^[a-zA-Z]:[/\\]/.test(readPath)) {
162
+ return readPath.indexOf(":", 2);
163
+ }
164
+ return readPath.indexOf(":");
165
+ }
166
+
167
+ export function parseChunkSelector(selector: string | undefined): { selector?: string } {
168
+ if (!selector || selector.length === 0) {
169
+ return {};
170
+ }
171
+ return { selector };
172
+ }
173
+
174
+ export function parseChunkReadPath(readPath: string): ParsedChunkReadPath {
175
+ const colonIndex = chunkReadPathSeparatorIndex(readPath);
176
+ if (colonIndex === -1) {
177
+ return { filePath: readPath };
178
+ }
179
+ const parsedSelector = parseChunkSelector(readPath.slice(colonIndex + 1) || undefined);
180
+ return {
181
+ filePath: readPath.slice(0, colonIndex),
182
+ selector: parsedSelector.selector,
183
+ };
184
+ }
185
+
186
+ export function isChunkReadablePath(readPath: string): boolean {
187
+ return parseChunkReadPath(readPath).selector !== undefined;
188
+ }
189
+
190
+ export async function loadChunkStateForFile(filePath: string, language: string | undefined): Promise<ChunkCacheEntry> {
191
+ const file = Bun.file(filePath);
192
+ const stat = await file.stat();
193
+ const cached = chunkStateCache.get(filePath);
194
+ if (cached && cached.mtimeMs === stat.mtimeMs && cached.size === stat.size) {
195
+ return cached;
196
+ }
197
+
198
+ const source = normalizeChunkSource(await file.text());
199
+ const state = ChunkState.parse(source, normalizeLanguage(language));
200
+ const entry = { mtimeMs: stat.mtimeMs, size: stat.size, source, state };
201
+ chunkStateCache.set(filePath, entry);
202
+ return entry;
203
+ }
204
+
205
+ export async function formatChunkedRead(params: {
206
+ filePath: string;
207
+ readPath: string;
208
+ cwd: string;
209
+ language?: string;
210
+ omitChecksum?: boolean;
211
+ anchorStyle?: ChunkAnchorStyle;
212
+ absoluteLineRange?: { startLine: number; endLine?: number };
213
+ }): Promise<{ text: string; resolvedPath?: string; chunk?: ChunkReadTarget }> {
214
+ const { filePath, readPath, cwd, language, omitChecksum = false, anchorStyle, absoluteLineRange } = params;
215
+ const normalizedLanguage = normalizeLanguage(language);
216
+ const { state } = await loadChunkStateForFile(filePath, normalizedLanguage);
217
+ const displayPath = displayPathForFile(filePath, cwd);
218
+ const result = state.renderRead({
219
+ readPath,
220
+ displayPath,
221
+ languageTag: fileLanguageTag(filePath, normalizedLanguage),
222
+ omitChecksum,
223
+ anchorStyle,
224
+ absoluteLineRange: absoluteLineRange
225
+ ? { startLine: absoluteLineRange.startLine, endLine: absoluteLineRange.endLine ?? absoluteLineRange.startLine }
226
+ : undefined,
227
+ tabReplacement: " ",
228
+ normalizeIndent: true,
229
+ });
230
+ return { text: result.text, resolvedPath: filePath, chunk: result.chunk };
231
+ }
232
+
233
+ export async function formatChunkedGrepLine(params: {
234
+ filePath: string;
235
+ lineNumber: number;
236
+ line: string;
237
+ cwd: string;
238
+ language?: string;
239
+ }): Promise<string> {
240
+ const { filePath, lineNumber, line, cwd, language } = params;
241
+ const { state } = await loadChunkStateForFile(filePath, language);
242
+ return state.formatGrepLine(displayPathForFile(filePath, cwd), lineNumber, line);
243
+ }
244
+
245
+ function toNativeEditOperation(operation: ChunkEditOperation): NativeEditOperation {
246
+ switch (operation.op) {
247
+ case "replace":
248
+ return {
249
+ op: ChunkEditOp.Replace,
250
+ sel: operation.sel,
251
+ content: operation.content,
252
+ };
253
+ case "before":
254
+ return { op: ChunkEditOp.Before, sel: operation.sel, content: operation.content };
255
+ case "after":
256
+ return { op: ChunkEditOp.After, sel: operation.sel, content: operation.content };
257
+ case "prepend":
258
+ return { op: ChunkEditOp.Prepend, sel: operation.sel, content: operation.content };
259
+ case "append":
260
+ return { op: ChunkEditOp.Append, sel: operation.sel, content: operation.content };
261
+ default: {
262
+ const exhaustive: never = operation;
263
+ return exhaustive;
264
+ }
265
+ }
266
+ }
267
+
268
+ export function applyChunkEdits(params: {
269
+ source: string;
270
+ language?: string;
271
+ cwd: string;
272
+ filePath: string;
273
+ operations: ChunkEditOperation[];
274
+ defaultSelector?: string;
275
+ defaultCrc?: string;
276
+ anchorStyle?: ChunkAnchorStyle;
277
+ }): ChunkEditResult {
278
+ const normalizedSource = normalizeChunkSource(params.source);
279
+ const nativeOperations = params.operations.map(toNativeEditOperation);
280
+ const state = ChunkState.parse(normalizedSource, normalizeLanguage(params.language));
281
+ const result = state.applyEdits({
282
+ operations: nativeOperations,
283
+ defaultSelector: params.defaultSelector,
284
+ defaultCrc: params.defaultCrc,
285
+ anchorStyle: params.anchorStyle,
286
+ cwd: params.cwd,
287
+ filePath: params.filePath,
288
+ });
289
+
290
+ return buildChunkEditResult(result);
291
+ }
292
+
293
+ export async function getChunkInfoForFile(
294
+ filePath: string,
295
+ language: string | undefined,
296
+ chunkPath: string,
297
+ ): Promise<ChunkInfo | undefined> {
298
+ const { state } = await loadChunkStateForFile(filePath, language);
299
+ return state.chunk(chunkPath) ?? undefined;
300
+ }
301
+
302
+ export function missingChunkReadTarget(selector: string): ChunkReadTarget {
303
+ return { status: ChunkReadStatus.NotFound, selector };
304
+ }
305
+
306
+ const CHUNK_OP_VALUES = ["replace", "after", "before", "prepend", "append"] as const;
307
+
308
+ export const chunkToolEditSchema = Type.Object({
309
+ op: StringEnum(CHUNK_OP_VALUES),
310
+ sel: Type.String({
311
+ description:
312
+ "Chunk selector. Format: 'path@region' for insertions, 'path#CRC@region' for replace. Omit @region to target the full chunk. Valid regions: head, body, tail, decl.",
313
+ }),
314
+ content: Type.String({
315
+ description: "New content. Use one leading space per indent level; do not include the chunk's base padding.",
316
+ }),
317
+ });
318
+ export const chunkEditParamsSchema = Type.Object(
319
+ {
320
+ path: Type.String({ description: "File path" }),
321
+ edits: Type.Array(chunkToolEditSchema, {
322
+ description: "Chunk edits",
323
+ minItems: 1,
324
+ }),
325
+ },
326
+ { additionalProperties: false },
327
+ );
328
+
329
+ export type ChunkToolEdit = Static<typeof chunkToolEditSchema>;
330
+ export type ChunkParams = Static<typeof chunkEditParamsSchema>;
331
+
332
+ interface ExecuteChunkModeOptions {
333
+ session: ToolSession;
334
+ params: ChunkParams;
335
+ signal?: AbortSignal;
336
+ batchRequest?: LspBatchRequest;
337
+ writethrough: WritethroughCallback;
338
+ beginDeferredDiagnosticsForPath: (path: string) => WritethroughDeferredHandle;
339
+ }
340
+
341
+ export function isChunkParams(params: unknown): params is ChunkParams {
342
+ return (
343
+ typeof params === "object" &&
344
+ params !== null &&
345
+ "edits" in params &&
346
+ Array.isArray(params.edits) &&
347
+ params.edits.length > 0 &&
348
+ typeof params.edits[0] === "object" &&
349
+ params.edits[0] !== null &&
350
+ "sel" in params.edits[0]
351
+ );
352
+ }
353
+
354
+ function normalizeChunkEditOperations(edits: ChunkToolEdit[]): ChunkEditOperation[] {
355
+ return edits as ChunkEditOperation[];
356
+ }
357
+
358
+ async function writeChunkResult(params: {
359
+ result: ChunkEditResult;
360
+ resolvedPath: string;
361
+ sourceFile: BunFile;
362
+ sourceText: string;
363
+ sourceExists: boolean;
364
+ signal?: AbortSignal;
365
+ batchRequest?: LspBatchRequest;
366
+ writethrough: WritethroughCallback;
367
+ beginDeferredDiagnosticsForPath: (path: string) => WritethroughDeferredHandle;
368
+ }): Promise<AgentToolResult<EditToolDetails, typeof chunkEditParamsSchema>> {
369
+ const {
370
+ result,
371
+ resolvedPath,
372
+ sourceFile,
373
+ sourceText,
374
+ sourceExists,
375
+ signal,
376
+ batchRequest,
377
+ writethrough,
378
+ beginDeferredDiagnosticsForPath,
379
+ } = params;
380
+
381
+ const { bom, text } = stripBom(sourceText);
382
+ const originalEnding = detectLineEnding(text);
383
+ const finalContent = bom + restoreLineEndings(result.diffSourceAfter, originalEnding);
384
+ const diagnostics = await writethrough(resolvedPath, finalContent, signal, sourceFile, batchRequest, dst =>
385
+ dst === resolvedPath ? beginDeferredDiagnosticsForPath(resolvedPath) : undefined,
386
+ );
387
+ invalidateFsScanAfterWrite(resolvedPath);
388
+
389
+ const diffResult = generateUnifiedDiffString(result.diffSourceBefore, result.diffSourceAfter);
390
+ const warningsBlock = result.warnings.length > 0 ? `\n\n${result.warnings.join("\n")}` : "";
391
+ const meta = outputMeta()
392
+ .diagnostics(diagnostics?.summary ?? "", diagnostics?.messages ?? [])
393
+ .get();
394
+
395
+ return {
396
+ content: [{ type: "text", text: `${result.responseText}${warningsBlock}` }],
397
+ details: {
398
+ diff: diffResult.diff,
399
+ firstChangedLine: diffResult.firstChangedLine,
400
+ diagnostics,
401
+ op: sourceExists ? "update" : "create",
402
+ meta,
403
+ },
404
+ };
405
+ }
406
+
407
+ export async function executeChunkMode(
408
+ options: ExecuteChunkModeOptions,
409
+ ): Promise<AgentToolResult<EditToolDetails, typeof chunkEditParamsSchema>> {
410
+ const { session, params, signal, batchRequest, writethrough, beginDeferredDiagnosticsForPath } = options;
411
+ const { path, edits } = params;
412
+ const { resolvedPath, sourceFile, sourceExists, rawContent, chunkLanguage } = await resolveChunkSourceContext(
413
+ session,
414
+ path,
415
+ );
416
+ const parentDir = nodePath.dirname(resolvedPath);
417
+ if (parentDir && parentDir !== ".") {
418
+ await fs.mkdir(parentDir, { recursive: true });
419
+ }
420
+ const normalizedOperations = normalizeChunkEditOperations(edits);
421
+
422
+ const chunkResult = applyChunkEdits({
423
+ source: rawContent,
424
+ language: chunkLanguage,
425
+ cwd: session.cwd,
426
+ filePath: resolvedPath,
427
+ operations: normalizedOperations,
428
+ anchorStyle: resolveAnchorStyle(session.settings),
429
+ });
430
+
431
+ if (!chunkResult.changed) {
432
+ const responseText = `[No changes needed — content already matches.]\n\n${chunkResult.responseText}`;
433
+ return {
434
+ content: [{ type: "text", text: responseText }],
435
+ details: {
436
+ diff: "",
437
+ op: sourceExists ? "update" : "create",
438
+ meta: outputMeta().get(),
439
+ },
440
+ };
441
+ }
442
+
443
+ return writeChunkResult({
444
+ result: chunkResult,
445
+ resolvedPath,
446
+ sourceFile,
447
+ sourceText: rawContent,
448
+ sourceExists,
449
+ signal,
450
+ batchRequest,
451
+ writethrough,
452
+ beginDeferredDiagnosticsForPath,
453
+ });
454
+ }