@oh-my-pi/pi-coding-agent 14.4.0 → 14.4.3
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 +70 -0
- package/package.json +7 -7
- package/src/cli.ts +0 -1
- package/src/config/prompt-templates.ts +1 -31
- package/src/config/settings-schema.ts +27 -37
- package/src/config/settings.ts +1 -1
- package/src/edit/index.ts +1 -53
- package/src/edit/line-hash.ts +13 -63
- package/src/edit/modes/atom.ts +334 -64
- package/src/edit/modes/hashline.ts +19 -26
- package/src/edit/renderer.ts +6 -8
- package/src/edit/streaming.ts +90 -114
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +10 -15
- package/src/internal-urls/docs-index.generated.ts +1 -2
- package/src/lsp/defaults.json +142 -652
- package/src/modes/components/session-selector.ts +3 -3
- package/src/modes/components/settings-defs.ts +0 -5
- package/src/modes/components/tool-execution.ts +2 -5
- package/src/modes/controllers/btw-controller.ts +17 -105
- package/src/modes/controllers/todo-command-controller.ts +537 -0
- package/src/modes/interactive-mode.ts +35 -9
- package/src/modes/types.ts +2 -0
- package/src/modes/utils/ui-helpers.ts +17 -0
- package/src/prompts/system/irc-incoming.md +8 -0
- package/src/prompts/system/subagent-system-prompt.md +8 -0
- package/src/prompts/tools/ast-edit.md +1 -1
- package/src/prompts/tools/ast-grep.md +1 -0
- package/src/prompts/tools/atom.md +55 -53
- package/src/prompts/tools/bash.md +2 -2
- package/src/prompts/tools/grep.md +2 -5
- package/src/prompts/tools/irc.md +49 -0
- package/src/prompts/tools/job.md +11 -0
- package/src/prompts/tools/read.md +12 -13
- package/src/prompts/tools/task.md +1 -1
- package/src/prompts/tools/todo-write.md +14 -5
- package/src/registry/agent-registry.ts +139 -0
- package/src/sdk.ts +35 -0
- package/src/session/agent-session.ts +217 -5
- package/src/session/session-manager.ts +4 -1
- package/src/session/streaming-output.ts +1 -1
- package/src/slash-commands/builtin-registry.ts +24 -0
- package/src/task/executor.ts +14 -0
- package/src/tools/bash.ts +1 -1
- package/src/tools/fetch.ts +18 -6
- package/src/tools/fs-cache-invalidation.ts +0 -5
- package/src/tools/grep.ts +5 -125
- package/src/tools/index.ts +12 -6
- package/src/tools/irc.ts +258 -0
- package/src/tools/job.ts +489 -0
- package/src/tools/match-line-format.ts +8 -7
- package/src/tools/output-meta.ts +1 -1
- package/src/tools/read.ts +37 -131
- package/src/tools/renderers.ts +2 -0
- package/src/tools/todo-write.ts +243 -12
- package/src/tools/write.ts +2 -2
- package/src/utils/edit-mode.ts +1 -2
- package/src/utils/file-display-mode.ts +0 -3
- package/src/cli/read-cli.ts +0 -67
- package/src/commands/read.ts +0 -33
- package/src/edit/modes/chunk.ts +0 -832
- package/src/prompts/tools/cancel-job.md +0 -5
- package/src/prompts/tools/chunk-edit.md +0 -158
- package/src/prompts/tools/poll.md +0 -5
- package/src/prompts/tools/read-chunk.md +0 -73
- package/src/tools/cancel-job.ts +0 -95
- package/src/tools/poll-tool.ts +0 -173
|
@@ -8,7 +8,7 @@
|
|
|
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: `LINE+ID
|
|
11
|
+
* Displayed format: `LINE+ID|TEXT`
|
|
12
12
|
* Reference format: `"LINE+ID"` (e.g. `"1ab"`)
|
|
13
13
|
*
|
|
14
14
|
* In tool JSON, each edit's `content` is `string[]` (one string per logical line) or
|
|
@@ -28,7 +28,7 @@ import { resolveToCwd } from "../../tools/path-utils";
|
|
|
28
28
|
import { enforcePlanModeWrite, resolvePlanPath } from "../../tools/plan-mode-guard";
|
|
29
29
|
import { formatCodeFrameLine } from "../../tools/render-utils";
|
|
30
30
|
import { generateDiffString } from "../diff";
|
|
31
|
-
import { computeLineHash,
|
|
31
|
+
import { computeLineHash, formatHashLine, HASHLINE_BIGRAM_RE_SRC, HASHLINE_CONTENT_SEPARATOR } from "../line-hash";
|
|
32
32
|
import { detectLineEnding, normalizeToLF, restoreLineEndings, stripBom } from "../normalize";
|
|
33
33
|
import type { EditToolDetails, LspBatchRequest } from "../renderer";
|
|
34
34
|
|
|
@@ -38,7 +38,7 @@ export interface HashMismatch {
|
|
|
38
38
|
actual: string;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
export type Anchor = { line: number; hash: string };
|
|
41
|
+
export type Anchor = { line: number; hash: string; contentHint?: string };
|
|
42
42
|
export type HashlineEdit =
|
|
43
43
|
| { op: "replace_line"; pos: Anchor; lines: string[] }
|
|
44
44
|
| { op: "replace_range"; pos: Anchor; end: Anchor; lines: string[] }
|
|
@@ -47,10 +47,11 @@ export type HashlineEdit =
|
|
|
47
47
|
| { op: "append_file"; lines: string[] }
|
|
48
48
|
| { op: "prepend_file"; lines: string[] };
|
|
49
49
|
|
|
50
|
-
// Tight prefix matchers for the new format `LINE+ID
|
|
51
|
-
//
|
|
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 = "[:|]";
|
|
54
55
|
const HASHLINE_PREFIX_RE = new RegExp(
|
|
55
56
|
`^\\s*(?:>>>|>>)?\\s*(?:\\+\\s*)?\\d+${HASHLINE_BIGRAM_RE_SRC}${HASHLINE_CONTENT_SEPARATOR_RE}`,
|
|
56
57
|
);
|
|
@@ -58,7 +59,7 @@ const HASHLINE_PREFIX_PLUS_RE = new RegExp(
|
|
|
58
59
|
`^\\s*(?:>>>|>>)?\\s*\\+\\s*\\d+${HASHLINE_BIGRAM_RE_SRC}${HASHLINE_CONTENT_SEPARATOR_RE}`,
|
|
59
60
|
);
|
|
60
61
|
const DIFF_PLUS_RE = /^[+](?![+])/;
|
|
61
|
-
const READ_TRUNCATION_NOTICE_RE = /^\[(?:Showing lines \d+-\d+ of \d+|\d+ more lines? in (?:file|\S+))\b.*\bsel=L
|
|
62
|
+
const READ_TRUNCATION_NOTICE_RE = /^\[(?:Showing lines \d+-\d+ of \d+|\d+ more lines? in (?:file|\S+))\b.*\bsel=L?\d+/;
|
|
62
63
|
|
|
63
64
|
type LinePrefixStats = {
|
|
64
65
|
nonEmpty: number;
|
|
@@ -312,8 +313,6 @@ interface ResolvedHashlineStreamOptions {
|
|
|
312
313
|
maxChunkBytes: number;
|
|
313
314
|
}
|
|
314
315
|
|
|
315
|
-
type HashlineLineFormatter = (lineNumber: number, line: string) => string;
|
|
316
|
-
|
|
317
316
|
interface HashlineChunkEmitter {
|
|
318
317
|
pushLine: (line: string) => string[];
|
|
319
318
|
flush: () => string | undefined;
|
|
@@ -329,7 +328,7 @@ function resolveHashlineStreamOptions(options: HashlineStreamOptions): ResolvedH
|
|
|
329
328
|
|
|
330
329
|
function createHashlineChunkEmitter(
|
|
331
330
|
options: ResolvedHashlineStreamOptions,
|
|
332
|
-
formatLine
|
|
331
|
+
formatLine = formatHashLine,
|
|
333
332
|
): HashlineChunkEmitter {
|
|
334
333
|
let lineNumber = options.startLine;
|
|
335
334
|
let outLines: string[] = [];
|
|
@@ -373,10 +372,6 @@ function createHashlineChunkEmitter(
|
|
|
373
372
|
return { pushLine, flush };
|
|
374
373
|
}
|
|
375
374
|
|
|
376
|
-
function formatHashlineStreamLine(lineNumber: number, line: string): string {
|
|
377
|
-
return `${formatLineHash(lineNumber, line)}${HASHLINE_CONTENT_SEPARATOR}${line}`;
|
|
378
|
-
}
|
|
379
|
-
|
|
380
375
|
function isReadableStream(value: unknown): value is ReadableStream<Uint8Array> {
|
|
381
376
|
return (
|
|
382
377
|
typeof value === "object" &&
|
|
@@ -416,7 +411,7 @@ export async function* streamHashLinesFromUtf8(
|
|
|
416
411
|
let pending = "";
|
|
417
412
|
let sawAnyText = false;
|
|
418
413
|
let endedWithNewline = false;
|
|
419
|
-
const emitter = createHashlineChunkEmitter(resolvedOptions
|
|
414
|
+
const emitter = createHashlineChunkEmitter(resolvedOptions);
|
|
420
415
|
|
|
421
416
|
const consumeText = (text: string): string[] => {
|
|
422
417
|
if (text.length === 0) return [];
|
|
@@ -469,7 +464,7 @@ export async function* streamHashLinesFromLines(
|
|
|
469
464
|
options: HashlineStreamOptions = {},
|
|
470
465
|
): AsyncGenerator<string> {
|
|
471
466
|
const resolvedOptions = resolveHashlineStreamOptions(options);
|
|
472
|
-
const emitter = createHashlineChunkEmitter(resolvedOptions
|
|
467
|
+
const emitter = createHashlineChunkEmitter(resolvedOptions);
|
|
473
468
|
let sawAnyLine = false;
|
|
474
469
|
|
|
475
470
|
const asyncIterator = (lines as AsyncIterable<string>)[Symbol.asyncIterator];
|
|
@@ -530,7 +525,7 @@ const MISMATCH_CONTEXT = 2;
|
|
|
530
525
|
/**
|
|
531
526
|
* Error thrown when one or more hashline references have stale hashes.
|
|
532
527
|
*
|
|
533
|
-
* Displays grep-style output with
|
|
528
|
+
* Displays grep-style output with `*` marker on mismatched lines and a leading space on
|
|
534
529
|
* surrounding context, showing the correct `LINE+ID` so the caller can fix all refs at once.
|
|
535
530
|
*/
|
|
536
531
|
export class HashlineMismatchError extends Error {
|
|
@@ -552,7 +547,7 @@ export class HashlineMismatchError extends Error {
|
|
|
552
547
|
/**
|
|
553
548
|
* User-visible variant of {@link formatMessage} — omits the bigram fingerprint
|
|
554
549
|
* and uses a `│` gutter so TUI rendering is clean. The model still receives
|
|
555
|
-
* the full `LINE+ID
|
|
550
|
+
* the full `LINE+ID|content` form via {@link Error.message}.
|
|
556
551
|
*/
|
|
557
552
|
get displayMessage(): string {
|
|
558
553
|
return HashlineMismatchError.formatDisplayMessage(this.mismatches, this.fileLines);
|
|
@@ -609,7 +604,7 @@ export class HashlineMismatchError extends Error {
|
|
|
609
604
|
|
|
610
605
|
lines.push(
|
|
611
606
|
`Edit rejected: ${mismatches.length} line${mismatches.length > 1 ? "s have" : " has"} changed since the last read. The edit was NOT applied.`,
|
|
612
|
-
"Use the updated anchors shown below (
|
|
607
|
+
"Use the updated anchors shown below (`*` marks changed lines, leading space marks context) and retry the edit.",
|
|
613
608
|
);
|
|
614
609
|
lines.push("");
|
|
615
610
|
|
|
@@ -626,9 +621,9 @@ export class HashlineMismatchError extends Error {
|
|
|
626
621
|
const prefix = `${lineNum}${hash}`;
|
|
627
622
|
|
|
628
623
|
if (mismatchSet.has(lineNum)) {
|
|
629
|
-
lines.push(
|
|
624
|
+
lines.push(`*${prefix}|${text}`);
|
|
630
625
|
} else {
|
|
631
|
-
lines.push(
|
|
626
|
+
lines.push(` ${prefix}|${text}`);
|
|
632
627
|
}
|
|
633
628
|
}
|
|
634
629
|
return lines.join("\n");
|
|
@@ -762,7 +757,7 @@ function collectBoundaryDuplicationWarning(edit: HashlineEdit, originalFileLines
|
|
|
762
757
|
const trimmedNext = nextSurvivingLine.trim();
|
|
763
758
|
const trimmedLast = lastInsertedLine.trim();
|
|
764
759
|
if (trimmedLast.length > 0 && trimmedLast === trimmedNext) {
|
|
765
|
-
const tag =
|
|
760
|
+
const tag = formatHashLine(endLine + 1, nextSurvivingLine);
|
|
766
761
|
warnings.push(
|
|
767
762
|
`Possible boundary duplication: your last replacement line \`${trimmedLast}\` is identical to the next surviving line ${tag}. ` +
|
|
768
763
|
`If you meant to replace the entire block, set \`end\` to ${tag} instead.`,
|
|
@@ -1065,7 +1060,6 @@ export interface CompactHashlineDiffPreview {
|
|
|
1065
1060
|
|
|
1066
1061
|
export interface CompactHashlineDiffOptions {
|
|
1067
1062
|
maxUnchangedRun?: number;
|
|
1068
|
-
maxAdditionRun?: number;
|
|
1069
1063
|
maxDeletionRun?: number;
|
|
1070
1064
|
maxOutputLines?: number;
|
|
1071
1065
|
}
|
|
@@ -1221,7 +1215,6 @@ export function buildCompactHashlineDiffPreview(
|
|
|
1221
1215
|
options: CompactHashlineDiffOptions = {},
|
|
1222
1216
|
): CompactHashlineDiffPreview {
|
|
1223
1217
|
const maxUnchangedRun = options.maxUnchangedRun ?? 2;
|
|
1224
|
-
const maxAdditionRun = options.maxAdditionRun ?? 2;
|
|
1225
1218
|
const maxDeletionRun = options.maxDeletionRun ?? 2;
|
|
1226
1219
|
const maxOutputLines = options.maxOutputLines ?? 16;
|
|
1227
1220
|
|
|
@@ -1240,7 +1233,7 @@ export function buildCompactHashlineDiffPreview(
|
|
|
1240
1233
|
break;
|
|
1241
1234
|
case "+":
|
|
1242
1235
|
addedLines += run.lines.length;
|
|
1243
|
-
out.push(...
|
|
1236
|
+
out.push(...run.lines);
|
|
1244
1237
|
break;
|
|
1245
1238
|
case "-":
|
|
1246
1239
|
removedLines += run.lines.length;
|
package/src/edit/renderer.ts
CHANGED
|
@@ -94,7 +94,7 @@ interface EditRenderArgs {
|
|
|
94
94
|
*/
|
|
95
95
|
previewDiff?: string;
|
|
96
96
|
__partialJson?: string;
|
|
97
|
-
// Hashline
|
|
97
|
+
// Hashline mode fields
|
|
98
98
|
edits?: EditRenderEntry[];
|
|
99
99
|
}
|
|
100
100
|
|
|
@@ -141,7 +141,7 @@ export interface EditRenderContext {
|
|
|
141
141
|
editMode?: EditMode;
|
|
142
142
|
/** Pre-computed diff preview (computed before tool executes) */
|
|
143
143
|
editDiffPreview?: DiffResult | DiffError;
|
|
144
|
-
/** Multi-file streaming diff preview (
|
|
144
|
+
/** Multi-file streaming diff preview (edits spanning several files) */
|
|
145
145
|
perFileDiffPreview?: PerFileDiffPreview[];
|
|
146
146
|
/** Function to render diff text with syntax highlighting */
|
|
147
147
|
renderDiff?: (diffText: string, options?: { filePath?: string }) => string;
|
|
@@ -151,11 +151,9 @@ const EDIT_STREAMING_PREVIEW_LINES = 12;
|
|
|
151
151
|
const CALL_TEXT_PREVIEW_LINES = 6;
|
|
152
152
|
const CALL_TEXT_PREVIEW_WIDTH = 80;
|
|
153
153
|
|
|
154
|
-
/** Extract file path from an edit entry
|
|
154
|
+
/** Extract file path from an edit entry. */
|
|
155
155
|
function filePathFromEditEntry(p: string | undefined): string | undefined {
|
|
156
|
-
|
|
157
|
-
const ci = /^[a-zA-Z]:[/\\]/.test(p) ? p.indexOf(":", 2) : p.indexOf(":");
|
|
158
|
-
return ci === -1 ? p : p.slice(0, ci);
|
|
156
|
+
return p ?? undefined;
|
|
159
157
|
}
|
|
160
158
|
|
|
161
159
|
function decodePartialJsonStringFragment(fragment: string): string {
|
|
@@ -284,7 +282,7 @@ function formatMultiFileStreamingDiff(previews: PerFileDiffPreview[], uiTheme: T
|
|
|
284
282
|
const parts: string[] = [];
|
|
285
283
|
for (const preview of previews) {
|
|
286
284
|
if (!preview.diff && !preview.error) continue;
|
|
287
|
-
const header = uiTheme.fg("dim", `\n\n
|
|
285
|
+
const header = uiTheme.fg("dim", `\n\n\u2500\u2500 ${shortenPath(preview.path)} \u2500\u2500`);
|
|
288
286
|
if (preview.error) {
|
|
289
287
|
parts.push(`${header}\n${uiTheme.fg("error", replaceTabs(preview.error))}`);
|
|
290
288
|
continue;
|
|
@@ -303,7 +301,7 @@ function getCallPreview(
|
|
|
303
301
|
renderContext: EditRenderContext | undefined,
|
|
304
302
|
): string {
|
|
305
303
|
const multi = renderContext?.perFileDiffPreview;
|
|
306
|
-
if (multi && multi.length >
|
|
304
|
+
if (multi && multi.length > 1 && multi.some(p => p.diff || p.error)) {
|
|
307
305
|
return formatMultiFileStreamingDiff(multi, uiTheme);
|
|
308
306
|
}
|
|
309
307
|
if (args.previewDiff) {
|
package/src/edit/streaming.ts
CHANGED
|
@@ -16,7 +16,6 @@ import type { Theme } from "../modes/theme/theme";
|
|
|
16
16
|
import { type EditMode, resolveEditMode } from "../utils/edit-mode";
|
|
17
17
|
import { computeEditDiff, type DiffError, type DiffResult } from "./diff";
|
|
18
18
|
import { expandApplyPatchToEntries, expandApplyPatchToPreviewEntries } from "./modes/apply-patch";
|
|
19
|
-
import { type ChunkToolEdit, computeChunkDiff, parseChunkEditPath } from "./modes/chunk";
|
|
20
19
|
import { computeHashlineDiff, type HashlineToolEdit } from "./modes/hashline";
|
|
21
20
|
import { computePatchDiff, type PatchEditEntry } from "./modes/patch";
|
|
22
21
|
import type { ReplaceEditEntry } from "./modes/replace";
|
|
@@ -126,6 +125,38 @@ export function dropIncompleteLastEdit<T>(edits: readonly T[], partialJson: stri
|
|
|
126
125
|
return [...edits];
|
|
127
126
|
}
|
|
128
127
|
|
|
128
|
+
// -----------------------------------------------------------------------------
|
|
129
|
+
// Multi-file grouping
|
|
130
|
+
// -----------------------------------------------------------------------------
|
|
131
|
+
|
|
132
|
+
/** Cap on how many distinct files a streaming preview will render diffs for. */
|
|
133
|
+
const MAX_PREVIEW_FILES = 5;
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Group a list of edits by their effective `path` (per-edit `path` falls back
|
|
137
|
+
* to the top-level `args.path`). Insertion order is preserved and the number of
|
|
138
|
+
* distinct buckets is capped at {@link MAX_PREVIEW_FILES}.
|
|
139
|
+
*/
|
|
140
|
+
function groupEditsByPath<T extends { path?: string }>(
|
|
141
|
+
edits: readonly T[],
|
|
142
|
+
fallbackPath: string | undefined,
|
|
143
|
+
): Map<string, Array<T & { path: string }>> {
|
|
144
|
+
const groups = new Map<string, Array<T & { path: string }>>();
|
|
145
|
+
for (const edit of edits) {
|
|
146
|
+
if (!edit) continue;
|
|
147
|
+
const editPath = edit.path ?? fallbackPath;
|
|
148
|
+
if (!editPath) continue;
|
|
149
|
+
let bucket = groups.get(editPath);
|
|
150
|
+
if (!bucket) {
|
|
151
|
+
if (groups.size >= MAX_PREVIEW_FILES) continue;
|
|
152
|
+
bucket = [];
|
|
153
|
+
groups.set(editPath, bucket);
|
|
154
|
+
}
|
|
155
|
+
bucket.push({ ...edit, path: editPath });
|
|
156
|
+
}
|
|
157
|
+
return groups;
|
|
158
|
+
}
|
|
159
|
+
|
|
129
160
|
// -----------------------------------------------------------------------------
|
|
130
161
|
// Strategies
|
|
131
162
|
// -----------------------------------------------------------------------------
|
|
@@ -142,22 +173,26 @@ const replaceStrategy: EditStreamingStrategy<ReplaceArgs> = {
|
|
|
142
173
|
return { ...args, edits: dropIncompleteLastEdit(args.edits, partialJson, "edits") };
|
|
143
174
|
},
|
|
144
175
|
async computeDiffPreview(args, ctx) {
|
|
145
|
-
const
|
|
146
|
-
if (
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
176
|
+
const groups = groupEditsByPath(args.edits ?? [], args.path);
|
|
177
|
+
if (groups.size === 0) return null;
|
|
178
|
+
const previews: PerFileDiffPreview[] = [];
|
|
179
|
+
for (const [path, fileEdits] of groups) {
|
|
180
|
+
const first = fileEdits[0];
|
|
181
|
+
if (!first || first.old_text === undefined || first.new_text === undefined) continue;
|
|
182
|
+
ctx.signal.throwIfAborted();
|
|
183
|
+
const result = await computeEditDiff(
|
|
184
|
+
path,
|
|
185
|
+
first.old_text,
|
|
186
|
+
first.new_text,
|
|
187
|
+
ctx.cwd,
|
|
188
|
+
ctx.allowFuzzy ?? true,
|
|
189
|
+
first.all,
|
|
190
|
+
ctx.fuzzyThreshold,
|
|
191
|
+
);
|
|
192
|
+
ctx.signal.throwIfAborted();
|
|
193
|
+
previews.push(toPerFilePreview(path, result));
|
|
194
|
+
}
|
|
195
|
+
return previews.length > 0 ? previews : null;
|
|
161
196
|
},
|
|
162
197
|
renderStreamingFallback() {
|
|
163
198
|
return "";
|
|
@@ -176,17 +211,22 @@ const patchStrategy: EditStreamingStrategy<PatchArgs> = {
|
|
|
176
211
|
return { ...args, edits: dropIncompleteLastEdit(args.edits, partialJson, "edits") };
|
|
177
212
|
},
|
|
178
213
|
async computeDiffPreview(args, ctx) {
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
ctx.
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
214
|
+
const groups = groupEditsByPath(args.edits ?? [], args.path);
|
|
215
|
+
if (groups.size === 0) return null;
|
|
216
|
+
const previews: PerFileDiffPreview[] = [];
|
|
217
|
+
for (const [path, fileEdits] of groups) {
|
|
218
|
+
const first = fileEdits[0];
|
|
219
|
+
if (!first) continue;
|
|
220
|
+
ctx.signal.throwIfAborted();
|
|
221
|
+
const result = await computePatchDiff(
|
|
222
|
+
{ path, op: first.op ?? "update", rename: first.rename, diff: first.diff },
|
|
223
|
+
ctx.cwd,
|
|
224
|
+
{ fuzzyThreshold: ctx.fuzzyThreshold, allowFuzzy: ctx.allowFuzzy },
|
|
225
|
+
);
|
|
226
|
+
ctx.signal.throwIfAborted();
|
|
227
|
+
previews.push(toPerFilePreview(path, result));
|
|
228
|
+
}
|
|
229
|
+
return previews.length > 0 ? previews : null;
|
|
190
230
|
},
|
|
191
231
|
renderStreamingFallback() {
|
|
192
232
|
return "";
|
|
@@ -205,83 +245,14 @@ const hashlineStrategy: EditStreamingStrategy<HashlineArgs> = {
|
|
|
205
245
|
return { ...args, edits: dropIncompleteLastEdit(args.edits, partialJson, "edits") };
|
|
206
246
|
},
|
|
207
247
|
async computeDiffPreview(args, ctx) {
|
|
208
|
-
const
|
|
209
|
-
|
|
210
|
-
if (!path) return null;
|
|
211
|
-
const fileEdits = (args.edits ?? [])
|
|
212
|
-
.map(e => {
|
|
213
|
-
if (!e || typeof e !== "object") return undefined;
|
|
214
|
-
const entryPath = (e as { path?: string }).path ?? args.path;
|
|
215
|
-
if (!entryPath || entryPath !== path) return undefined;
|
|
216
|
-
return { ...(e as HashlineToolEdit), path } as HashlineToolEdit & { path: string };
|
|
217
|
-
})
|
|
218
|
-
.filter((e): e is HashlineToolEdit & { path: string } => e !== undefined);
|
|
219
|
-
ctx.signal.throwIfAborted();
|
|
220
|
-
const result = await computeHashlineDiff({ path, edits: fileEdits }, ctx.cwd);
|
|
221
|
-
ctx.signal.throwIfAborted();
|
|
222
|
-
return [toPerFilePreview(path, result)];
|
|
223
|
-
},
|
|
224
|
-
renderStreamingFallback() {
|
|
225
|
-
return "";
|
|
226
|
-
},
|
|
227
|
-
};
|
|
228
|
-
|
|
229
|
-
interface ChunkArgs {
|
|
230
|
-
path?: string;
|
|
231
|
-
edits?: ChunkToolEdit[];
|
|
232
|
-
__partialJson?: string;
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
const chunkStrategy: EditStreamingStrategy<ChunkArgs> = {
|
|
236
|
-
extractCompleteEdits(args, partialJson) {
|
|
237
|
-
if (!args?.edits) return args;
|
|
238
|
-
let edits = dropIncompleteLastEdit(args.edits, partialJson, "edits");
|
|
239
|
-
// Extra guard: if partial JSON still contains `":nu` / `":nul` (partial
|
|
240
|
-
// `null` literals), `partial-json` may have already surfaced the last
|
|
241
|
-
// entry with `write === null`. When that entry's `}` hasn't closed
|
|
242
|
-
// yet, it has already been dropped above. But if dropping was not
|
|
243
|
-
// triggered (e.g. list still open and no new `{` after), also drop the
|
|
244
|
-
// trailing null-write entry so the preview does not flicker with an
|
|
245
|
-
// error for an incomplete string/null literal.
|
|
246
|
-
if (partialJson && edits.length > 0) {
|
|
247
|
-
const last = edits[edits.length - 1] as Partial<ChunkToolEdit> | undefined;
|
|
248
|
-
const endsInPartialNull = /:\s*nu?l?\s*$/.test(partialJson.trimEnd());
|
|
249
|
-
if (last && endsInPartialNull && last.write === null) {
|
|
250
|
-
edits = edits.slice(0, -1);
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
return { ...args, edits };
|
|
254
|
-
},
|
|
255
|
-
async computeDiffPreview(args, ctx) {
|
|
256
|
-
const edits = args.edits ?? [];
|
|
257
|
-
if (edits.length === 0) return null;
|
|
258
|
-
// Group edits by file path
|
|
259
|
-
const groups = new Map<string, ChunkToolEdit[]>();
|
|
260
|
-
const fileOrder: string[] = [];
|
|
261
|
-
for (const edit of edits) {
|
|
262
|
-
if (!edit) continue;
|
|
263
|
-
const editPath = edit.path ?? args.path;
|
|
264
|
-
if (!editPath) continue;
|
|
265
|
-
const { filePath } = parseChunkEditPath(editPath);
|
|
266
|
-
if (!filePath) continue;
|
|
267
|
-
let bucket = groups.get(filePath);
|
|
268
|
-
if (!bucket) {
|
|
269
|
-
bucket = [];
|
|
270
|
-
groups.set(filePath, bucket);
|
|
271
|
-
fileOrder.push(filePath);
|
|
272
|
-
}
|
|
273
|
-
bucket.push({ ...edit, path: editPath });
|
|
274
|
-
}
|
|
275
|
-
if (fileOrder.length === 0) return null;
|
|
276
|
-
|
|
277
|
-
const MAX_FILES = 5;
|
|
278
|
-
const selected = fileOrder.slice(0, MAX_FILES);
|
|
248
|
+
const groups = groupEditsByPath(args.edits ?? [], args.path);
|
|
249
|
+
if (groups.size === 0) return null;
|
|
279
250
|
const previews: PerFileDiffPreview[] = [];
|
|
280
|
-
for (const
|
|
251
|
+
for (const [path, fileEdits] of groups) {
|
|
281
252
|
ctx.signal.throwIfAborted();
|
|
282
|
-
const
|
|
283
|
-
|
|
284
|
-
previews.push(toPerFilePreview(
|
|
253
|
+
const result = await computeHashlineDiff({ path, edits: fileEdits }, ctx.cwd);
|
|
254
|
+
ctx.signal.throwIfAborted();
|
|
255
|
+
previews.push(toPerFilePreview(path, result));
|
|
285
256
|
}
|
|
286
257
|
return previews;
|
|
287
258
|
},
|
|
@@ -311,16 +282,22 @@ const applyPatchStrategy: EditStreamingStrategy<ApplyPatchArgs> = {
|
|
|
311
282
|
return [{ path: "", error: err instanceof Error ? err.message : String(err) }];
|
|
312
283
|
}
|
|
313
284
|
}
|
|
314
|
-
const
|
|
315
|
-
if (
|
|
316
|
-
|
|
317
|
-
const
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
285
|
+
const groups = groupEditsByPath(entries, undefined);
|
|
286
|
+
if (groups.size === 0) return null;
|
|
287
|
+
const previews: PerFileDiffPreview[] = [];
|
|
288
|
+
for (const [path, fileEntries] of groups) {
|
|
289
|
+
const first = fileEntries[0];
|
|
290
|
+
if (!first) continue;
|
|
291
|
+
ctx.signal.throwIfAborted();
|
|
292
|
+
const result = await computePatchDiff(
|
|
293
|
+
{ path, op: first.op ?? "update", rename: first.rename, diff: first.diff },
|
|
294
|
+
ctx.cwd,
|
|
295
|
+
{ fuzzyThreshold: ctx.fuzzyThreshold, allowFuzzy: ctx.allowFuzzy },
|
|
296
|
+
);
|
|
297
|
+
ctx.signal.throwIfAborted();
|
|
298
|
+
previews.push(toPerFilePreview(path, result));
|
|
299
|
+
}
|
|
300
|
+
return previews.length > 0 ? previews : null;
|
|
324
301
|
},
|
|
325
302
|
renderStreamingFallback() {
|
|
326
303
|
return "";
|
|
@@ -365,7 +342,6 @@ export const EDIT_MODE_STRATEGIES: Record<EditMode, EditStreamingStrategy<unknow
|
|
|
365
342
|
replace: replaceStrategy as EditStreamingStrategy<unknown>,
|
|
366
343
|
patch: patchStrategy as EditStreamingStrategy<unknown>,
|
|
367
344
|
hashline: hashlineStrategy as EditStreamingStrategy<unknown>,
|
|
368
|
-
chunk: chunkStrategy as EditStreamingStrategy<unknown>,
|
|
369
345
|
apply_patch: applyPatchStrategy as EditStreamingStrategy<unknown>,
|
|
370
346
|
vim: vimStrategy,
|
|
371
347
|
atom: atomStrategy as EditStreamingStrategy<unknown>,
|