@oh-my-pi/pi-coding-agent 14.2.1 → 14.4.0
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.
- package/CHANGELOG.md +143 -1
- package/package.json +19 -19
- package/src/autoresearch/prompt.md +1 -1
- package/src/cli/args.ts +10 -1
- package/src/cli/shell-cli.ts +15 -3
- package/src/commit/agentic/prompts/analyze-file.md +1 -1
- package/src/config/model-registry.ts +67 -15
- package/src/config/prompt-templates.ts +5 -5
- package/src/config/settings-schema.ts +63 -4
- package/src/cursor.ts +3 -8
- package/src/debug/system-info.ts +6 -2
- package/src/discovery/claude.ts +58 -36
- package/src/discovery/helpers.ts +3 -3
- package/src/discovery/opencode.ts +20 -2
- package/src/edit/diff.ts +50 -47
- package/src/edit/index.ts +87 -57
- package/src/edit/line-hash.ts +735 -19
- package/src/edit/modes/apply-patch.ts +0 -9
- package/src/edit/modes/atom.ts +658 -0
- package/src/edit/modes/chunk.ts +144 -78
- package/src/edit/modes/hashline.ts +223 -146
- package/src/edit/modes/patch.ts +5 -9
- package/src/edit/modes/replace.ts +6 -11
- package/src/edit/renderer.ts +112 -143
- package/src/edit/streaming.ts +385 -0
- package/src/exec/bash-executor.ts +58 -5
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +4 -12
- package/src/extensibility/custom-tools/types.ts +2 -0
- package/src/extensibility/custom-tools/wrapper.ts +2 -1
- package/src/internal-urls/docs-index.generated.ts +7 -7
- package/src/internal-urls/pi-protocol.ts +0 -2
- package/src/lsp/client.ts +8 -1
- package/src/lsp/defaults.json +2 -1
- package/src/lsp/index.ts +1 -1
- package/src/mcp/render.ts +1 -8
- package/src/modes/acp/acp-agent.ts +76 -2
- package/src/modes/components/assistant-message.ts +5 -34
- package/src/modes/components/diff.ts +23 -14
- package/src/modes/components/footer.ts +21 -16
- package/src/modes/components/hook-editor.ts +1 -1
- package/src/modes/components/settings-defs.ts +6 -1
- package/src/modes/components/todo-reminder.ts +1 -8
- package/src/modes/components/tool-execution.ts +112 -105
- package/src/modes/controllers/input-controller.ts +1 -1
- package/src/modes/controllers/selector-controller.ts +1 -1
- package/src/modes/interactive-mode.ts +0 -2
- package/src/modes/print-mode.ts +8 -0
- package/src/modes/theme/mermaid-cache.ts +13 -52
- package/src/modes/theme/theme.ts +2 -2
- package/src/prompts/agents/librarian.md +1 -1
- package/src/prompts/agents/reviewer.md +4 -4
- package/src/prompts/ci-green-request.md +1 -1
- package/src/prompts/review-request.md +1 -1
- package/src/prompts/system/subagent-system-prompt.md +3 -3
- package/src/prompts/system/subagent-yield-reminder.md +11 -0
- package/src/prompts/system/system-prompt.md +4 -1
- package/src/prompts/tools/ask.md +3 -2
- package/src/prompts/tools/ast-edit.md +15 -19
- package/src/prompts/tools/ast-grep.md +18 -24
- package/src/prompts/tools/atom.md +96 -0
- package/src/prompts/tools/browser.md +1 -0
- package/src/prompts/tools/chunk-edit.md +58 -179
- package/src/prompts/tools/debug.md +4 -5
- package/src/prompts/tools/exit-plan-mode.md +4 -5
- package/src/prompts/tools/find.md +4 -8
- package/src/prompts/tools/github.md +18 -0
- package/src/prompts/tools/grep.md +8 -8
- package/src/prompts/tools/hashline.md +22 -89
- package/src/prompts/tools/{gemini-image.md → image-gen.md} +1 -1
- package/src/prompts/tools/inspect-image.md +6 -6
- package/src/prompts/tools/lsp.md +6 -0
- package/src/prompts/tools/patch.md +12 -19
- package/src/prompts/tools/python.md +3 -2
- package/src/prompts/tools/read-chunk.md +46 -8
- package/src/prompts/tools/read.md +9 -6
- package/src/prompts/tools/ssh.md +8 -17
- package/src/prompts/tools/todo-write.md +54 -41
- package/src/sdk.ts +22 -14
- package/src/session/agent-session.ts +61 -22
- package/src/session/session-manager.ts +228 -57
- package/src/session/streaming-output.ts +11 -0
- package/src/system-prompt.ts +7 -2
- package/src/task/executor.ts +44 -48
- package/src/task/render.ts +11 -13
- package/src/tools/ask.ts +7 -7
- package/src/tools/ast-edit.ts +45 -41
- package/src/tools/ast-grep.ts +77 -85
- package/src/tools/bash.ts +21 -9
- package/src/tools/browser.ts +32 -30
- package/src/tools/calculator.ts +4 -4
- package/src/tools/cancel-job.ts +1 -1
- package/src/tools/checkpoint.ts +2 -2
- package/src/tools/debug.ts +41 -37
- package/src/tools/exit-plan-mode.ts +1 -1
- package/src/tools/find.ts +4 -4
- package/src/tools/gh-renderer.ts +12 -4
- package/src/tools/gh.ts +514 -712
- package/src/tools/grep.ts +115 -130
- package/src/tools/{gemini-image.ts → image-gen.ts} +459 -60
- package/src/tools/index.ts +14 -32
- package/src/tools/inspect-image.ts +3 -3
- package/src/tools/json-tree.ts +114 -114
- package/src/tools/match-line-format.ts +9 -8
- package/src/tools/notebook.ts +8 -7
- package/src/tools/poll-tool.ts +2 -1
- package/src/tools/python.ts +9 -23
- package/src/tools/read.ts +32 -21
- package/src/tools/render-mermaid.ts +1 -1
- package/src/tools/render-utils.ts +18 -0
- package/src/tools/renderers.ts +2 -2
- package/src/tools/report-tool-issue.ts +3 -2
- package/src/tools/resolve.ts +1 -1
- package/src/tools/review.ts +12 -10
- package/src/tools/search-tool-bm25.ts +2 -4
- package/src/tools/sqlite-reader.ts +116 -3
- package/src/tools/ssh.ts +4 -4
- package/src/tools/todo-write.ts +172 -147
- package/src/tools/vim.ts +14 -15
- package/src/tools/write.ts +4 -4
- package/src/tools/{submit-result.ts → yield.ts} +11 -13
- package/src/utils/edit-mode.ts +2 -1
- package/src/utils/file-display-mode.ts +10 -5
- package/src/utils/git.ts +9 -5
- package/src/utils/shell-snapshot.ts +2 -3
- package/src/vim/render.ts +4 -4
- package/src/web/search/providers/codex.ts +129 -6
- package/src/prompts/system/subagent-submit-reminder.md +0 -11
- package/src/prompts/tools/gh-issue-view.md +0 -11
- package/src/prompts/tools/gh-pr-checkout.md +0 -12
- package/src/prompts/tools/gh-pr-diff.md +0 -12
- package/src/prompts/tools/gh-pr-push.md +0 -11
- package/src/prompts/tools/gh-pr-view.md +0 -11
- package/src/prompts/tools/gh-repo-view.md +0 -11
- package/src/prompts/tools/gh-run-watch.md +0 -12
- package/src/prompts/tools/gh-search-issues.md +0 -11
- package/src/prompts/tools/gh-search-prs.md +0 -11
package/src/edit/modes/chunk.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as fs from "node:fs/promises";
|
|
2
2
|
import * as nodePath from "node:path";
|
|
3
3
|
import type { AgentToolResult } from "@oh-my-pi/pi-agent-core";
|
|
4
|
-
import { StringEnum } from "@oh-my-pi/pi-
|
|
4
|
+
import { StringEnum } from "@oh-my-pi/pi-ai";
|
|
5
5
|
import {
|
|
6
6
|
ChunkAnchorStyle,
|
|
7
7
|
ChunkEditOp,
|
|
@@ -25,6 +25,7 @@ import { invalidateFsScanAfterWrite } from "../../tools/fs-cache-invalidation";
|
|
|
25
25
|
import { outputMeta } from "../../tools/output-meta";
|
|
26
26
|
import { enforcePlanModeWrite, resolvePlanPath } from "../../tools/plan-mode-guard";
|
|
27
27
|
import { generateUnifiedDiffString } from "../diff";
|
|
28
|
+
import { HASHLINE_BIGRAMS } from "../line-hash";
|
|
28
29
|
import { detectLineEnding, normalizeToLF, restoreLineEndings, stripBom } from "../normalize";
|
|
29
30
|
import type { EditToolDetails, LspBatchRequest } from "../renderer";
|
|
30
31
|
|
|
@@ -32,7 +33,6 @@ export type { ChunkReadTarget };
|
|
|
32
33
|
|
|
33
34
|
export type ChunkEditOperation =
|
|
34
35
|
| { op: "put"; sel?: string; content: string }
|
|
35
|
-
| { op: "replace"; sel?: string; content: string; find: string }
|
|
36
36
|
| { op: "delete"; sel?: string }
|
|
37
37
|
| { op: "before"; sel?: string; content: string }
|
|
38
38
|
| { op: "after"; sel?: string; content: string }
|
|
@@ -120,6 +120,8 @@ type ChunkSourceContext = {
|
|
|
120
120
|
chunkLanguage: string | undefined;
|
|
121
121
|
};
|
|
122
122
|
|
|
123
|
+
type ChunkSourceIntent = "read" | "write";
|
|
124
|
+
|
|
123
125
|
function normalizeLanguage(language: string | undefined): string {
|
|
124
126
|
return language?.trim().toLowerCase() || "";
|
|
125
127
|
}
|
|
@@ -140,11 +142,17 @@ function fileLanguageTag(filePath: string, language?: string): string | undefine
|
|
|
140
142
|
return ext.length > 0 ? ext : undefined;
|
|
141
143
|
}
|
|
142
144
|
|
|
143
|
-
async function resolveChunkSourceContext(
|
|
145
|
+
async function resolveChunkSourceContext(
|
|
146
|
+
session: ToolSession,
|
|
147
|
+
path: string,
|
|
148
|
+
options?: { intent?: ChunkSourceIntent },
|
|
149
|
+
): Promise<ChunkSourceContext> {
|
|
144
150
|
const resolvedPath = resolvePlanPath(session, path);
|
|
145
151
|
const sourceFile = Bun.file(resolvedPath);
|
|
146
152
|
const sourceExists = await sourceFile.exists();
|
|
147
|
-
|
|
153
|
+
if ((options?.intent ?? "write") === "write") {
|
|
154
|
+
enforcePlanModeWrite(session, path, { op: sourceExists ? "update" : "create" });
|
|
155
|
+
}
|
|
148
156
|
|
|
149
157
|
let rawContent = "";
|
|
150
158
|
if (sourceExists) {
|
|
@@ -161,6 +169,57 @@ async function resolveChunkSourceContext(session: ToolSession, path: string): Pr
|
|
|
161
169
|
};
|
|
162
170
|
}
|
|
163
171
|
|
|
172
|
+
/**
|
|
173
|
+
* Preview-safe loader: read raw source without plan-mode enforcement or
|
|
174
|
+
* editable-file guards. Used by streaming diff previews that must not throw
|
|
175
|
+
* side-effecting errors while args are still being streamed.
|
|
176
|
+
*/
|
|
177
|
+
export async function loadChunkSource(params: {
|
|
178
|
+
cwd: string;
|
|
179
|
+
path: string;
|
|
180
|
+
}): Promise<{ resolvedPath: string; rawContent: string; language: string | undefined; exists: boolean }> {
|
|
181
|
+
const resolvedPath = nodePath.isAbsolute(params.path) ? params.path : nodePath.resolve(params.cwd, params.path);
|
|
182
|
+
const sourceFile = Bun.file(resolvedPath);
|
|
183
|
+
const exists = await sourceFile.exists();
|
|
184
|
+
const rawContent = exists ? await sourceFile.text() : "";
|
|
185
|
+
return { resolvedPath, rawContent, language: getLanguageFromPath(resolvedPath), exists };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Compute a unified diff preview for a chunk edit without applying it.
|
|
190
|
+
* Used for streaming previews while args are still arriving. Returns
|
|
191
|
+
* `{ error }` on any failure so callers can decide whether to surface it.
|
|
192
|
+
*/
|
|
193
|
+
export async function computeChunkDiff(
|
|
194
|
+
input: { path: string; edits: ChunkToolEdit[] },
|
|
195
|
+
cwd: string,
|
|
196
|
+
options?: { anchorStyle?: ChunkAnchorStyle; signal?: AbortSignal },
|
|
197
|
+
): Promise<{ diff: string; firstChangedLine: number | undefined } | { error: string }> {
|
|
198
|
+
try {
|
|
199
|
+
options?.signal?.throwIfAborted?.();
|
|
200
|
+
const { filePath } = parseChunkEditPath(input.path);
|
|
201
|
+
if (!filePath) return { error: "chunk edit path is empty" };
|
|
202
|
+
const { resolvedPath, rawContent, language } = await loadChunkSource({ cwd, path: filePath });
|
|
203
|
+
options?.signal?.throwIfAborted?.();
|
|
204
|
+
const { operations } = normalizeChunkEditOperations(input.edits);
|
|
205
|
+
const result = applyChunkEdits({
|
|
206
|
+
source: rawContent,
|
|
207
|
+
language,
|
|
208
|
+
cwd,
|
|
209
|
+
filePath: resolvedPath,
|
|
210
|
+
operations,
|
|
211
|
+
anchorStyle: options?.anchorStyle,
|
|
212
|
+
});
|
|
213
|
+
options?.signal?.throwIfAborted?.();
|
|
214
|
+
if (!result.changed) {
|
|
215
|
+
return { diff: "", firstChangedLine: undefined };
|
|
216
|
+
}
|
|
217
|
+
return generateUnifiedDiffString(result.diffSourceBefore, result.diffSourceAfter);
|
|
218
|
+
} catch (err) {
|
|
219
|
+
return { error: err instanceof Error ? err.message : String(err) };
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
164
223
|
function normalizeChunkRegionSyntax(text: string): string {
|
|
165
224
|
return text.replaceAll("@body", "~").replaceAll("@head", "^");
|
|
166
225
|
}
|
|
@@ -189,6 +248,20 @@ function chunkReadPathSeparatorIndex(readPath: string): number {
|
|
|
189
248
|
if (/^[a-zA-Z]:[/\\]/.test(readPath)) {
|
|
190
249
|
return readPath.indexOf(":", 2);
|
|
191
250
|
}
|
|
251
|
+
const urlMatch = readPath.match(/^([a-z][a-z0-9+.-]*):\/\//i);
|
|
252
|
+
if (urlMatch) {
|
|
253
|
+
const scheme = urlMatch[1].toLowerCase();
|
|
254
|
+
const urlPrefixEnd = urlMatch[0].length;
|
|
255
|
+
if (scheme === "local") {
|
|
256
|
+
const index = readPath.lastIndexOf(":");
|
|
257
|
+
return index >= urlPrefixEnd ? index : -1;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const pathStart = readPath.indexOf("/", urlPrefixEnd);
|
|
261
|
+
if (pathStart === -1) return -1;
|
|
262
|
+
const index = readPath.lastIndexOf(":");
|
|
263
|
+
return index >= pathStart ? index : -1;
|
|
264
|
+
}
|
|
192
265
|
return readPath.indexOf(":");
|
|
193
266
|
}
|
|
194
267
|
|
|
@@ -300,11 +373,13 @@ export async function describeChunkedGrepMatch(params: {
|
|
|
300
373
|
};
|
|
301
374
|
}
|
|
302
375
|
|
|
303
|
-
const
|
|
376
|
+
const CHUNK_CHECKSUM_BIGRAMS = new Set<string>(HASHLINE_BIGRAMS);
|
|
304
377
|
type NativeChunkRegion = "head" | "body";
|
|
305
378
|
|
|
306
379
|
function isChunkChecksumToken(value: string): boolean {
|
|
307
|
-
|
|
380
|
+
if (value.length !== 4) return false;
|
|
381
|
+
const lower = value.toLowerCase();
|
|
382
|
+
return CHUNK_CHECKSUM_BIGRAMS.has(lower.slice(0, 2)) && CHUNK_CHECKSUM_BIGRAMS.has(lower.slice(2, 4));
|
|
308
383
|
}
|
|
309
384
|
|
|
310
385
|
function parseChunkEditSelector(selector: string | undefined): {
|
|
@@ -334,11 +409,11 @@ function parseChunkEditSelector(selector: string | undefined): {
|
|
|
334
409
|
if (hashIndex >= 0) {
|
|
335
410
|
const suffix = selectorPart.slice(hashIndex + 1).trim();
|
|
336
411
|
if (isChunkChecksumToken(suffix)) {
|
|
337
|
-
crc = suffix.
|
|
412
|
+
crc = suffix.toLowerCase();
|
|
338
413
|
selectorPart = selectorPart.slice(0, hashIndex).trimEnd();
|
|
339
414
|
}
|
|
340
415
|
} else if (isChunkChecksumToken(selectorPart)) {
|
|
341
|
-
crc = selectorPart.
|
|
416
|
+
crc = selectorPart.toLowerCase();
|
|
342
417
|
selectorPart = "";
|
|
343
418
|
}
|
|
344
419
|
|
|
@@ -376,15 +451,6 @@ function toNativeEditOperation(
|
|
|
376
451
|
region: nativeRegion,
|
|
377
452
|
content: operation.content,
|
|
378
453
|
};
|
|
379
|
-
case "replace":
|
|
380
|
-
return {
|
|
381
|
-
op: ChunkEditOp.Replace,
|
|
382
|
-
sel: selector,
|
|
383
|
-
crc,
|
|
384
|
-
region: nativeRegion,
|
|
385
|
-
find: operation.find,
|
|
386
|
-
content: operation.content,
|
|
387
|
-
};
|
|
388
454
|
case "before":
|
|
389
455
|
return { op: ChunkEditOp.Before, sel: selector, crc, region: nativeRegion, content: operation.content };
|
|
390
456
|
case "after":
|
|
@@ -486,22 +552,21 @@ export function missingChunkReadTarget(selector: string): ChunkReadTarget {
|
|
|
486
552
|
|
|
487
553
|
export const chunkToolEditSchema = Type.Object(
|
|
488
554
|
{
|
|
489
|
-
path: Type.
|
|
490
|
-
|
|
491
|
-
|
|
555
|
+
path: Type.Optional(
|
|
556
|
+
Type.String({
|
|
557
|
+
description: "File path with chunk selector. Examples: 'src/app.ts:fn_foo#thth~', 'src/app.ts:class_Bar'.",
|
|
558
|
+
}),
|
|
559
|
+
),
|
|
492
560
|
write: Type.Optional(
|
|
493
561
|
Type.Union([Type.String(), Type.Null()], {
|
|
494
|
-
description:
|
|
562
|
+
description:
|
|
563
|
+
"Write complete new content to the targeted region. Null is rejected; use delete: true for deletion.",
|
|
495
564
|
}),
|
|
496
565
|
),
|
|
497
|
-
|
|
498
|
-
Type.
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
new: Type.String({ description: "Replacement text." }),
|
|
502
|
-
},
|
|
503
|
-
{ description: "Find and replace a substring within the chunk." },
|
|
504
|
-
),
|
|
566
|
+
delete: Type.Optional(
|
|
567
|
+
Type.Boolean({
|
|
568
|
+
description: "Explicitly delete the targeted chunk. Must be true; include the current chunk ID.",
|
|
569
|
+
}),
|
|
505
570
|
),
|
|
506
571
|
insert: Type.Optional(
|
|
507
572
|
Type.Object(
|
|
@@ -517,6 +582,7 @@ export const chunkToolEditSchema = Type.Object(
|
|
|
517
582
|
);
|
|
518
583
|
export const chunkEditParamsSchema = Type.Object(
|
|
519
584
|
{
|
|
585
|
+
path: Type.Optional(Type.String({ description: "Default file path used when an edit omits its own `path`" })),
|
|
520
586
|
edits: Type.Array(chunkToolEditSchema, {
|
|
521
587
|
description: "Chunk edits",
|
|
522
588
|
minItems: 1,
|
|
@@ -538,25 +604,6 @@ export interface ExecuteChunkSingleOptions {
|
|
|
538
604
|
beginDeferredDiagnosticsForPath: (path: string) => WritethroughDeferredHandle;
|
|
539
605
|
}
|
|
540
606
|
|
|
541
|
-
export function isChunkParams(params: unknown): params is ChunkParams {
|
|
542
|
-
if (
|
|
543
|
-
typeof params !== "object" ||
|
|
544
|
-
params === null ||
|
|
545
|
-
!("edits" in params) ||
|
|
546
|
-
!Array.isArray(params.edits) ||
|
|
547
|
-
params.edits.length === 0
|
|
548
|
-
) {
|
|
549
|
-
return false;
|
|
550
|
-
}
|
|
551
|
-
const first = params.edits[0];
|
|
552
|
-
// Accept a bare `{ path }` entry: it is interpreted downstream as a chunk
|
|
553
|
-
// delete. Some providers strip `null` values from tool-call JSON, so a
|
|
554
|
-
// documented `{ path, write: null }` delete can arrive here as just
|
|
555
|
-
// `{ path }`. Rejecting that surfaced as a misleading
|
|
556
|
-
// "Invalid edit parameters for chunk mode." error.
|
|
557
|
-
return typeof first === "object" && first !== null && "path" in first;
|
|
558
|
-
}
|
|
559
|
-
|
|
560
607
|
/** Auto-correct indentation for content targeting a body region (`~`) when autoIndent is on.
|
|
561
608
|
* Handles two patterns:
|
|
562
609
|
* 1. Tab-based over-indentation: models include the function's base \t indent.
|
|
@@ -605,6 +652,29 @@ function autoCorrectBodyIndent(content: string, index: number): { content: strin
|
|
|
605
652
|
return { content, warnings };
|
|
606
653
|
}
|
|
607
654
|
|
|
655
|
+
function chunkEditOperationFields(edit: ChunkToolEdit): string[] {
|
|
656
|
+
const fields: string[] = [];
|
|
657
|
+
if (edit.write !== undefined) fields.push("write");
|
|
658
|
+
if (edit.insert != null) fields.push("insert");
|
|
659
|
+
if (edit.delete === true) fields.push("delete");
|
|
660
|
+
return fields;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
function assertSingleChunkOperation(edit: ChunkToolEdit, index: number): string {
|
|
664
|
+
const fields = chunkEditOperationFields(edit);
|
|
665
|
+
if (fields.length === 0) {
|
|
666
|
+
throw new Error(
|
|
667
|
+
`Edit ${index + 1}: no operation specified. Use write:"..." to replace, insert:{loc,body} to insert, or delete:true to delete. Use the open tool to inspect chunks.`,
|
|
668
|
+
);
|
|
669
|
+
}
|
|
670
|
+
if (fields.length > 1) {
|
|
671
|
+
throw new Error(
|
|
672
|
+
`Edit ${index + 1}: multiple operation fields set (${fields.join(", ")}). Each chunk edit entry must have exactly one operation.`,
|
|
673
|
+
);
|
|
674
|
+
}
|
|
675
|
+
return fields[0];
|
|
676
|
+
}
|
|
677
|
+
|
|
608
678
|
function normalizeChunkEditOperations(edits: ChunkToolEdit[]): {
|
|
609
679
|
operations: ChunkEditOperation[];
|
|
610
680
|
warnings: string[];
|
|
@@ -612,26 +682,22 @@ function normalizeChunkEditOperations(edits: ChunkToolEdit[]): {
|
|
|
612
682
|
const warnings: string[] = [];
|
|
613
683
|
const operations = edits.map((edit, index): ChunkEditOperation => {
|
|
614
684
|
const { selector } = parseChunkEditPath(edit.path);
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
);
|
|
632
|
-
}
|
|
633
|
-
if (hasWrite) {
|
|
634
|
-
let writeContent = edit.write!;
|
|
685
|
+
const operation = assertSingleChunkOperation(edit, index);
|
|
686
|
+
if (operation === "write") {
|
|
687
|
+
if (edit.write === null) {
|
|
688
|
+
throw new Error(
|
|
689
|
+
`Edit ${index + 1}: write:null no longer deletes chunks. Use delete:true to delete, or open the chunk to inspect its content without modifying the file.`,
|
|
690
|
+
);
|
|
691
|
+
}
|
|
692
|
+
if (typeof edit.write !== "string") {
|
|
693
|
+
throw new Error(`Edit ${index + 1}: write must be a string.`);
|
|
694
|
+
}
|
|
695
|
+
if (edit.write.length === 0) {
|
|
696
|
+
throw new Error(
|
|
697
|
+
`Edit ${index + 1}: write:"" is a destructive empty replacement. Use delete:true to delete the chunk, or open the chunk to inspect its content without modifying the file.`,
|
|
698
|
+
);
|
|
699
|
+
}
|
|
700
|
+
let writeContent = edit.write;
|
|
635
701
|
if (selector?.endsWith("~")) {
|
|
636
702
|
const corrected = autoCorrectBodyIndent(writeContent, index);
|
|
637
703
|
writeContent = corrected.content;
|
|
@@ -639,15 +705,12 @@ function normalizeChunkEditOperations(edits: ChunkToolEdit[]): {
|
|
|
639
705
|
}
|
|
640
706
|
return { op: "put", sel: selector, content: writeContent };
|
|
641
707
|
}
|
|
642
|
-
if (
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
if (hasInsert) {
|
|
649
|
-
const op = edit.insert!.loc === "prepend" ? "before" : "after";
|
|
650
|
-
let insertContent = edit.insert!.body;
|
|
708
|
+
if (operation === "insert") {
|
|
709
|
+
if (edit.insert == null || typeof edit.insert.body !== "string" || edit.insert.body.length === 0) {
|
|
710
|
+
throw new Error(`Edit ${index + 1}: insert.body must be a non-empty string.`);
|
|
711
|
+
}
|
|
712
|
+
const op = edit.insert.loc === "prepend" ? "before" : "after";
|
|
713
|
+
let insertContent = edit.insert.body;
|
|
651
714
|
if (selector?.endsWith("~")) {
|
|
652
715
|
const corrected = autoCorrectBodyIndent(insertContent, index);
|
|
653
716
|
insertContent = corrected.content;
|
|
@@ -655,7 +718,9 @@ function normalizeChunkEditOperations(edits: ChunkToolEdit[]): {
|
|
|
655
718
|
}
|
|
656
719
|
return { op, sel: selector, content: insertContent };
|
|
657
720
|
}
|
|
658
|
-
|
|
721
|
+
if (operation !== "delete") {
|
|
722
|
+
throw new Error(`Edit ${index + 1}: unsupported chunk edit operation "${operation}".`);
|
|
723
|
+
}
|
|
659
724
|
return { op: "delete", sel: selector };
|
|
660
725
|
});
|
|
661
726
|
return { operations, warnings };
|
|
@@ -717,6 +782,7 @@ export async function executeChunkSingle(
|
|
|
717
782
|
const { resolvedPath, sourceFile, sourceExists, rawContent, chunkLanguage } = await resolveChunkSourceContext(
|
|
718
783
|
session,
|
|
719
784
|
path,
|
|
785
|
+
{ intent: "write" },
|
|
720
786
|
);
|
|
721
787
|
const parentDir = nodePath.dirname(resolvedPath);
|
|
722
788
|
if (parentDir && parentDir !== ".") {
|