@oh-my-pi/pi-coding-agent 13.19.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 (202) hide show
  1. package/CHANGELOG.md +266 -1
  2. package/package.json +86 -20
  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 +91 -0
  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 +83 -125
  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 -5
  28. package/src/commit/agentic/index.ts +3 -4
  29. package/src/commit/agentic/tools/analyze-file.ts +3 -3
  30. package/src/commit/agentic/validation.ts +1 -1
  31. package/src/commit/analysis/conventional.ts +4 -4
  32. package/src/commit/analysis/summary.ts +3 -3
  33. package/src/commit/changelog/generate.ts +4 -4
  34. package/src/commit/map-reduce/map-phase.ts +4 -4
  35. package/src/commit/map-reduce/reduce-phase.ts +4 -4
  36. package/src/commit/pipeline.ts +3 -4
  37. package/src/config/prompt-templates.ts +44 -226
  38. package/src/config/resolve-config-value.ts +4 -2
  39. package/src/config/settings-schema.ts +54 -2
  40. package/src/config/settings.ts +25 -26
  41. package/src/dap/client.ts +674 -0
  42. package/src/dap/config.ts +150 -0
  43. package/src/dap/defaults.json +211 -0
  44. package/src/dap/index.ts +4 -0
  45. package/src/dap/session.ts +1255 -0
  46. package/src/dap/types.ts +600 -0
  47. package/src/debug/log-viewer.ts +3 -2
  48. package/src/discovery/builtin.ts +1 -2
  49. package/src/discovery/codex.ts +2 -2
  50. package/src/discovery/github.ts +2 -1
  51. package/src/discovery/helpers.ts +2 -2
  52. package/src/discovery/opencode.ts +2 -2
  53. package/src/edit/diff.ts +818 -0
  54. package/src/edit/index.ts +309 -0
  55. package/src/edit/line-hash.ts +67 -0
  56. package/src/edit/modes/chunk.ts +454 -0
  57. package/src/{patch → edit/modes}/hashline.ts +741 -361
  58. package/src/{patch/applicator.ts → edit/modes/patch.ts} +420 -117
  59. package/src/{patch/fuzzy.ts → edit/modes/replace.ts} +519 -197
  60. package/src/{patch → edit}/normalize.ts +97 -76
  61. package/src/{patch/shared.ts → edit/renderer.ts} +181 -108
  62. package/src/exec/bash-executor.ts +4 -2
  63. package/src/exec/idle-timeout-watchdog.ts +126 -0
  64. package/src/exec/non-interactive-env.ts +5 -0
  65. package/src/extensibility/custom-commands/bundled/ci-green/index.ts +2 -2
  66. package/src/extensibility/custom-commands/bundled/review/index.ts +2 -2
  67. package/src/extensibility/custom-commands/loader.ts +1 -2
  68. package/src/extensibility/custom-tools/loader.ts +34 -11
  69. package/src/extensibility/extensions/loader.ts +9 -4
  70. package/src/extensibility/extensions/runner.ts +24 -1
  71. package/src/extensibility/extensions/types.ts +1 -1
  72. package/src/extensibility/hooks/loader.ts +5 -6
  73. package/src/extensibility/hooks/types.ts +1 -1
  74. package/src/extensibility/plugins/doctor.ts +2 -1
  75. package/src/extensibility/slash-commands.ts +3 -7
  76. package/src/index.ts +2 -1
  77. package/src/internal-urls/docs-index.generated.ts +11 -11
  78. package/src/ipy/executor.ts +58 -17
  79. package/src/ipy/gateway-coordinator.ts +6 -4
  80. package/src/ipy/kernel.ts +45 -22
  81. package/src/ipy/runtime.ts +2 -2
  82. package/src/lsp/client.ts +7 -4
  83. package/src/lsp/clients/lsp-linter-client.ts +4 -4
  84. package/src/lsp/config.ts +2 -2
  85. package/src/lsp/defaults.json +688 -154
  86. package/src/lsp/index.ts +234 -45
  87. package/src/lsp/lspmux.ts +2 -2
  88. package/src/lsp/startup-events.ts +13 -0
  89. package/src/lsp/types.ts +12 -1
  90. package/src/lsp/utils.ts +8 -1
  91. package/src/main.ts +102 -46
  92. package/src/memories/index.ts +4 -5
  93. package/src/modes/acp/acp-agent.ts +563 -163
  94. package/src/modes/acp/acp-event-mapper.ts +9 -1
  95. package/src/modes/acp/acp-mode.ts +4 -2
  96. package/src/modes/components/agent-dashboard.ts +3 -4
  97. package/src/modes/components/diff.ts +6 -7
  98. package/src/modes/components/read-tool-group.ts +6 -12
  99. package/src/modes/components/settings-defs.ts +5 -0
  100. package/src/modes/components/tool-execution.ts +1 -1
  101. package/src/modes/components/welcome.ts +1 -1
  102. package/src/modes/controllers/btw-controller.ts +2 -2
  103. package/src/modes/controllers/command-controller.ts +3 -2
  104. package/src/modes/controllers/input-controller.ts +12 -8
  105. package/src/modes/index.ts +20 -2
  106. package/src/modes/interactive-mode.ts +94 -37
  107. package/src/modes/rpc/host-tools.ts +186 -0
  108. package/src/modes/rpc/rpc-client.ts +178 -13
  109. package/src/modes/rpc/rpc-mode.ts +73 -3
  110. package/src/modes/rpc/rpc-types.ts +53 -1
  111. package/src/modes/theme/theme.ts +80 -8
  112. package/src/modes/types.ts +2 -2
  113. package/src/prompts/system/system-prompt.md +2 -1
  114. package/src/prompts/tools/chunk-edit.md +219 -0
  115. package/src/prompts/tools/debug.md +43 -0
  116. package/src/prompts/tools/grep.md +3 -0
  117. package/src/prompts/tools/lsp.md +5 -5
  118. package/src/prompts/tools/read-chunk.md +17 -0
  119. package/src/prompts/tools/read.md +19 -5
  120. package/src/sdk.ts +190 -154
  121. package/src/secrets/obfuscator.ts +1 -1
  122. package/src/session/agent-session.ts +306 -256
  123. package/src/session/agent-storage.ts +12 -12
  124. package/src/session/compaction/branch-summarization.ts +3 -3
  125. package/src/session/compaction/compaction.ts +5 -6
  126. package/src/session/compaction/utils.ts +3 -3
  127. package/src/session/history-storage.ts +62 -19
  128. package/src/session/messages.ts +3 -3
  129. package/src/session/session-dump-format.ts +203 -0
  130. package/src/session/session-storage.ts +4 -2
  131. package/src/session/streaming-output.ts +1 -1
  132. package/src/session/tool-choice-queue.ts +213 -0
  133. package/src/slash-commands/builtin-registry.ts +56 -8
  134. package/src/ssh/connection-manager.ts +2 -2
  135. package/src/ssh/sshfs-mount.ts +5 -5
  136. package/src/stt/downloader.ts +4 -4
  137. package/src/stt/recorder.ts +4 -4
  138. package/src/stt/transcriber.ts +2 -2
  139. package/src/system-prompt.ts +21 -13
  140. package/src/task/agents.ts +5 -6
  141. package/src/task/commands.ts +2 -5
  142. package/src/task/executor.ts +4 -4
  143. package/src/task/index.ts +3 -4
  144. package/src/task/template.ts +2 -2
  145. package/src/task/worktree.ts +4 -4
  146. package/src/tools/ask.ts +2 -3
  147. package/src/tools/ast-edit.ts +7 -7
  148. package/src/tools/ast-grep.ts +7 -7
  149. package/src/tools/auto-generated-guard.ts +36 -41
  150. package/src/tools/await-tool.ts +2 -2
  151. package/src/tools/bash.ts +5 -23
  152. package/src/tools/browser.ts +4 -5
  153. package/src/tools/calculator.ts +2 -3
  154. package/src/tools/cancel-job.ts +2 -2
  155. package/src/tools/checkpoint.ts +3 -3
  156. package/src/tools/debug.ts +1007 -0
  157. package/src/tools/exit-plan-mode.ts +2 -3
  158. package/src/tools/fetch.ts +67 -3
  159. package/src/tools/find.ts +4 -5
  160. package/src/tools/fs-cache-invalidation.ts +5 -0
  161. package/src/tools/gemini-image.ts +13 -5
  162. package/src/tools/gh.ts +10 -11
  163. package/src/tools/grep.ts +57 -9
  164. package/src/tools/index.ts +44 -22
  165. package/src/tools/inspect-image.ts +4 -4
  166. package/src/tools/output-meta.ts +1 -1
  167. package/src/tools/python.ts +19 -6
  168. package/src/tools/read.ts +198 -67
  169. package/src/tools/render-mermaid.ts +2 -3
  170. package/src/tools/render-utils.ts +20 -6
  171. package/src/tools/renderers.ts +3 -1
  172. package/src/tools/report-tool-issue.ts +80 -0
  173. package/src/tools/resolve.ts +70 -39
  174. package/src/tools/search-tool-bm25.ts +2 -2
  175. package/src/tools/ssh.ts +2 -2
  176. package/src/tools/todo-write.ts +2 -2
  177. package/src/tools/tool-timeouts.ts +1 -0
  178. package/src/tools/write.ts +5 -6
  179. package/src/tui/tree-list.ts +3 -1
  180. package/src/utils/clipboard.ts +80 -0
  181. package/src/utils/commit-message-generator.ts +2 -3
  182. package/src/utils/edit-mode.ts +49 -0
  183. package/src/utils/file-display-mode.ts +6 -5
  184. package/src/utils/file-mentions.ts +8 -7
  185. package/src/utils/git.ts +4 -4
  186. package/src/utils/image-loading.ts +98 -0
  187. package/src/utils/title-generator.ts +2 -3
  188. package/src/utils/tools-manager.ts +6 -6
  189. package/src/web/scrapers/choosealicense.ts +1 -1
  190. package/src/web/search/index.ts +3 -3
  191. package/src/autoresearch/command-initialize.md +0 -34
  192. package/src/patch/diff.ts +0 -433
  193. package/src/patch/index.ts +0 -888
  194. package/src/patch/parser.ts +0 -532
  195. package/src/patch/types.ts +0 -292
  196. package/src/prompts/agents/oracle.md +0 -77
  197. package/src/tools/pending-action.ts +0 -49
  198. package/src/utils/child-process.ts +0 -88
  199. package/src/utils/frontmatter.ts +0 -117
  200. package/src/utils/image-input.ts +0 -274
  201. package/src/utils/mime.ts +0 -53
  202. 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
+ }