pi-hashline-edit-pro 6.3.0 → 6.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/README.md +32 -31
- package/index.ts +32 -16
- package/package.json +1 -1
- package/src/auto-read-all.ts +1 -1
- package/src/batch.ts +6 -4
- package/src/config-ui.ts +1 -1
- package/src/constants.ts +5 -2
- package/src/copy-move.ts +90 -65
- package/src/edit-common.ts +93 -51
- package/src/edit-fidelity.ts +16 -6
- package/src/grep.ts +22 -11
- package/src/insert.ts +5 -4
- package/src/model-gate.ts +1 -1
- package/src/payload-contract.ts +13 -9
- package/src/prompts.ts +32 -0
- package/src/read.ts +5 -10
- package/src/replace-match.ts +2 -2
- package/src/replace-render.ts +4 -4
- package/src/replace-response.ts +1 -0
- package/src/replace-undo.ts +2 -2
- package/src/replace.ts +2 -2
- package/src/write-hook.ts +1 -1
- package/tool-prompts/copy-snippet.md +1 -1
- package/tool-prompts/copy.md +2 -2
- package/tool-prompts/grep-guidelines.md +2 -1
- package/tool-prompts/grep-snippet.md +1 -1
- package/tool-prompts/grep.md +1 -1
- package/tool-prompts/insert-guidelines.md +1 -1
- package/tool-prompts/insert.md +1 -1
- package/tool-prompts/move-snippet.md +1 -1
- package/tool-prompts/move.md +2 -2
- package/tool-prompts/read-guidelines.md +3 -3
- package/tool-prompts/read.md +1 -3
- package/tool-prompts/replace-match-guidelines.md +1 -0
- package/tool-prompts/replace-match-snippet.md +1 -1
- package/tool-prompts/replace-match.md +1 -1
- package/tool-prompts/replace.md +3 -2
- package/tool-prompts/transfer-guidelines.md +3 -3
- package/tool-prompts/undo-last-change-guidelines.md +1 -1
- package/tool-prompts/undo-last-change-snippet.md +1 -1
- package/tool-prompts/undo-last-change.md +1 -1
package/src/edit-fidelity.ts
CHANGED
|
@@ -218,15 +218,22 @@ function sameCallHead(payloadLine: string, referenceLine: string): boolean {
|
|
|
218
218
|
return left !== undefined && left === right;
|
|
219
219
|
}
|
|
220
220
|
|
|
221
|
-
function indentMismatchHint(
|
|
221
|
+
function indentMismatchHint(
|
|
222
|
+
payloadLine: string,
|
|
223
|
+
reference: ReferenceRow,
|
|
224
|
+
newAnchor: string | undefined,
|
|
225
|
+
referenceAnchor: string | undefined,
|
|
226
|
+
): string | undefined {
|
|
222
227
|
const payloadIndent = leadingWhitespace(payloadLine);
|
|
223
228
|
const referenceIndent = leadingWhitespace(reference.line);
|
|
224
229
|
if (payloadIndent.length >= referenceIndent.length || !referenceIndent.startsWith(payloadIndent)) return undefined;
|
|
225
230
|
const grams = gramSet([payloadLine], INDENT_SIMILARITY_RUN);
|
|
226
231
|
const sharesLongRun = grams !== undefined && sharesRun(reference.line, grams, INDENT_SIMILARITY_RUN);
|
|
227
232
|
if (!sharesLongRun && !sameCallHead(payloadLine, reference.line)) return undefined;
|
|
228
|
-
const
|
|
229
|
-
|
|
233
|
+
const count = plural(payloadIndent.length, "leading whitespace character");
|
|
234
|
+
const referenceLabel = referenceAnchor === undefined ? "the nearby line" : `neighboring line ${referenceAnchor}${HASH_SEP}`;
|
|
235
|
+
const newLabel = newAnchor === undefined ? "new line" : `new line ${newAnchor}${HASH_SEP}`;
|
|
236
|
+
return `[H_INDENT_MISMATCH] ${newLabel} has ${count}; ${referenceLabel} has ${referenceIndent.length}.`;
|
|
230
237
|
}
|
|
231
238
|
|
|
232
239
|
function referenceRows(lines: string[], start: number, end: number): ReferenceRow[] {
|
|
@@ -373,11 +380,12 @@ export function fidelityHints(
|
|
|
373
380
|
resultContent: string,
|
|
374
381
|
spans: readonly DiffSpan[] | undefined,
|
|
375
382
|
originalHashes?: readonly string[],
|
|
376
|
-
options?: { separatorMoved?: boolean; indentHints?: boolean },
|
|
383
|
+
options?: { separatorMoved?: boolean; indentHints?: boolean; resultHashes?: readonly string[] },
|
|
377
384
|
): string[] {
|
|
378
385
|
if (spans === undefined || spans.length === 0) return [];
|
|
379
386
|
const oldLines = splitLines(originalContent);
|
|
380
387
|
const newLines = splitLines(resultContent);
|
|
388
|
+
const resultHashes = options?.resultHashes;
|
|
381
389
|
const hints: string[] = [];
|
|
382
390
|
const seen = new Set<string>();
|
|
383
391
|
let sensitiveIndex: Map<string, IndexedLine[]> | undefined;
|
|
@@ -460,7 +468,7 @@ export function fidelityHints(
|
|
|
460
468
|
let nearestDistance = Number.POSITIVE_INFINITY;
|
|
461
469
|
for (const candidate of references) {
|
|
462
470
|
if (candidate.index < span.start - INDENT_REFERENCE_WINDOW || candidate.index > span.end + INDENT_REFERENCE_WINDOW) continue;
|
|
463
|
-
if (indentMismatchHint(payload[payloadIndex]!, candidate, undefined) === undefined) continue;
|
|
471
|
+
if (indentMismatchHint(payload[payloadIndex]!, candidate, undefined, undefined) === undefined) continue;
|
|
464
472
|
const distance = Math.abs(candidate.index - span.start);
|
|
465
473
|
if (distance < nearestDistance) {
|
|
466
474
|
nearest = candidate;
|
|
@@ -474,12 +482,14 @@ export function fidelityHints(
|
|
|
474
482
|
}
|
|
475
483
|
return undefined;
|
|
476
484
|
};
|
|
485
|
+
const resultIndexForPayload = (payloadIndex: number): number =>
|
|
486
|
+
span.start + offset + (carried !== undefined && payloadIndex >= carried ? payloadIndex + 1 : payloadIndex);
|
|
477
487
|
for (let index = 0; index < payload.length; index += 1) {
|
|
478
488
|
if (hints.length >= MAX_FIDELITY_HINTS) break;
|
|
479
489
|
const payloadLine = payload[index]!;
|
|
480
490
|
const reference = indentReferenceFor(index);
|
|
481
491
|
if (reference === undefined) continue;
|
|
482
|
-
const indentHint = indentMismatchHint(payloadLine, reference, originalHashes?.[reference.index]);
|
|
492
|
+
const indentHint = indentMismatchHint(payloadLine, reference, resultHashes?.[resultIndexForPayload(index)], originalHashes?.[reference.index]);
|
|
483
493
|
if (indentHint === undefined) continue;
|
|
484
494
|
const key = `indent:${reference.index}:${payloadLine}`;
|
|
485
495
|
if (seen.has(key)) continue;
|
package/src/grep.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { formatSize, DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, type TruncationResult } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { formatSize, DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, createGrepToolDefinition, type TruncationResult } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import { Type } from "typebox";
|
|
4
4
|
import { stat } from "node:fs/promises";
|
|
5
5
|
import { dirname, isAbsolute, join, win32 } from "node:path";
|
|
@@ -11,10 +11,10 @@ import { globToRegex } from "./glob";
|
|
|
11
11
|
import { MAX_HASH_LINES, fmtRow, HASH_LEN, HASH_SEP, HASH_CLASS } from "./hashline";
|
|
12
12
|
import { ANCHOR_POOL_EXHAUSTED_PREFIX, MAX_GREP_LINE_BYTES } from "./constants";
|
|
13
13
|
import { toCwd, toDisplayPath } from "./paths";
|
|
14
|
-
import { loadP,
|
|
14
|
+
import { loadP, loadGuidelines } from "./prompts";
|
|
15
15
|
import { normReq } from "./payload-contract";
|
|
16
16
|
import { DEFAULT_EDIT_FLAGS, withGrepPrompts, type EditToolFlags } from "./edit-common";
|
|
17
|
-
import { abortIf, clipLine, errCode, isRec, makePrepareArguments, rejectUnknownFields, truncateToBytes, visLines,
|
|
17
|
+
import { abortIf, clipLine, errCode, isRec, makePrepareArguments, rejectUnknownFields, truncateToBytes, visLines, gutterWidth } from "./utils";
|
|
18
18
|
import { withAnchorSession, formatAnchorReclaimNotice, takeReclaimedPaths } from "./anchor-registry";
|
|
19
19
|
import { serveRows } from "./served";
|
|
20
20
|
import { Text } from "@earendil-works/pi-tui";
|
|
@@ -351,7 +351,12 @@ async function collectRgMatches(
|
|
|
351
351
|
|
|
352
352
|
|
|
353
353
|
function displayRowsForHit(hit: FileHit): string[] {
|
|
354
|
-
|
|
354
|
+
const matched = new Set(hit.matchLines);
|
|
355
|
+
const width = gutterWidth(hit.lineNumbers.reduce((m, n) => (n > m ? n : m), 0), hit.lineNumbers.length);
|
|
356
|
+
return hit.rows.map((row, index) => {
|
|
357
|
+
const lineNumber = hit.lineNumbers[index]!;
|
|
358
|
+
return `${String(lineNumber).padStart(width)}${matched.has(lineNumber) ? ":" : "-"}${HASH_SEP} ${row}`;
|
|
359
|
+
});
|
|
355
360
|
}
|
|
356
361
|
|
|
357
362
|
const grepToolSchema = Type.Object(
|
|
@@ -408,7 +413,7 @@ export function fmtGrepCall(args: { pattern?: unknown; path?: unknown; glob?: un
|
|
|
408
413
|
if (args?.ignoreCase === true) qualifiers.push("case-insensitive");
|
|
409
414
|
if (typeof args?.context === "number") qualifiers.push(`context ${args.context}`);
|
|
410
415
|
if (typeof args?.limit === "number") qualifiers.push(`limit ${args.limit}`);
|
|
411
|
-
let text = `${theme.fg("toolTitle", theme.bold("
|
|
416
|
+
let text = `${theme.fg("toolTitle", theme.bold("grep"))} ${pattern}`;
|
|
412
417
|
if (qualifiers.length > 0) text += ` ${theme.fg("dim", qualifiers.join(" "))}`;
|
|
413
418
|
return text;
|
|
414
419
|
}
|
|
@@ -436,6 +441,7 @@ function highlightMatches(text: string, regex: RegExp, theme: FgT): string {
|
|
|
436
441
|
}
|
|
437
442
|
|
|
438
443
|
const ANCHORED_ROW_RE = new RegExp(`^${HASH_CLASS}${HASH_SEP}`);
|
|
444
|
+
const GREP_GUTTER_RE = /^\s*\d+(?: │ |[:-]│ )/;
|
|
439
445
|
|
|
440
446
|
function highlightHitRow(row: string, highlight: RegExp, theme: FgT): string {
|
|
441
447
|
const anchored = row.match(ANCHORED_ROW_RE);
|
|
@@ -447,9 +453,9 @@ function styleGrepLine(line: string, highlight: RegExp | undefined, theme: FgT):
|
|
|
447
453
|
if (line.startsWith("=== ")) return theme.fg("accent", line);
|
|
448
454
|
if (line.startsWith("[grep:") || line.startsWith("... ")) return theme.fg("dim", line);
|
|
449
455
|
if (!highlight) return line;
|
|
450
|
-
const gutter =
|
|
451
|
-
if (gutter
|
|
452
|
-
const cut = gutter
|
|
456
|
+
const gutter = GREP_GUTTER_RE.exec(line);
|
|
457
|
+
if (gutter === null) return highlightHitRow(line, highlight, theme);
|
|
458
|
+
const cut = gutter[0].length;
|
|
453
459
|
return line.slice(0, cut) + highlightHitRow(line.slice(cut), highlight, theme);
|
|
454
460
|
}
|
|
455
461
|
|
|
@@ -471,11 +477,11 @@ export function regGrep(pi: ExtensionAPI, flags: EditToolFlags = DEFAULT_EDIT_FL
|
|
|
471
477
|
const prompted = withGrepPrompts({
|
|
472
478
|
description: loadP("../tool-prompts/grep.md"),
|
|
473
479
|
snippet: loadP("../tool-prompts/grep-snippet.md"),
|
|
474
|
-
guidelines:
|
|
480
|
+
guidelines: loadGuidelines("../tool-prompts/grep-guidelines.md"),
|
|
475
481
|
}, flags);
|
|
476
482
|
pi.registerTool({
|
|
477
|
-
name: "
|
|
478
|
-
label: "
|
|
483
|
+
name: "grep",
|
|
484
|
+
label: "Grep",
|
|
479
485
|
description: prompted.description,
|
|
480
486
|
promptSnippet: prompted.snippet,
|
|
481
487
|
promptGuidelines: prompted.guidelines,
|
|
@@ -627,6 +633,7 @@ export function regGrep(pi: ExtensionAPI, flags: EditToolFlags = DEFAULT_EDIT_FL
|
|
|
627
633
|
if (limitTruncated) notes.push(`[grep: showing first ${limit} matches; increase limit to see more.]`);
|
|
628
634
|
if (linesReplaced > 0) notes.push(`[grep: ${linesReplaced} line(s) exceed ${formatSize(MAX_GREP_LINE_BYTES)} and are shown as truncated fragments; use read to see the full lines.]`);
|
|
629
635
|
if (poolSkipped > 0) notes.push(`[grep: ${poolSkipped} file(s) skipped because the session's anchor pool is exhausted; narrow the search.]`);
|
|
636
|
+
if (context > 0 && hits.length > 0) notes.push('[grep: match rows are "line:│", context rows are "line-│".]');
|
|
630
637
|
const truncated = limitTruncated || rowTruncated;
|
|
631
638
|
const truncation: TruncationResult | undefined = rowTruncated
|
|
632
639
|
? {
|
|
@@ -679,3 +686,7 @@ export function regGrep(pi: ExtensionAPI, flags: EditToolFlags = DEFAULT_EDIT_FL
|
|
|
679
686
|
},
|
|
680
687
|
});
|
|
681
688
|
}
|
|
689
|
+
|
|
690
|
+
export function regVanillaGrep(pi: ExtensionAPI, cwd: string): void {
|
|
691
|
+
pi.registerTool(createGrepToolDefinition(cwd));
|
|
692
|
+
}
|
package/src/insert.ts
CHANGED
|
@@ -6,10 +6,11 @@ import { commitEdit } from "./commit";
|
|
|
6
6
|
import { batchMemberFor, executeBatchMember, noteBatchFailure, registerBatchPlanner, type BatchMemberPlan, type BatchPlannerInput } from "./batch";
|
|
7
7
|
import { readNormFile, type NormFile } from "./file-reader";
|
|
8
8
|
import { MAX_HASH_LINES, parseHashRef, parsePayloadText, planEdit, preserveDeletionSeparators, resEdit, resolveAnchorLine, type Anchor, type HTEdit, type StripWarningLocation } from "./hashline";
|
|
9
|
+
import { TEXT_ENDING_NOTE } from "./constants";
|
|
9
10
|
import type { LineEnding } from "./normalize";
|
|
10
11
|
import { stripAnchorRow } from "./hashline/resolve";
|
|
11
12
|
import { withAnchorSession } from "./anchor-registry";
|
|
12
|
-
import { loadP,
|
|
13
|
+
import { loadP, loadGuidelines } from "./prompts";
|
|
13
14
|
import { assertInsertReq, normReq, type InsertReq } from "./payload-contract";
|
|
14
15
|
import { isRec, literalEscapeHints, splitLines } from "./utils";
|
|
15
16
|
import { queuedEdit, editToolBase, editRenderCallWrapper, editRenderResultWrapper, resolveEditTargetWithRequirement, throwIfStrictInput, withInsertPrompts, DEFAULT_EDIT_FLAGS, type EditToolFlags } from "./edit-common";
|
|
@@ -19,7 +20,7 @@ export { assertInsertReq, type InsertReq };
|
|
|
19
20
|
|
|
20
21
|
const insertAnchorSchema = Type.String({
|
|
21
22
|
description:
|
|
22
|
-
"4-char anchor of the line to insert next to
|
|
23
|
+
"4-char anchor of the line to insert next to.",
|
|
23
24
|
});
|
|
24
25
|
|
|
25
26
|
const insertDirectionSchema = Type.Union(
|
|
@@ -28,7 +29,7 @@ const insertDirectionSchema = Type.Union(
|
|
|
28
29
|
);
|
|
29
30
|
const insertTextSchema = Type.String({
|
|
30
31
|
description:
|
|
31
|
-
|
|
32
|
+
`The exact text to insert; an empty string inserts one blank line. ${TEXT_ENDING_NOTE} Never include the anchor line.`,
|
|
32
33
|
});
|
|
33
34
|
|
|
34
35
|
const insertPathRequiredSchema = Type.String({
|
|
@@ -192,7 +193,7 @@ export function buildInsertToolDef(flags: EditToolFlags = DEFAULT_EDIT_FLAGS): I
|
|
|
192
193
|
const prompted = withInsertPrompts({
|
|
193
194
|
description: loadP("../tool-prompts/insert.md"),
|
|
194
195
|
snippet: loadP("../tool-prompts/insert-snippet.md"),
|
|
195
|
-
guidelines:
|
|
196
|
+
guidelines: loadGuidelines("../tool-prompts/insert-guidelines.md"),
|
|
196
197
|
}, flags);
|
|
197
198
|
return {
|
|
198
199
|
name: "insert",
|
package/src/model-gate.ts
CHANGED
package/src/payload-contract.ts
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import { Type } from "typebox";
|
|
2
2
|
import { isRec, normalizeRequest, rejectUnknownFields, assertNoNul } from "./utils";
|
|
3
|
-
import { TEXT_NOT_STRING_MSG, NEW_CONTENT_NOT_STRING_MSG } from "./constants";
|
|
3
|
+
import { TEXT_ENDING_NOTE, TEXT_NOT_STRING_MSG, NEW_CONTENT_NOT_STRING_MSG } from "./constants";
|
|
4
4
|
|
|
5
5
|
const textSchema = Type.String({
|
|
6
6
|
description:
|
|
7
|
-
|
|
7
|
+
`The exact text to write in place of the removed range. "" deletes the range. ${TEXT_ENDING_NOTE}`,
|
|
8
8
|
});
|
|
9
9
|
|
|
10
10
|
const removeFromSchema = Type.String({
|
|
11
11
|
description:
|
|
12
|
-
"4-char anchor of the FIRST line to remove
|
|
12
|
+
"4-char anchor of the FIRST line to remove.",
|
|
13
13
|
});
|
|
14
14
|
|
|
15
15
|
const removeToSchema = Type.String({
|
|
@@ -134,13 +134,14 @@ export function assertInsertReq(request: unknown): asserts request is InsertReq
|
|
|
134
134
|
assertNoNul([request.text]);
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
-
const TRANSFER_KEYS = new Set(["path", "source_from", "source_to", "
|
|
137
|
+
const TRANSFER_KEYS = new Set(["path", "source_from", "source_to", "anchor", "direction"]);
|
|
138
138
|
|
|
139
139
|
export interface TransferReq {
|
|
140
140
|
path?: string;
|
|
141
141
|
source_from: string;
|
|
142
142
|
source_to: string;
|
|
143
|
-
|
|
143
|
+
anchor: string;
|
|
144
|
+
direction: "before" | "after";
|
|
144
145
|
}
|
|
145
146
|
|
|
146
147
|
export function assertTransferReq(request: unknown): asserts request is TransferReq {
|
|
@@ -157,8 +158,11 @@ export function assertTransferReq(request: unknown): asserts request is Transfer
|
|
|
157
158
|
if (typeof request.source_to !== "string" || request.source_to.length === 0) {
|
|
158
159
|
throw new Error('[E_BAD_SHAPE] Copy/move request requires a "source_to" string (4-char anchor from read output).');
|
|
159
160
|
}
|
|
160
|
-
if (typeof request.
|
|
161
|
-
throw new Error('[E_BAD_SHAPE] Copy/move request requires an "
|
|
161
|
+
if (typeof request.anchor !== "string" || request.anchor.length === 0) {
|
|
162
|
+
throw new Error('[E_BAD_SHAPE] Copy/move request requires an "anchor" string (4-char anchor from read output).');
|
|
163
|
+
}
|
|
164
|
+
if (request.direction !== "before" && request.direction !== "after") {
|
|
165
|
+
throw new Error('[E_BAD_SHAPE] Copy/move request "direction" must be "before" or "after" (the anchor is the destination line).');
|
|
162
166
|
}
|
|
163
167
|
}
|
|
164
168
|
|
|
@@ -222,7 +226,7 @@ export function getReplaceMatchInput(args: unknown): { path?: string; replace_fr
|
|
|
222
226
|
|
|
223
227
|
const replaceMatchFromSchema = Type.String({
|
|
224
228
|
description:
|
|
225
|
-
"4-char anchor of the FIRST line of the range searched
|
|
229
|
+
"4-char anchor of the FIRST line of the range searched.",
|
|
226
230
|
});
|
|
227
231
|
const replaceMatchToSchema = Type.String({
|
|
228
232
|
description:
|
|
@@ -234,7 +238,7 @@ const replaceMatchOldStringSchema = Type.String({
|
|
|
234
238
|
});
|
|
235
239
|
const replaceMatchNewStringSchema = Type.String({
|
|
236
240
|
description:
|
|
237
|
-
|
|
241
|
+
`The exact replacement for every matched occurrence. "" deletes the matches. ${TEXT_ENDING_NOTE} The rest of the range is kept byte-for-byte.`,
|
|
238
242
|
});
|
|
239
243
|
const replaceMatchPathRequiredSchema = Type.String({
|
|
240
244
|
description:
|
package/src/prompts.ts
CHANGED
|
@@ -1,6 +1,23 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
import { errCode } from "./utils";
|
|
3
3
|
|
|
4
|
+
export interface Guideline {
|
|
5
|
+
id: string;
|
|
6
|
+
text: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const GUIDELINE_IDS_BY_NAME: Record<string, readonly string[]> = {
|
|
10
|
+
"read-guidelines.md": ["read-reread", "read-outline", "read-match-served"],
|
|
11
|
+
"replace-guidelines.md": ["replace-disjoint", "replace-deletion", "replace-single-line"],
|
|
12
|
+
"replace-match-guidelines.md": ["replace-match-fragment", "replace-match-wide-range"],
|
|
13
|
+
"insert-guidelines.md": ["insert-blank-edges"],
|
|
14
|
+
"copy-guidelines.md": ["copy-single-anchor"],
|
|
15
|
+
"move-guidelines.md": ["move-same-place"],
|
|
16
|
+
"transfer-guidelines.md": ["transfer-boundary-serving", "transfer-same-file", "transfer-cross-file", "transfer-batch-move"],
|
|
17
|
+
"grep-guidelines.md": ["grep-literal", "grep-row-markers"],
|
|
18
|
+
"undo-last-change-guidelines.md": ["undo-single-level", "undo-write-clears", "undo-cross-file-move"],
|
|
19
|
+
};
|
|
20
|
+
|
|
4
21
|
export function loadP(relativePath: string): string {
|
|
5
22
|
return readFileSync(new URL(relativePath, import.meta.url), "utf-8").trim();
|
|
6
23
|
}
|
|
@@ -22,3 +39,18 @@ export function loadGuide(relativePath: string): string[] {
|
|
|
22
39
|
.filter((line) => line.startsWith("- "))
|
|
23
40
|
.map((line) => line.slice(2));
|
|
24
41
|
}
|
|
42
|
+
|
|
43
|
+
export function loadGuidelines(relativePath: string): Guideline[] {
|
|
44
|
+
const name = relativePath.slice(relativePath.lastIndexOf("/") + 1);
|
|
45
|
+
const ids = GUIDELINE_IDS_BY_NAME[name];
|
|
46
|
+
if (ids === undefined) throw new Error(`[E_REGISTRY] No guideline ids are registered for ${relativePath}.`);
|
|
47
|
+
const texts = loadGuide(relativePath);
|
|
48
|
+
if (texts.length === 0) return [];
|
|
49
|
+
if (texts.length !== ids.length) {
|
|
50
|
+
console.warn(`hashline: ${relativePath} holds ${texts.length} guideline(s) but declares ${ids.length}; gating may be wrong`);
|
|
51
|
+
}
|
|
52
|
+
return ids.flatMap((id, index) => {
|
|
53
|
+
const text = texts[index];
|
|
54
|
+
return text === undefined ? [] : [{ id, text }];
|
|
55
|
+
});
|
|
56
|
+
}
|
package/src/read.ts
CHANGED
|
@@ -16,7 +16,7 @@ import { readNormFile, safeSnapId } from "./file-reader";
|
|
|
16
16
|
import { lineHashes, fmtRegion, fmtRow, HASH_SEP, MAX_HASH_LINES, parseHashRef, resolveAnchorLine, AnchorMismatchError } from "./hashline";
|
|
17
17
|
import { toCwd } from "./paths";
|
|
18
18
|
import { abortIf, isRec, makePrepareArguments, numberedRead, visLines, splitLines } from "./utils";
|
|
19
|
-
import { loadP,
|
|
19
|
+
import { loadP, loadGuidelines, type Guideline } from "./prompts";
|
|
20
20
|
import { withReadPrompts, DEFAULT_EDIT_FLAGS, type EditToolFlags } from "./edit-common";
|
|
21
21
|
import { valAccess } from "./validation";
|
|
22
22
|
import { withAnchorSession, adoptAnchors, ownerOf, ownersDifferingOnlyByCase, formatAnchorReclaimNotice, takeReclaimedPaths } from "./anchor-registry";
|
|
@@ -26,8 +26,8 @@ import { Text } from "@earendil-works/pi-tui";
|
|
|
26
26
|
import { anchoredLine, readResultSchema, withStructuredErrors, type AnchoredLine, type ReadResult } from "./structured";
|
|
27
27
|
const R_DESC = loadP("../tool-prompts/read.md");
|
|
28
28
|
const R_SNIPPET = loadP("../tool-prompts/read-snippet.md");
|
|
29
|
-
function readGuide():
|
|
30
|
-
return
|
|
29
|
+
function readGuide(): Guideline[] {
|
|
30
|
+
return loadGuidelines("../tool-prompts/read-guidelines.md");
|
|
31
31
|
}
|
|
32
32
|
function normPosInt(
|
|
33
33
|
value: number | undefined,
|
|
@@ -300,13 +300,8 @@ export function regRead(pi: ExtensionAPI, flags: EditToolFlags = DEFAULT_EDIT_FL
|
|
|
300
300
|
}),
|
|
301
301
|
offset: Type.Optional(
|
|
302
302
|
Type.Union([
|
|
303
|
-
Type.Integer({
|
|
304
|
-
|
|
305
|
-
description: "Line number to start reading from (1-indexed)",
|
|
306
|
-
}),
|
|
307
|
-
Type.String({
|
|
308
|
-
description: "4-char anchor of a served line to start reading from",
|
|
309
|
-
}),
|
|
303
|
+
Type.Integer({ minimum: 1 }),
|
|
304
|
+
Type.String(),
|
|
310
305
|
], { description: "Line number (1-indexed) or a served anchor to start reading from" }),
|
|
311
306
|
),
|
|
312
307
|
limit: Type.Optional(
|
package/src/replace-match.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { readNormFile, type NormFile } from "./file-reader";
|
|
|
6
6
|
import { fmtRegion, MAX_HASH_LINES, parseHashRef, planEdit, preserveDeletionSeparators, resEdit, resolveAnchorLine, stripAnchorRow, type Anchor } from "./hashline";
|
|
7
7
|
import { servedForPath, withAnchorSession } from "./anchor-registry";
|
|
8
8
|
import { batchMemberFor, executeBatchMember, noteBatchFailure, registerBatchPlanner, type BatchMemberPlan, type BatchPlannerInput } from "./batch";
|
|
9
|
-
import { loadP,
|
|
9
|
+
import { loadP, loadGuidelines } from "./prompts";
|
|
10
10
|
import {
|
|
11
11
|
assertReplaceMatchReq,
|
|
12
12
|
buildReplaceMatchToolSchema,
|
|
@@ -177,7 +177,7 @@ export function buildReplaceMatchToolDef(flags: EditToolFlags = DEFAULT_EDIT_FLA
|
|
|
177
177
|
const prompted = withReplaceMatchPrompts({
|
|
178
178
|
description: loadP("../tool-prompts/replace-match.md"),
|
|
179
179
|
snippet: loadP("../tool-prompts/replace-match-snippet.md"),
|
|
180
|
-
guidelines:
|
|
180
|
+
guidelines: loadGuidelines("../tool-prompts/replace-match-guidelines.md"),
|
|
181
181
|
}, flags);
|
|
182
182
|
return {
|
|
183
183
|
name: "replace_match",
|
package/src/replace-render.ts
CHANGED
|
@@ -81,7 +81,7 @@ export function highlightBatchRefs(text: string, theme: FgT): string {
|
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
export function fmtCall(
|
|
84
|
-
args: { path?: string; remove_from?: string; remove_to?: string; anchor?: string; source_from?: string; source_to?: string;
|
|
84
|
+
args: { path?: string; remove_from?: string; remove_to?: string; anchor?: string; source_from?: string; source_to?: string; direction?: string; replace_from?: string; replace_to?: string } | undefined,
|
|
85
85
|
state: RRState,
|
|
86
86
|
expanded: boolean,
|
|
87
87
|
theme: CallT,
|
|
@@ -89,7 +89,7 @@ export function fmtCall(
|
|
|
89
89
|
): string {
|
|
90
90
|
const previewPath = state.preview && "path" in state.preview ? state.preview.path : undefined;
|
|
91
91
|
const path = args?.path ?? state.resolvedPath ?? previewPath;
|
|
92
|
-
const anchorFallback = typeof args?.remove_from === "string" && typeof args?.remove_to === "string" ? `${args.remove_from}→${args.remove_to}` : typeof args?.
|
|
92
|
+
const anchorFallback = typeof args?.remove_from === "string" && typeof args?.remove_to === "string" ? `${args.remove_from}→${args.remove_to}` : typeof args?.source_from === "string" && typeof args?.anchor === "string" ? `${args.source_from}→${args.anchor}` : typeof args?.anchor === "string" ? args.anchor : typeof args?.replace_from === "string" && typeof args?.replace_to === "string" ? `${args.replace_from}→${args.replace_to}` : undefined;
|
|
93
93
|
const pathDisplay =
|
|
94
94
|
typeof path === "string" && path.length > 0
|
|
95
95
|
? theme.fg("accent", path)
|
|
@@ -272,9 +272,9 @@ export function reuseMarkdown(context: any, content: string, theme: any): Markdo
|
|
|
272
272
|
export function makeRenderCall(
|
|
273
273
|
preview: (args: unknown, cwd: string, signal?: AbortSignal) => Promise<RPreview>,
|
|
274
274
|
options: {
|
|
275
|
-
getInput?: (args: unknown) => { path?: string; remove_from?: string; remove_to?: string; anchor?: string; source_from?: string; source_to?: string;
|
|
275
|
+
getInput?: (args: unknown) => { path?: string; remove_from?: string; remove_to?: string; anchor?: string; source_from?: string; source_to?: string; direction?: string; replace_from?: string; replace_to?: string } | null;
|
|
276
276
|
toolName?: string;
|
|
277
|
-
resolveTarget?: (input: { remove_from?: string; remove_to?: string; anchor?: string; source_from?: string; source_to?: string;
|
|
277
|
+
resolveTarget?: (input: { remove_from?: string; remove_to?: string; anchor?: string; source_from?: string; source_to?: string; direction?: string; replace_from?: string; replace_to?: string }) => string | undefined;
|
|
278
278
|
} = {},
|
|
279
279
|
) {
|
|
280
280
|
const getInput = options.getInput ?? getPreviewInput;
|
package/src/replace-response.ts
CHANGED
|
@@ -160,6 +160,7 @@ export function buildChanged(input: SuccessInput, verb = "replaced", diffContext
|
|
|
160
160
|
const fidelity = fidelityHints(originalNormalized, result, spans, originalHashes, {
|
|
161
161
|
separatorMoved: fidelityOptions?.separatorMoved ?? (verb === "inserted" || verb === "edited"),
|
|
162
162
|
indentHints: fidelityOptions?.indentHints ?? (verb !== "copied" && verb !== "moved"),
|
|
163
|
+
resultHashes,
|
|
163
164
|
});
|
|
164
165
|
const { warnings: noticeWarnings, hints } = splitNotices(fidelity.length > 0 ? [...(warnings ?? []), ...fidelity] : warnings);
|
|
165
166
|
const annotatedHints = annotateLiteralEscapeHints(hints, result, spans, resultHashes);
|
package/src/replace-undo.ts
CHANGED
|
@@ -13,7 +13,7 @@ import { joinSeparators } from "./line-endings";
|
|
|
13
13
|
import { genDiff, genPatch, spansFromHashes } from "./replace-diff";
|
|
14
14
|
import { getDiffContextLines } from "./config";
|
|
15
15
|
import { cntDiff, errCode, makePrepareArguments, splitLines } from "./utils";
|
|
16
|
-
import { loadP,
|
|
16
|
+
import { loadP, loadGuidelines } from "./prompts";
|
|
17
17
|
import { withUndoPrompts, DEFAULT_EDIT_FLAGS, type EditToolFlags } from "./edit-common";
|
|
18
18
|
import { buildMetrics } from "./replace-response";
|
|
19
19
|
import { renderEditResult, fmtCall } from "./replace-render";
|
|
@@ -115,7 +115,7 @@ export function regUndo(pi: ExtensionAPI, flags: EditToolFlags = DEFAULT_EDIT_FL
|
|
|
115
115
|
const prompted = withUndoPrompts({
|
|
116
116
|
description: loadP("../tool-prompts/undo-last-change.md"),
|
|
117
117
|
snippet: loadP("../tool-prompts/undo-last-change-snippet.md"),
|
|
118
|
-
guidelines:
|
|
118
|
+
guidelines: loadGuidelines("../tool-prompts/undo-last-change-guidelines.md"),
|
|
119
119
|
}, flags);
|
|
120
120
|
pi.registerTool({
|
|
121
121
|
name: "undo_last_change",
|
package/src/replace.ts
CHANGED
|
@@ -12,7 +12,7 @@ import { readNormFile, type NormFile } from "./file-reader";
|
|
|
12
12
|
import { editToolSchema, buildEditToolSchema, type ReqParams, type RawReqParams, assertReq, normReq } from "./payload-contract";
|
|
13
13
|
import { literalEscapeHints, makePrepareArguments, splitLines } from "./utils";
|
|
14
14
|
import { editResultSchema, withStructuredErrors } from "./structured";
|
|
15
|
-
import { loadP,
|
|
15
|
+
import { loadP, loadGuidelines } from "./prompts";
|
|
16
16
|
import { type FileIdentity } from "./fs-write";
|
|
17
17
|
import { applyEdit,
|
|
18
18
|
lineHashes,
|
|
@@ -292,7 +292,7 @@ export function buildToolDef(flags: EditToolFlags = DEFAULT_EDIT_FLAGS): ToolDef
|
|
|
292
292
|
const prompted = withReplacePrompts({
|
|
293
293
|
description: loadP("../tool-prompts/replace.md"),
|
|
294
294
|
snippet: loadP("../tool-prompts/replace-snippet.md"),
|
|
295
|
-
guidelines:
|
|
295
|
+
guidelines: loadGuidelines("../tool-prompts/replace-guidelines.md"),
|
|
296
296
|
}, flags);
|
|
297
297
|
const parameters = buildEditToolSchema(flags.requirePath);
|
|
298
298
|
return {
|
package/src/write-hook.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { resolveInCwd } from "./fs-write";
|
|
|
6
6
|
import { abortIf, splitLines, isRec } from "./utils";
|
|
7
7
|
import type { ModelLike } from "./model-gate";
|
|
8
8
|
|
|
9
|
-
const HASH_ECHO_RE = new RegExp(`^(?: *[0-9]+
|
|
9
|
+
const HASH_ECHO_RE = new RegExp(`^(?: *[0-9]+(?: │ |[:-]│ ))?[+ -]?(${HASH_CLASS})${HASH_SEP}`);
|
|
10
10
|
|
|
11
11
|
function searchEcho(lines: string[], served: ReadonlyMap<string, string> | ReadonlySet<string>): { line: number; hash: string } | undefined {
|
|
12
12
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
Copy lines to another position by anchor: `source_from`/`source_to` mark the range,
|
|
1
|
+
Copy lines to another position by anchor: `source_from`/`source_to` mark the range, `anchor` and `direction` the destination
|
package/tool-prompts/copy.md
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
|
|
1
|
+
Duplicate a range of lines at another anchor, in this or another file. The source rows and anchors stay live; the copied lines get fresh anchors in the destination's post-edit diff and keep their source line endings.
|
|
2
2
|
|
|
3
|
-
A
|
|
3
|
+
A copy duplicates the content its source anchors were served from, so a same-message edit to the source file does not change what is copied.
|
|
@@ -1 +1,2 @@
|
|
|
1
|
-
- `
|
|
1
|
+
- `grep`: set `literal: true` when the pattern contains regex metacharacters.
|
|
2
|
+
- `grep`: `N:│` rows are matches and `N-│` rows are context;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
Search with `lineNumber
|
|
1
|
+
Search with `lineNumber:│ anchor│content` hits (`lineNumber-│` for context) usable directly for `replace`/`insert`; use `literal`/`glob`/`context`
|
package/tool-prompts/grep.md
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
Search text files with ripgrep. Hits
|
|
1
|
+
Search text files with ripgrep. Hits come back as `lineNumber:│ anchor│content` rows and context lines as `lineNumber-│ anchor│content`, editable with replace, insert, copy, or move without a new read; the `=== path ===` header and line numbers locate the match. Patterns are regex by default; set `literal: true` for exact text, `ignoreCase` for case-insensitive search, and `context` for surrounding rows. Rust regex syntax (no backreferences or look-around) through a linear-time engine. Searches respect `.gitignore`, always skip `.git`, and skip binary and image files silently. A match over 500 bytes is shown as a `...` fragment around the hit, but its anchor still covers the whole line. When cut by match limit, raise `limit` (default 100); when cut by rows/bytes, refine `pattern` or narrow `path`.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
- `insert`:
|
|
1
|
+
- `insert`: a missing or extra `+anchor│` blank row at either block edge in the diff is the classic insert slip.
|
package/tool-prompts/insert.md
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
Insert text after or before one existing line
|
|
1
|
+
Insert text after or before one existing line, addressed by `anchor` (a bare anchor from any served `anchor│content` row). `direction` places `text` after or before the anchor line; `text` is the exact text to insert.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
Move lines to another position by anchor: `source_from`/`source_to` mark the range,
|
|
1
|
+
Move lines to another position by anchor: `source_from`/`source_to` mark the range, `anchor` and `direction` the destination
|
package/tool-prompts/move.md
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
|
|
1
|
+
Relocate a range of lines to another anchor, in this or another file. Within one file the destination must sit outside the source range; moving a range onto itself reports `No changes made` and leaves the anchors alone.
|
|
2
2
|
|
|
3
|
-
A cross-file
|
|
3
|
+
A cross-file move commits on its own when its source file also has batched edits in the message.
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
- `read`: call again after an edit when you need anchors you lack — post-edit diff
|
|
2
|
-
- `read`:
|
|
3
|
-
- `read`:
|
|
1
|
+
- `read`: call again after an edit when you need anchors you lack — post-edit diff `+`/` ` rows and served `anchor│content` rows already carry fresh anchors for the changed range.
|
|
2
|
+
- `read`: an auto-read-all outline row shows a symbol label, or a single-line symbol's content; its `[limit N]` is the span to request.
|
|
3
|
+
- `read`: `replace_match` needs both boundary lines served; copy `old_string` from a served row or read the lines first.
|
package/tool-prompts/read.md
CHANGED
|
@@ -1,3 +1 @@
|
|
|
1
|
-
Read a text file
|
|
2
|
-
|
|
3
|
-
`offset` takes a 1-indexed line number or a served anchor; with an anchor, `limit` counts the lines to return after it. A stale anchor is refused with the current context rows and fresh anchors.
|
|
1
|
+
Read a text file as `anchor│content` rows: a 4-character anchor (letters only, case-sensitive), `│`, then the line. Target edits by anchor, never by content or line number. `offset` takes a 1-indexed line number or a served anchor; with an anchor, `limit` counts lines after it — no line-number math. Page long files with `offset` and `limit`; when output says truncated, continue from the hinted `offset`. A stale anchor is refused with the current rows and fresh anchors. Images attach visually; binary, directory, and UTF-16/UTF-32 text are rejected; an empty file returns one empty row you can replace to seed content.
|
|
@@ -1 +1,2 @@
|
|
|
1
1
|
- `replace_match`: `old_string` is usually a fragment of the line, not the whole line; every occurrence inside the range is replaced, so scope the range with anchors when only some occurrences should change.
|
|
2
|
+
- `replace_match`: when you intend to change every matching line in a range, anchor the first and last matching lines of that range so one call replaces them all, instead of one call per line.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
Replace part of a line or range by anchor: every
|
|
1
|
+
Replace part of a line or range by anchor: every `old_string` occurrence inside `replace_from`/`replace_to` becomes `new_string`; the rest of the line stays untouched
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
Replace part of a line (or a range of lines) without retyping the rest. `replace_from
|
|
1
|
+
Replace part of a line (or a range of lines) without retyping the rest. `replace_from`/`replace_to` are bare anchors from served `anchor│content` rows marking the range's first and last line (the same anchor for one line), `old_string` is the text to find inside it, and `new_string` replaces every occurrence. `remove_from`/`remove_to` are accepted as aliases.
|
|
2
2
|
|
|
3
3
|
The two boundary anchors must still match what was last shown; lines strictly inside the range are matched against the file as it is on disk.
|
package/tool-prompts/replace.md
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
|
-
Replace a range of lines (or a single line) in a text file by anchor
|
|
2
|
-
|
|
1
|
+
Replace a range of lines (or a single line) in a text file by anchor: `remove_from` and `remove_to` are the 4-character anchors of its first and last line, `text` is the exact replacement text, and `replace_from`/`replace_to` are accepted as aliases. Deleting every line empties the file, and the result names the empty-line anchor, so a follow-up `replace` can seed content without a `read`.
|
|
2
|
+
|
|
3
|
+
To change only part of a line without retyping the rest, use `replace_match` instead; it preserves every character the request does not name.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
- `copy`/`move`:
|
|
2
|
-
- `copy`/`move`:
|
|
3
|
-
- `copy`/`move`: `source_from` and `source_to` may live in a different file than `
|
|
1
|
+
- `copy`/`move`: only the source's two boundary rows and the destination line need serving; to append to the end of a file, anchor that file's last line with `direction: "after"` — not a marker line and not the block's own end.
|
|
2
|
+
- `copy`/`move`: within one file the insertion point must fall before the source range's first line or after its last; anchoring the block's last line with `direction: "after"` (or its first line with `direction: "before"`) targets the gap right beside the block.
|
|
3
|
+
- `copy`/`move`: `source_from` and `source_to` may live in a different file than `anchor`; an empty destination file is seeded with the copied or moved lines.
|
|
4
4
|
- `move`: a batched cross-file move does not display the source file's diff; its removal commits with the batch, and the source file needs a read for fresh anchors.
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
- `undo_last_change`: history is one slot per file, not a stack; a second undo returns `E_UNDO_NONE`.
|
|
2
|
-
- `undo_last_change`: a `write` clears the history, so undo
|
|
2
|
+
- `undo_last_change`: a `write` clears the history, so undo before any other edit or write; the undo diff's `+anchor│` rows are the restored lines.
|
|
3
3
|
- `undo_last_change`: a cross-file `move` records one undo entry per file; undo both the source and the destination to revert the whole move, because undoing one side alone leaves the moved lines duplicated or missing.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
Revert a file's most recent edit (single level)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
Undo the last replace, replace_match, insert, copy, or move on a file, restoring the previous content, BOM, and line endings.
|
|
1
|
+
Undo the last replace, replace_match, insert, copy, or move on a file, restoring the previous content, BOM, and line endings. If the file changed since that edit, the undo is refused and the file is left untouched; a file deleted since the edit is restored.
|