@oh-my-pi/pi-coding-agent 14.3.0 → 14.4.1
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 +98 -1
- package/package.json +7 -7
- package/src/autoresearch/prompt.md +1 -1
- 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 +4 -4
- package/src/cursor.ts +3 -8
- package/src/discovery/helpers.ts +3 -3
- package/src/edit/diff.ts +50 -47
- package/src/edit/index.ts +86 -57
- package/src/edit/line-hash.ts +743 -24
- package/src/edit/modes/apply-patch.ts +0 -9
- package/src/edit/modes/atom.ts +893 -0
- package/src/edit/modes/chunk.ts +14 -24
- package/src/edit/modes/hashline.ts +193 -146
- package/src/edit/modes/patch.ts +5 -9
- package/src/edit/modes/replace.ts +6 -11
- package/src/edit/renderer.ts +14 -10
- package/src/edit/streaming.ts +50 -16
- package/src/exec/bash-executor.ts +2 -4
- 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 +2 -2
- package/src/lsp/defaults.json +142 -652
- package/src/lsp/index.ts +1 -1
- package/src/mcp/render.ts +1 -8
- package/src/modes/components/assistant-message.ts +4 -0
- package/src/modes/components/diff.ts +23 -14
- package/src/modes/components/footer.ts +21 -16
- package/src/modes/components/session-selector.ts +3 -3
- 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 +1 -4
- package/src/modes/controllers/selector-controller.ts +1 -1
- package/src/modes/print-mode.ts +8 -0
- 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 +3 -0
- package/src/prompts/tools/ask.md +3 -2
- package/src/prompts/tools/ast-edit.md +16 -20
- package/src/prompts/tools/ast-grep.md +19 -24
- package/src/prompts/tools/atom.md +87 -0
- package/src/prompts/tools/chunk-edit.md +37 -161
- 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 +4 -5
- 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 +1 -1
- package/src/prompts/tools/patch.md +12 -19
- package/src/prompts/tools/python.md +3 -2
- package/src/prompts/tools/read-chunk.md +2 -3
- package/src/prompts/tools/read.md +2 -2
- package/src/prompts/tools/ssh.md +8 -17
- package/src/prompts/tools/todo-write.md +54 -41
- package/src/sdk.ts +14 -9
- package/src/session/agent-session.ts +25 -2
- package/src/session/session-manager.ts +4 -1
- package/src/task/executor.ts +43 -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 +8 -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 +509 -697
- package/src/tools/grep.ts +116 -131
- 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 +8 -7
- 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 -25
- 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/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/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 -12
- 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
|
@@ -2,18 +2,19 @@
|
|
|
2
2
|
* Hashline edit mode — a line-addressable edit format using text hashes.
|
|
3
3
|
*
|
|
4
4
|
* Each line in a file is identified by its 1-indexed line number and a short
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* The combined `LINE
|
|
5
|
+
* BPE-bigram hash derived from the normalized line text (xxHash32 mod 647,
|
|
6
|
+
* mapped through HASHLINE_BIGRAMS).
|
|
7
|
+
* The combined `LINE+ID` reference acts as both an address and a staleness check:
|
|
8
8
|
* if the file has changed since the caller last read it, hash mismatches are caught
|
|
9
9
|
* before any mutation occurs.
|
|
10
10
|
*
|
|
11
|
-
* Displayed format: `
|
|
12
|
-
* Reference format: `"
|
|
11
|
+
* Displayed format: `LINE+ID|TEXT`
|
|
12
|
+
* Reference format: `"LINE+ID"` (e.g. `"1ab"`)
|
|
13
|
+
*
|
|
14
|
+
* In tool JSON, each edit's `content` is `string[]` (one string per logical line) or
|
|
15
|
+
* `null` to delete the targeted range.
|
|
13
16
|
*/
|
|
14
17
|
|
|
15
|
-
import * as fs from "node:fs/promises";
|
|
16
|
-
import * as nodePath from "node:path";
|
|
17
18
|
import type { AgentToolResult } from "@oh-my-pi/pi-agent-core";
|
|
18
19
|
import { isEnoent } from "@oh-my-pi/pi-utils";
|
|
19
20
|
import { type Static, Type } from "@sinclair/typebox";
|
|
@@ -21,16 +22,13 @@ import type { BunFile } from "bun";
|
|
|
21
22
|
import type { WritethroughCallback, WritethroughDeferredHandle } from "../../lsp";
|
|
22
23
|
import type { ToolSession } from "../../tools";
|
|
23
24
|
import { assertEditableFileContent } from "../../tools/auto-generated-guard";
|
|
24
|
-
import {
|
|
25
|
-
invalidateFsScanAfterDelete,
|
|
26
|
-
invalidateFsScanAfterRename,
|
|
27
|
-
invalidateFsScanAfterWrite,
|
|
28
|
-
} from "../../tools/fs-cache-invalidation";
|
|
25
|
+
import { invalidateFsScanAfterWrite } from "../../tools/fs-cache-invalidation";
|
|
29
26
|
import { outputMeta } from "../../tools/output-meta";
|
|
30
27
|
import { resolveToCwd } from "../../tools/path-utils";
|
|
31
28
|
import { enforcePlanModeWrite, resolvePlanPath } from "../../tools/plan-mode-guard";
|
|
29
|
+
import { formatCodeFrameLine } from "../../tools/render-utils";
|
|
32
30
|
import { generateDiffString } from "../diff";
|
|
33
|
-
import { computeLineHash,
|
|
31
|
+
import { computeLineHash, formatHashLine, HASHLINE_BIGRAM_RE_SRC, HASHLINE_CONTENT_SEPARATOR } from "../line-hash";
|
|
34
32
|
import { detectLineEnding, normalizeToLF, restoreLineEndings, stripBom } from "../normalize";
|
|
35
33
|
import type { EditToolDetails, LspBatchRequest } from "../renderer";
|
|
36
34
|
|
|
@@ -40,7 +38,7 @@ export interface HashMismatch {
|
|
|
40
38
|
actual: string;
|
|
41
39
|
}
|
|
42
40
|
|
|
43
|
-
export type Anchor = { line: number; hash: string };
|
|
41
|
+
export type Anchor = { line: number; hash: string; contentHint?: string };
|
|
44
42
|
export type HashlineEdit =
|
|
45
43
|
| { op: "replace_line"; pos: Anchor; lines: string[] }
|
|
46
44
|
| { op: "replace_range"; pos: Anchor; end: Anchor; lines: string[] }
|
|
@@ -49,8 +47,17 @@ export type HashlineEdit =
|
|
|
49
47
|
| { op: "append_file"; lines: string[] }
|
|
50
48
|
| { op: "prepend_file"; lines: string[] };
|
|
51
49
|
|
|
52
|
-
|
|
53
|
-
|
|
50
|
+
// Tight prefix matchers for the new format `LINE+ID|content`. The pipe is the
|
|
51
|
+
// canonical separator; legacy reads using `:` are tolerated for back-compat.
|
|
52
|
+
// Line-number digits are mandatory.
|
|
53
|
+
// Accept both `|` (canonical) and `:` (legacy) so re-reads of older outputs still parse.
|
|
54
|
+
const HASHLINE_CONTENT_SEPARATOR_RE = "[:|]";
|
|
55
|
+
const HASHLINE_PREFIX_RE = new RegExp(
|
|
56
|
+
`^\\s*(?:>>>|>>)?\\s*(?:\\+\\s*)?\\d+${HASHLINE_BIGRAM_RE_SRC}${HASHLINE_CONTENT_SEPARATOR_RE}`,
|
|
57
|
+
);
|
|
58
|
+
const HASHLINE_PREFIX_PLUS_RE = new RegExp(
|
|
59
|
+
`^\\s*(?:>>>|>>)?\\s*\\+\\s*\\d+${HASHLINE_BIGRAM_RE_SRC}${HASHLINE_CONTENT_SEPARATOR_RE}`,
|
|
60
|
+
);
|
|
54
61
|
const DIFF_PLUS_RE = /^[+](?![+])/;
|
|
55
62
|
const READ_TRUNCATION_NOTICE_RE = /^\[(?:Showing lines \d+-\d+ of \d+|\d+ more lines? in (?:file|\S+))\b.*\bsel=L\d+/;
|
|
56
63
|
|
|
@@ -129,11 +136,7 @@ export function stripHashlinePrefixes(lines: string[]): string[] {
|
|
|
129
136
|
return lines.filter(line => !READ_TRUNCATION_NOTICE_RE.test(line)).map(line => stripLeadingHashlinePrefixes(line));
|
|
130
137
|
}
|
|
131
138
|
|
|
132
|
-
const linesSchema = Type.Union([
|
|
133
|
-
Type.Array(Type.String(), { description: "content (preferred format)" }),
|
|
134
|
-
Type.String(),
|
|
135
|
-
Type.Null(),
|
|
136
|
-
]);
|
|
139
|
+
const linesSchema = Type.Union([Type.Array(Type.String()), Type.Null()]);
|
|
137
140
|
|
|
138
141
|
const locSchema = Type.Union(
|
|
139
142
|
[
|
|
@@ -153,17 +156,16 @@ const locSchema = Type.Union(
|
|
|
153
156
|
|
|
154
157
|
export const hashlineEditSchema = Type.Object(
|
|
155
158
|
{
|
|
156
|
-
path: Type.String({ description: "File path" }),
|
|
159
|
+
path: Type.Optional(Type.String({ description: "File path (omit to use top-level `path`)" })),
|
|
157
160
|
loc: Type.Optional(locSchema),
|
|
158
161
|
content: Type.Optional(linesSchema),
|
|
159
|
-
delete: Type.Optional(Type.Boolean({ description: "Delete the file" })),
|
|
160
|
-
move: Type.Optional(Type.String({ description: "Move/rename the file to this path" })),
|
|
161
162
|
},
|
|
162
163
|
{ additionalProperties: false },
|
|
163
164
|
);
|
|
164
165
|
|
|
165
166
|
export const hashlineEditParamsSchema = Type.Object(
|
|
166
167
|
{
|
|
168
|
+
path: Type.Optional(Type.String({ description: "Default file path used when an edit omits its own `path`" })),
|
|
167
169
|
edits: Type.Array(hashlineEditSchema, { description: "edits" }),
|
|
168
170
|
},
|
|
169
171
|
{ additionalProperties: false },
|
|
@@ -182,6 +184,11 @@ export interface ExecuteHashlineSingleOptions {
|
|
|
182
184
|
beginDeferredDiagnosticsForPath: (path: string) => WritethroughDeferredHandle;
|
|
183
185
|
}
|
|
184
186
|
|
|
187
|
+
/**
|
|
188
|
+
* Normalize line payloads for apply: strip read/grep line prefixes. The tool schema
|
|
189
|
+
* supplies `string[]` (one element per line). `null` / `undefined` yield `[]`.
|
|
190
|
+
* A single multiline `string` is still split on `\n` for the same normalization path.
|
|
191
|
+
*/
|
|
185
192
|
export function hashlineParseText(edit: string[] | string | null | undefined): string[] {
|
|
186
193
|
if (edit == null) return [];
|
|
187
194
|
if (typeof edit === "string") {
|
|
@@ -191,15 +198,6 @@ export function hashlineParseText(edit: string[] | string | null | undefined): s
|
|
|
191
198
|
return stripNewLinePrefixes(edit);
|
|
192
199
|
}
|
|
193
200
|
|
|
194
|
-
export function isHashlineParams(params: unknown): params is HashlineParams {
|
|
195
|
-
if (typeof params !== "object" || params === null || !("edits" in params) || !Array.isArray(params.edits))
|
|
196
|
-
return false;
|
|
197
|
-
if (params.edits.length === 0) return true;
|
|
198
|
-
const first = params.edits[0];
|
|
199
|
-
if (typeof first !== "object" || first === null) return false;
|
|
200
|
-
return "loc" in first || "delete" in first || "move" in first;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
201
|
function resolveEditAnchors(edits: HashlineToolEdit[]): HashlineEdit[] {
|
|
204
202
|
return edits.map(resolveEditAnchor);
|
|
205
203
|
}
|
|
@@ -224,6 +222,15 @@ function resolveHashlineEditsForDiff(edits: HashlineEditInput[]): HashlineEdit[]
|
|
|
224
222
|
});
|
|
225
223
|
}
|
|
226
224
|
|
|
225
|
+
export function formatFullAnchorRequirement(raw?: string): string {
|
|
226
|
+
const suffix = typeof raw === "string" ? raw.trim() : "";
|
|
227
|
+
const hashOnlyHint = /^[A-Za-z]{2}$/.test(suffix)
|
|
228
|
+
? ` It looks like you supplied only the 2-letter suffix (${JSON.stringify(suffix)}). Copy the full anchor exactly as shown (for example, "160${suffix}").`
|
|
229
|
+
: "";
|
|
230
|
+
const received = raw === undefined ? "" : ` Received ${JSON.stringify(raw)}.`;
|
|
231
|
+
return `the full anchor exactly as shown by read/grep (line number + 2-letter suffix, for example "160sr")${received}${hashOnlyHint}`;
|
|
232
|
+
}
|
|
233
|
+
|
|
227
234
|
function tryParseTag(raw: string): Anchor | undefined {
|
|
228
235
|
try {
|
|
229
236
|
return parseTag(raw);
|
|
@@ -234,14 +241,24 @@ function tryParseTag(raw: string): Anchor | undefined {
|
|
|
234
241
|
|
|
235
242
|
function requireParsedAnchor(raw: string, op: "append" | "prepend"): Anchor {
|
|
236
243
|
const anchor = tryParseTag(raw);
|
|
237
|
-
if (!anchor) throw new Error(`${op} requires
|
|
244
|
+
if (!anchor) throw new Error(`${op} requires ${formatFullAnchorRequirement(raw)}.`);
|
|
238
245
|
return anchor;
|
|
239
246
|
}
|
|
240
247
|
|
|
241
248
|
function requireParsedRange(range: { pos: string; end: string }): { pos: Anchor; end: Anchor } {
|
|
242
249
|
const pos = tryParseTag(range.pos);
|
|
243
250
|
const end = tryParseTag(range.end);
|
|
244
|
-
if (!pos || !end)
|
|
251
|
+
if (!pos || !end) {
|
|
252
|
+
const invalid = [
|
|
253
|
+
!pos ? `pos=${JSON.stringify(range.pos)}` : null,
|
|
254
|
+
!end ? `end=${JSON.stringify(range.end)}` : null,
|
|
255
|
+
]
|
|
256
|
+
.filter(Boolean)
|
|
257
|
+
.join(", ");
|
|
258
|
+
throw new Error(
|
|
259
|
+
`range requires valid pos and end anchors. Use ${formatFullAnchorRequirement()}. Invalid: ${invalid}.`,
|
|
260
|
+
);
|
|
261
|
+
}
|
|
245
262
|
return { pos, end };
|
|
246
263
|
}
|
|
247
264
|
|
|
@@ -296,8 +313,6 @@ interface ResolvedHashlineStreamOptions {
|
|
|
296
313
|
maxChunkBytes: number;
|
|
297
314
|
}
|
|
298
315
|
|
|
299
|
-
type HashlineLineFormatter = (lineNumber: number, line: string) => string;
|
|
300
|
-
|
|
301
316
|
interface HashlineChunkEmitter {
|
|
302
317
|
pushLine: (line: string) => string[];
|
|
303
318
|
flush: () => string | undefined;
|
|
@@ -313,7 +328,7 @@ function resolveHashlineStreamOptions(options: HashlineStreamOptions): ResolvedH
|
|
|
313
328
|
|
|
314
329
|
function createHashlineChunkEmitter(
|
|
315
330
|
options: ResolvedHashlineStreamOptions,
|
|
316
|
-
formatLine
|
|
331
|
+
formatLine = formatHashLine,
|
|
317
332
|
): HashlineChunkEmitter {
|
|
318
333
|
let lineNumber = options.startLine;
|
|
319
334
|
let outLines: string[] = [];
|
|
@@ -357,10 +372,6 @@ function createHashlineChunkEmitter(
|
|
|
357
372
|
return { pushLine, flush };
|
|
358
373
|
}
|
|
359
374
|
|
|
360
|
-
function formatHashlineStreamLine(lineNumber: number, line: string): string {
|
|
361
|
-
return `${formatLineHash(lineNumber, line)}:${line}`;
|
|
362
|
-
}
|
|
363
|
-
|
|
364
375
|
function isReadableStream(value: unknown): value is ReadableStream<Uint8Array> {
|
|
365
376
|
return (
|
|
366
377
|
typeof value === "object" &&
|
|
@@ -400,7 +411,7 @@ export async function* streamHashLinesFromUtf8(
|
|
|
400
411
|
let pending = "";
|
|
401
412
|
let sawAnyText = false;
|
|
402
413
|
let endedWithNewline = false;
|
|
403
|
-
const emitter = createHashlineChunkEmitter(resolvedOptions
|
|
414
|
+
const emitter = createHashlineChunkEmitter(resolvedOptions);
|
|
404
415
|
|
|
405
416
|
const consumeText = (text: string): string[] => {
|
|
406
417
|
if (text.length === 0) return [];
|
|
@@ -453,7 +464,7 @@ export async function* streamHashLinesFromLines(
|
|
|
453
464
|
options: HashlineStreamOptions = {},
|
|
454
465
|
): AsyncGenerator<string> {
|
|
455
466
|
const resolvedOptions = resolveHashlineStreamOptions(options);
|
|
456
|
-
const emitter = createHashlineChunkEmitter(resolvedOptions
|
|
467
|
+
const emitter = createHashlineChunkEmitter(resolvedOptions);
|
|
457
468
|
let sawAnyLine = false;
|
|
458
469
|
|
|
459
470
|
const asyncIterator = (lines as AsyncIterable<string>)[Symbol.asyncIterator];
|
|
@@ -484,20 +495,18 @@ export async function* streamHashLinesFromLines(
|
|
|
484
495
|
}
|
|
485
496
|
|
|
486
497
|
/**
|
|
487
|
-
* Parse a line reference string like `"
|
|
498
|
+
* Parse a line reference string like `"5th"` into structured form.
|
|
488
499
|
*
|
|
489
|
-
* @throws Error if the format is invalid (not `
|
|
500
|
+
* @throws Error if the format is invalid (not `NUMBERBIGRAM`)
|
|
490
501
|
*/
|
|
491
502
|
export function parseTag(ref: string): { line: number; hash: string } {
|
|
492
|
-
//
|
|
493
|
-
// 1. optional leading "
|
|
503
|
+
// Captures:
|
|
504
|
+
// 1. optional leading ">+-" markers and whitespace
|
|
494
505
|
// 2. line number (1+ digits)
|
|
495
|
-
// 3.
|
|
496
|
-
|
|
497
|
-
// 5. optional trailing display suffix (":..." or " ...")
|
|
498
|
-
const match = ref.match(/^\s*[>+-]*\s*(\d+)\s*#\s*([ZPMQVRWSNKTXJBYH]{2})/);
|
|
506
|
+
// 3. hash (one BPE bigram from HASHLINE_BIGRAMS) directly adjacent (no separator)
|
|
507
|
+
const match = ref.match(new RegExp(`^\\s*[>+-]*\\s*(\\d+)(${HASHLINE_BIGRAM_RE_SRC})`));
|
|
499
508
|
if (!match) {
|
|
500
|
-
throw new Error(`Invalid line reference
|
|
509
|
+
throw new Error(`Invalid line reference. Expected ${formatFullAnchorRequirement(ref)}.`);
|
|
501
510
|
}
|
|
502
511
|
const line = Number.parseInt(match[1], 10);
|
|
503
512
|
if (line < 1) {
|
|
@@ -516,8 +525,8 @@ const MISMATCH_CONTEXT = 2;
|
|
|
516
525
|
/**
|
|
517
526
|
* Error thrown when one or more hashline references have stale hashes.
|
|
518
527
|
*
|
|
519
|
-
* Displays grep-style output with
|
|
520
|
-
* showing the correct `LINE
|
|
528
|
+
* Displays grep-style output with `>` separator on mismatched lines and `:` on
|
|
529
|
+
* surrounding context, showing the correct `LINE+ID` so the caller can fix all refs at once.
|
|
521
530
|
*/
|
|
522
531
|
export class HashlineMismatchError extends Error {
|
|
523
532
|
readonly remaps: ReadonlyMap<string, string>;
|
|
@@ -530,11 +539,50 @@ export class HashlineMismatchError extends Error {
|
|
|
530
539
|
const remaps = new Map<string, string>();
|
|
531
540
|
for (const m of mismatches) {
|
|
532
541
|
const actual = computeLineHash(m.line, fileLines[m.line - 1]);
|
|
533
|
-
remaps.set(`${m.line}
|
|
542
|
+
remaps.set(`${m.line}${m.expected}`, `${m.line}${actual}`);
|
|
534
543
|
}
|
|
535
544
|
this.remaps = remaps;
|
|
536
545
|
}
|
|
537
546
|
|
|
547
|
+
/**
|
|
548
|
+
* User-visible variant of {@link formatMessage} — omits the bigram fingerprint
|
|
549
|
+
* and uses a `│` gutter so TUI rendering is clean. The model still receives
|
|
550
|
+
* the full `LINE+ID|content` form via {@link Error.message}.
|
|
551
|
+
*/
|
|
552
|
+
get displayMessage(): string {
|
|
553
|
+
return HashlineMismatchError.formatDisplayMessage(this.mismatches, this.fileLines);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
static formatDisplayMessage(mismatches: HashMismatch[], fileLines: string[]): string {
|
|
557
|
+
const mismatchSet = new Set<number>();
|
|
558
|
+
for (const m of mismatches) mismatchSet.add(m.line);
|
|
559
|
+
|
|
560
|
+
const displayLines = new Set<number>();
|
|
561
|
+
for (const m of mismatches) {
|
|
562
|
+
const lo = Math.max(1, m.line - MISMATCH_CONTEXT);
|
|
563
|
+
const hi = Math.min(fileLines.length, m.line + MISMATCH_CONTEXT);
|
|
564
|
+
for (let i = lo; i <= hi; i++) displayLines.add(i);
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
const sorted = [...displayLines].sort((a, b) => a - b);
|
|
568
|
+
const out: string[] = [
|
|
569
|
+
`Edit rejected: ${mismatches.length} line${mismatches.length > 1 ? "s have" : " has"} changed since the last read. The edit was NOT applied.`,
|
|
570
|
+
"Realign your edit to the file state shown below. Copy the full anchors exactly as shown (for example `160sr`, not just `sr`).",
|
|
571
|
+
"",
|
|
572
|
+
];
|
|
573
|
+
|
|
574
|
+
const lineNumberWidth = sorted.reduce((width, lineNum) => Math.max(width, String(lineNum).length), 0);
|
|
575
|
+
let prevLine = -1;
|
|
576
|
+
for (const lineNum of sorted) {
|
|
577
|
+
if (prevLine !== -1 && lineNum > prevLine + 1) out.push("...");
|
|
578
|
+
prevLine = lineNum;
|
|
579
|
+
const text = fileLines[lineNum - 1];
|
|
580
|
+
const marker = mismatchSet.has(lineNum) ? "*" : " ";
|
|
581
|
+
out.push(formatCodeFrameLine(marker, lineNum, text ?? "", lineNumberWidth));
|
|
582
|
+
}
|
|
583
|
+
return out.join("\n");
|
|
584
|
+
}
|
|
585
|
+
|
|
538
586
|
static formatMessage(mismatches: HashMismatch[], fileLines: string[]): string {
|
|
539
587
|
const mismatchSet = new Map<number, HashMismatch>();
|
|
540
588
|
for (const m of mismatches) {
|
|
@@ -555,7 +603,8 @@ export class HashlineMismatchError extends Error {
|
|
|
555
603
|
const lines: string[] = [];
|
|
556
604
|
|
|
557
605
|
lines.push(
|
|
558
|
-
`Edit rejected: ${mismatches.length} line${mismatches.length > 1 ? "s have" : " has"} changed since the last read. The edit was NOT applied
|
|
606
|
+
`Edit rejected: ${mismatches.length} line${mismatches.length > 1 ? "s have" : " has"} changed since the last read. The edit was NOT applied.`,
|
|
607
|
+
"Use the updated anchors shown below (`>` marks changed lines, `:` marks context) and retry the edit.",
|
|
559
608
|
);
|
|
560
609
|
lines.push("");
|
|
561
610
|
|
|
@@ -563,18 +612,18 @@ export class HashlineMismatchError extends Error {
|
|
|
563
612
|
for (const lineNum of sorted) {
|
|
564
613
|
// Gap separator between non-contiguous regions
|
|
565
614
|
if (prevLine !== -1 && lineNum > prevLine + 1) {
|
|
566
|
-
lines.push("
|
|
615
|
+
lines.push("...");
|
|
567
616
|
}
|
|
568
617
|
prevLine = lineNum;
|
|
569
618
|
|
|
570
619
|
const text = fileLines[lineNum - 1];
|
|
571
620
|
const hash = computeLineHash(lineNum, text);
|
|
572
|
-
const prefix = `${lineNum}
|
|
621
|
+
const prefix = `${lineNum}${hash}`;
|
|
573
622
|
|
|
574
623
|
if (mismatchSet.has(lineNum)) {
|
|
575
|
-
lines.push(
|
|
624
|
+
lines.push(`${prefix}>${text}`);
|
|
576
625
|
} else {
|
|
577
|
-
lines.push(
|
|
626
|
+
lines.push(`${prefix}:${text}`);
|
|
578
627
|
}
|
|
579
628
|
}
|
|
580
629
|
return lines.join("\n");
|
|
@@ -599,6 +648,39 @@ export function validateLineRef(ref: { line: number; hash: string }, fileLines:
|
|
|
599
648
|
}
|
|
600
649
|
}
|
|
601
650
|
|
|
651
|
+
/**
|
|
652
|
+
* Default search window for {@link tryRebaseAnchor} (lines on each side of the requested anchor).
|
|
653
|
+
*/
|
|
654
|
+
export const ANCHOR_REBASE_WINDOW = 2;
|
|
655
|
+
|
|
656
|
+
/**
|
|
657
|
+
* Look for the requested hash within ±`window` lines of `anchor.line`.
|
|
658
|
+
*
|
|
659
|
+
* Returns the new line number when exactly one nearby line matches the hash;
|
|
660
|
+
* otherwise `null` (genuine mismatch or ambiguous). The caller is expected to
|
|
661
|
+
* mutate `anchor.line` in place and surface a warning so the model knows the
|
|
662
|
+
* edit was retargeted.
|
|
663
|
+
*
|
|
664
|
+
* The exact-position match (anchor.line itself) is intentionally skipped: the
|
|
665
|
+
* caller has already determined the requested line's hash does not match.
|
|
666
|
+
*/
|
|
667
|
+
export function tryRebaseAnchor(
|
|
668
|
+
anchor: { line: number; hash: string },
|
|
669
|
+
fileLines: string[],
|
|
670
|
+
window: number = ANCHOR_REBASE_WINDOW,
|
|
671
|
+
): number | null {
|
|
672
|
+
const lo = Math.max(1, anchor.line - window);
|
|
673
|
+
const hi = Math.min(fileLines.length, anchor.line + window);
|
|
674
|
+
let found: number | null = null;
|
|
675
|
+
for (let line = lo; line <= hi; line++) {
|
|
676
|
+
if (line === anchor.line) continue;
|
|
677
|
+
if (computeLineHash(line, fileLines[line - 1]) !== anchor.hash) continue;
|
|
678
|
+
if (found !== null) return null; // ambiguous: more than one match in window
|
|
679
|
+
found = line;
|
|
680
|
+
}
|
|
681
|
+
return found;
|
|
682
|
+
}
|
|
683
|
+
|
|
602
684
|
function isEscapedTabAutocorrectEnabled(): boolean {
|
|
603
685
|
switch (Bun.env.PI_HASHLINE_AUTOCORRECT_ESCAPED_TABS) {
|
|
604
686
|
case "0":
|
|
@@ -675,7 +757,7 @@ function collectBoundaryDuplicationWarning(edit: HashlineEdit, originalFileLines
|
|
|
675
757
|
const trimmedNext = nextSurvivingLine.trim();
|
|
676
758
|
const trimmedLast = lastInsertedLine.trim();
|
|
677
759
|
if (trimmedLast.length > 0 && trimmedLast === trimmedNext) {
|
|
678
|
-
const tag =
|
|
760
|
+
const tag = formatHashLine(endLine + 1, nextSurvivingLine);
|
|
679
761
|
warnings.push(
|
|
680
762
|
`Possible boundary duplication: your last replacement line \`${trimmedLast}\` is identical to the next surviving line ${tag}. ` +
|
|
681
763
|
`If you meant to replace the entire block, set \`end\` to ${tag} instead.`,
|
|
@@ -754,7 +836,7 @@ function applyHashlineEditToLines(
|
|
|
754
836
|
if (origLines.length === newLines.length && origLines.every((line, i) => line === newLines[i])) {
|
|
755
837
|
noopEdits.push({
|
|
756
838
|
editIndex,
|
|
757
|
-
loc: `${edit.pos.line}
|
|
839
|
+
loc: `${edit.pos.line}${edit.pos.hash}`,
|
|
758
840
|
current: origLines.join("\n"),
|
|
759
841
|
});
|
|
760
842
|
break;
|
|
@@ -765,6 +847,15 @@ function applyHashlineEditToLines(
|
|
|
765
847
|
}
|
|
766
848
|
case "replace_range": {
|
|
767
849
|
const count = edit.end.line - edit.pos.line + 1;
|
|
850
|
+
const origRange = originalFileLines.slice(edit.pos.line - 1, edit.pos.line - 1 + count);
|
|
851
|
+
if (count === edit.lines.length && origRange.every((line, i) => line === edit.lines[i])) {
|
|
852
|
+
noopEdits.push({
|
|
853
|
+
editIndex,
|
|
854
|
+
loc: `${edit.pos.line}${edit.pos.hash}-${edit.end.line}${edit.end.hash}`,
|
|
855
|
+
current: origRange.join("\n"),
|
|
856
|
+
});
|
|
857
|
+
break;
|
|
858
|
+
}
|
|
768
859
|
fileLines.splice(edit.pos.line - 1, count, ...edit.lines);
|
|
769
860
|
trackFirstChanged(edit.pos.line);
|
|
770
861
|
break;
|
|
@@ -774,7 +865,7 @@ function applyHashlineEditToLines(
|
|
|
774
865
|
if (inserted.length === 0) {
|
|
775
866
|
noopEdits.push({
|
|
776
867
|
editIndex,
|
|
777
|
-
loc: `${edit.pos.line}
|
|
868
|
+
loc: `${edit.pos.line}${edit.pos.hash}`,
|
|
778
869
|
current: originalFileLines[edit.pos.line - 1],
|
|
779
870
|
});
|
|
780
871
|
break;
|
|
@@ -788,7 +879,7 @@ function applyHashlineEditToLines(
|
|
|
788
879
|
if (inserted.length === 0) {
|
|
789
880
|
noopEdits.push({
|
|
790
881
|
editIndex,
|
|
791
|
-
loc: `${edit.pos.line}
|
|
882
|
+
loc: `${edit.pos.line}${edit.pos.hash}`,
|
|
792
883
|
current: originalFileLines[edit.pos.line - 1],
|
|
793
884
|
});
|
|
794
885
|
break;
|
|
@@ -849,7 +940,7 @@ function buildHashlineEditResult(params: {
|
|
|
849
940
|
};
|
|
850
941
|
}
|
|
851
942
|
|
|
852
|
-
function validateHashlineEditRefs(edits: HashlineEdit[], fileLines: string[]): HashMismatch[] {
|
|
943
|
+
function validateHashlineEditRefs(edits: HashlineEdit[], fileLines: string[], warnings: string[]): HashMismatch[] {
|
|
853
944
|
const mismatches: HashMismatch[] = [];
|
|
854
945
|
for (const edit of edits) {
|
|
855
946
|
switch (edit.op) {
|
|
@@ -884,6 +975,15 @@ function validateHashlineEditRefs(edits: HashlineEdit[], fileLines: string[]): H
|
|
|
884
975
|
if (actualHash === ref.hash) {
|
|
885
976
|
return;
|
|
886
977
|
}
|
|
978
|
+
const rebased = tryRebaseAnchor(ref, fileLines);
|
|
979
|
+
if (rebased !== null) {
|
|
980
|
+
const original = `${ref.line}${ref.hash}`;
|
|
981
|
+
ref.line = rebased;
|
|
982
|
+
warnings.push(
|
|
983
|
+
`Auto-rebased anchor ${original} → ${rebased}${ref.hash} (line shifted within ±${ANCHOR_REBASE_WINDOW}; hash matched).`,
|
|
984
|
+
);
|
|
985
|
+
return;
|
|
986
|
+
}
|
|
887
987
|
mismatches.push({ line: ref.line, expected: ref.hash, actual: actualHash });
|
|
888
988
|
}
|
|
889
989
|
}
|
|
@@ -922,7 +1022,7 @@ export function applyHashlineEdits(
|
|
|
922
1022
|
const noopEdits: Array<{ editIndex: number; loc: string; current: string }> = [];
|
|
923
1023
|
const warnings: string[] = [];
|
|
924
1024
|
|
|
925
|
-
const mismatches = validateHashlineEditRefs(edits, fileLines);
|
|
1025
|
+
const mismatches = validateHashlineEditRefs(edits, fileLines, warnings);
|
|
926
1026
|
if (mismatches.length > 0) {
|
|
927
1027
|
throw new HashlineMismatchError(mismatches, fileLines);
|
|
928
1028
|
}
|
|
@@ -1022,14 +1122,12 @@ function syncNewLineCounters(counters: CompactPreviewCounters, lineNumber: numbe
|
|
|
1022
1122
|
counters.newLine = lineNumber;
|
|
1023
1123
|
}
|
|
1024
1124
|
|
|
1025
|
-
function formatCompactHashlineLine(kind: " " | "+", lineNumber: number,
|
|
1026
|
-
|
|
1027
|
-
return `${kind}${padded}#${computeLineHash(lineNumber, content)}|${content}`;
|
|
1125
|
+
function formatCompactHashlineLine(kind: " " | "+", lineNumber: number, content: string): string {
|
|
1126
|
+
return `${kind}${lineNumber}${computeLineHash(lineNumber, content)}${HASHLINE_CONTENT_SEPARATOR}${content}`;
|
|
1028
1127
|
}
|
|
1029
1128
|
|
|
1030
|
-
function formatCompactRemovedLine(lineNumber: number,
|
|
1031
|
-
|
|
1032
|
-
return `-${padded}${HASHLINE_PREVIEW_PLACEHOLDER}|${content}`;
|
|
1129
|
+
function formatCompactRemovedLine(lineNumber: number, content: string): string {
|
|
1130
|
+
return `-${lineNumber}${HASHLINE_PREVIEW_PLACEHOLDER}${HASHLINE_CONTENT_SEPARATOR}${content}`;
|
|
1033
1131
|
}
|
|
1034
1132
|
|
|
1035
1133
|
function formatCompactPreviewLine(line: string, counters: CompactPreviewCounters): { kind: DiffRunKind; text: string } {
|
|
@@ -1050,13 +1148,13 @@ function formatCompactPreviewLine(line: string, counters: CompactPreviewCounters
|
|
|
1050
1148
|
syncNewLineCounters(counters, parsed.lineNumber);
|
|
1051
1149
|
const newLine = counters.newLine;
|
|
1052
1150
|
if (newLine === undefined) return { kind: "+", text: parsed.raw };
|
|
1053
|
-
const text = formatCompactHashlineLine("+", newLine, parsed.
|
|
1151
|
+
const text = formatCompactHashlineLine("+", newLine, parsed.content);
|
|
1054
1152
|
counters.newLine = newLine + 1;
|
|
1055
1153
|
return { kind: "+", text };
|
|
1056
1154
|
}
|
|
1057
1155
|
case "-": {
|
|
1058
1156
|
syncOldLineCounters(counters, parsed.lineNumber);
|
|
1059
|
-
const text = formatCompactRemovedLine(parsed.lineNumber, parsed.
|
|
1157
|
+
const text = formatCompactRemovedLine(parsed.lineNumber, parsed.content);
|
|
1060
1158
|
counters.oldLine = parsed.lineNumber + 1;
|
|
1061
1159
|
return { kind: "-", text };
|
|
1062
1160
|
}
|
|
@@ -1064,7 +1162,7 @@ function formatCompactPreviewLine(line: string, counters: CompactPreviewCounters
|
|
|
1064
1162
|
syncOldLineCounters(counters, parsed.lineNumber);
|
|
1065
1163
|
const newLine = counters.newLine;
|
|
1066
1164
|
if (newLine === undefined) return { kind: " ", text: parsed.raw };
|
|
1067
|
-
const text = formatCompactHashlineLine(" ", newLine, parsed.
|
|
1165
|
+
const text = formatCompactHashlineLine(" ", newLine, parsed.content);
|
|
1068
1166
|
counters.oldLine = parsed.lineNumber + 1;
|
|
1069
1167
|
counters.newLine = newLine + 1;
|
|
1070
1168
|
return { kind: " ", text };
|
|
@@ -1170,7 +1268,7 @@ export function buildCompactHashlineDiffPreview(
|
|
|
1170
1268
|
}
|
|
1171
1269
|
|
|
1172
1270
|
export async function computeHashlineDiff(
|
|
1173
|
-
input: { path: string; edits: HashlineEditInput[]
|
|
1271
|
+
input: { path: string; edits: HashlineEditInput[] },
|
|
1174
1272
|
cwd: string,
|
|
1175
1273
|
): Promise<
|
|
1176
1274
|
| {
|
|
@@ -1181,28 +1279,19 @@ export async function computeHashlineDiff(
|
|
|
1181
1279
|
error: string;
|
|
1182
1280
|
}
|
|
1183
1281
|
> {
|
|
1184
|
-
const { path, edits
|
|
1282
|
+
const { path, edits } = input;
|
|
1185
1283
|
|
|
1186
1284
|
try {
|
|
1187
1285
|
const absolutePath = resolveToCwd(path, cwd);
|
|
1188
|
-
const movePath = move ? resolveToCwd(move, cwd) : undefined;
|
|
1189
|
-
const isMoveOnly = Boolean(movePath) && movePath !== absolutePath && edits.length === 0;
|
|
1190
1286
|
const resolvedEdits = resolveHashlineEditsForDiff(edits);
|
|
1191
1287
|
const file = Bun.file(absolutePath);
|
|
1192
1288
|
|
|
1193
|
-
if (movePath === absolutePath) {
|
|
1194
|
-
return { error: "move path is the same as source path" };
|
|
1195
|
-
}
|
|
1196
|
-
if (isMoveOnly) {
|
|
1197
|
-
return { diff: "", firstChangedLine: undefined };
|
|
1198
|
-
}
|
|
1199
|
-
|
|
1200
1289
|
const rawContent = await readHashlineFileText(file, path);
|
|
1201
1290
|
|
|
1202
1291
|
const { text: content } = stripBom(rawContent);
|
|
1203
1292
|
const normalizedContent = normalizeToLF(content);
|
|
1204
1293
|
const result = applyHashlineEdits(normalizedContent, resolvedEdits);
|
|
1205
|
-
if (normalizedContent === result.lines
|
|
1294
|
+
if (normalizedContent === result.lines) {
|
|
1206
1295
|
return { error: `No changes would be made to ${path}. The edits produce identical content.` };
|
|
1207
1296
|
}
|
|
1208
1297
|
|
|
@@ -1229,63 +1318,18 @@ export async function executeHashlineSingle(
|
|
|
1229
1318
|
): Promise<AgentToolResult<EditToolDetails, typeof hashlineEditParamsSchema>> {
|
|
1230
1319
|
const { session, path, edits, signal, batchRequest, writethrough, beginDeferredDiagnosticsForPath } = options;
|
|
1231
1320
|
|
|
1232
|
-
// Extract file-level ops from edits
|
|
1233
|
-
const deleteFile = edits.some(e => e.delete);
|
|
1234
|
-
const move = edits.find(e => e.move)?.move;
|
|
1235
|
-
// Filter to content edits only (those with loc)
|
|
1236
1321
|
const contentEdits = edits.filter(e => e.loc != null);
|
|
1237
1322
|
|
|
1238
|
-
enforcePlanModeWrite(session, path, { op:
|
|
1323
|
+
enforcePlanModeWrite(session, path, { op: "update" });
|
|
1239
1324
|
|
|
1240
1325
|
if (path.endsWith(".ipynb") && contentEdits.length > 0) {
|
|
1241
1326
|
throw new Error("Cannot edit Jupyter notebooks with the Edit tool. Use the NotebookEdit tool instead.");
|
|
1242
1327
|
}
|
|
1243
1328
|
|
|
1244
1329
|
const absolutePath = resolvePlanPath(session, path);
|
|
1245
|
-
const resolvedMove = move ? resolvePlanPath(session, move) : undefined;
|
|
1246
|
-
if (resolvedMove === absolutePath) {
|
|
1247
|
-
throw new Error("move path is the same as source path");
|
|
1248
|
-
}
|
|
1249
1330
|
|
|
1250
1331
|
const sourceFile = Bun.file(absolutePath);
|
|
1251
1332
|
const sourceExists = await sourceFile.exists();
|
|
1252
|
-
const isMoveOnly = Boolean(resolvedMove) && contentEdits.length === 0;
|
|
1253
|
-
|
|
1254
|
-
if (deleteFile) {
|
|
1255
|
-
if (sourceExists) {
|
|
1256
|
-
await sourceFile.unlink();
|
|
1257
|
-
}
|
|
1258
|
-
invalidateFsScanAfterDelete(absolutePath);
|
|
1259
|
-
return {
|
|
1260
|
-
content: [{ type: "text", text: `Deleted ${path}` }],
|
|
1261
|
-
details: {
|
|
1262
|
-
diff: "",
|
|
1263
|
-
op: "delete",
|
|
1264
|
-
meta: outputMeta().get(),
|
|
1265
|
-
},
|
|
1266
|
-
};
|
|
1267
|
-
}
|
|
1268
|
-
|
|
1269
|
-
if (isMoveOnly && resolvedMove) {
|
|
1270
|
-
if (!sourceExists) {
|
|
1271
|
-
throw new Error(`File not found: ${path}`);
|
|
1272
|
-
}
|
|
1273
|
-
const parentDir = nodePath.dirname(resolvedMove);
|
|
1274
|
-
if (parentDir && parentDir !== ".") {
|
|
1275
|
-
await fs.mkdir(parentDir, { recursive: true });
|
|
1276
|
-
}
|
|
1277
|
-
await fs.rename(absolutePath, resolvedMove);
|
|
1278
|
-
invalidateFsScanAfterRename(absolutePath, resolvedMove);
|
|
1279
|
-
return {
|
|
1280
|
-
content: [{ type: "text", text: `Moved ${path} to ${move}` }],
|
|
1281
|
-
details: {
|
|
1282
|
-
diff: "",
|
|
1283
|
-
op: "update",
|
|
1284
|
-
move,
|
|
1285
|
-
meta: outputMeta().get(),
|
|
1286
|
-
},
|
|
1287
|
-
};
|
|
1288
|
-
}
|
|
1289
1333
|
|
|
1290
1334
|
if (!sourceExists) {
|
|
1291
1335
|
const lines: string[] = [];
|
|
@@ -1329,7 +1373,7 @@ export async function executeHashlineSingle(
|
|
|
1329
1373
|
warnings: anchorResult.warnings,
|
|
1330
1374
|
noopEdits: anchorResult.noopEdits,
|
|
1331
1375
|
};
|
|
1332
|
-
if (originalNormalized === result.text
|
|
1376
|
+
if (originalNormalized === result.text) {
|
|
1333
1377
|
let diagnostic = `No changes made to ${path}. The edits produced identical content.`;
|
|
1334
1378
|
if (result.noopEdits && result.noopEdits.length > 0) {
|
|
1335
1379
|
const details = result.noopEdits
|
|
@@ -1345,28 +1389,32 @@ export async function executeHashlineSingle(
|
|
|
1345
1389
|
diagnostic += `\nThe file currently contains these lines:\n${preview}\nYour edits were normalized back to the original content (whitespace-only differences are preserved as-is). Ensure your replacement changes actual code, not just formatting.`;
|
|
1346
1390
|
}
|
|
1347
1391
|
}
|
|
1392
|
+
if (result.noopEdits.some(e => e.loc.includes("-"))) {
|
|
1393
|
+
diagnostic +=
|
|
1394
|
+
"\nHint: a `range` loc replaces the entire span inclusive of both endpoints. " +
|
|
1395
|
+
"If your replacement repeats the existing content, narrow the range or change the replacement.";
|
|
1396
|
+
}
|
|
1348
1397
|
}
|
|
1349
1398
|
throw new Error(diagnostic);
|
|
1350
1399
|
}
|
|
1351
1400
|
|
|
1352
|
-
const writePath = resolvedMove ?? absolutePath;
|
|
1353
1401
|
const finalContent = bom + restoreLineEndings(result.text, originalEnding);
|
|
1354
|
-
const diagnostics = await writethrough(
|
|
1355
|
-
|
|
1402
|
+
const diagnostics = await writethrough(
|
|
1403
|
+
absolutePath,
|
|
1404
|
+
finalContent,
|
|
1405
|
+
signal,
|
|
1406
|
+
Bun.file(absolutePath),
|
|
1407
|
+
batchRequest,
|
|
1408
|
+
dst => (dst === absolutePath ? beginDeferredDiagnosticsForPath(absolutePath) : undefined),
|
|
1356
1409
|
);
|
|
1357
|
-
|
|
1358
|
-
await sourceFile.unlink();
|
|
1359
|
-
invalidateFsScanAfterRename(absolutePath, resolvedMove);
|
|
1360
|
-
} else {
|
|
1361
|
-
invalidateFsScanAfterWrite(absolutePath);
|
|
1362
|
-
}
|
|
1410
|
+
invalidateFsScanAfterWrite(absolutePath);
|
|
1363
1411
|
|
|
1364
1412
|
const diffResult = generateDiffString(originalNormalized, result.text);
|
|
1365
1413
|
const meta = outputMeta()
|
|
1366
1414
|
.diagnostics(diagnostics?.summary ?? "", diagnostics?.messages ?? [])
|
|
1367
1415
|
.get();
|
|
1368
1416
|
|
|
1369
|
-
const resultText =
|
|
1417
|
+
const resultText = `Updated ${path}`;
|
|
1370
1418
|
const preview = buildCompactHashlineDiffPreview(diffResult.diff);
|
|
1371
1419
|
const summaryLine = `Changes: +${preview.addedLines} -${preview.removedLines}${preview.preview ? "" : " (no textual diff preview)"}`;
|
|
1372
1420
|
const warningsBlock = result.warnings?.length ? `\n\nWarnings:\n${result.warnings.join("\n")}` : "";
|
|
@@ -1384,7 +1432,6 @@ export async function executeHashlineSingle(
|
|
|
1384
1432
|
firstChangedLine: result.firstChangedLine ?? diffResult.firstChangedLine,
|
|
1385
1433
|
diagnostics,
|
|
1386
1434
|
op: "update",
|
|
1387
|
-
move,
|
|
1388
1435
|
meta,
|
|
1389
1436
|
},
|
|
1390
1437
|
};
|