pi-hashline-edit-pro 6.1.1 → 6.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -5
- package/index.ts +30 -15
- package/package.json +1 -1
- package/prompts/replace-match.md +1 -1
- package/prompts/replace.md +1 -1
- package/src/batch.ts +2 -2
- package/src/config-ui.ts +2 -2
- package/src/config.ts +24 -6
- package/src/payload-contract.ts +7 -2
- package/src/replace-match.ts +5 -3
- package/src/replace.ts +4 -3
- package/src/utils.ts +19 -3
package/README.md
CHANGED
|
@@ -190,6 +190,8 @@ Edge cases:
|
|
|
190
190
|
| `remove_to` | 4-char anchor marking the LAST line to remove (inclusive). |
|
|
191
191
|
| `text` | The exact text to write in place of the removed range, as one string: `""` deletes the range, `"\n"` is one blank line, and a trailing line break sets the last line's ending instead of adding a blank line. Embedded `\r\n`/`\r`/`\n` are preserved; JSON decoding happens once, before the tool; the tool writes the string it receives and never decodes — `\uXXXX` is the character, `\\uXXXX` the literal text. Legacy arrays are converted to text (elements joined with LF); prefer the string form. |
|
|
192
192
|
|
|
193
|
+
`replace_from` and `replace_to` are accepted as aliases for `remove_from` and `remove_to`.
|
|
194
|
+
|
|
193
195
|
Example: read showed `Hasu│old` and `arvm│old2`; to replace both:
|
|
194
196
|
|
|
195
197
|
```json
|
|
@@ -219,6 +221,8 @@ After a successful edit, the diff is capped at 50KB. A row over 50KB is shown as
|
|
|
219
221
|
|
|
220
222
|
`replace_match` changes part of a line (or a range of lines) without retyping the rest. `replace_from` and `replace_to` are bare anchors marking the first and last line of the range; use the same anchor for a single line. `old_string` is the exact text to find inside that range, and `new_string` replaces every occurrence of it; every other character stays untouched. That makes it the tool for a change the request quotes as a substring: a whole-line `replace` has to reproduce the rest of the line, so a slipped character becomes a wrong byte, while `replace_match` leaves everything the request did not name untouched. It is enabled by default; turn Replace match off in `/hashline-config` to remove the tool.
|
|
221
223
|
|
|
224
|
+
`remove_from` and `remove_to` are accepted as aliases for `replace_from` and `replace_to`.
|
|
225
|
+
|
|
222
226
|
`old_string` is matched against the range's text (LF line breaks, no final terminator) and every non-overlapping occurrence is replaced, left to right. A missing match is refused with `[E_SUBSTRING_NOT_FOUND]` and the current `anchor│content` rows, so the retry needs no `read`. The two boundary anchors are verified against what was last shown; lines strictly inside the range are matched against the file as it currently stands on disk.
|
|
223
227
|
|
|
224
228
|
In a same-message batch it joins the other calls on its file: the batch validates everything against the pre-batch state and commits once, with one undo. A missing `old_string` aborts the whole batch unwritten. The post-edit diff carries fresh anchors, and the edit is undoable with `undo_last_change`.
|
|
@@ -346,7 +350,7 @@ The setting lives in `/hashline-config` as Auto-read all and in `config.json` as
|
|
|
346
350
|
|
|
347
351
|
| Command | Description |
|
|
348
352
|
| --- | --- |
|
|
349
|
-
| `/hashline-config` | Open the settings window: auto-read anchors, auto-read all mode, ignore folders/files, disable on models,
|
|
353
|
+
| `/hashline-config` | Open the settings window: auto-read anchors, auto-read all mode, ignore folders/files, disable on models, read on disabled models, diff context lines, `anchor_grep` tool, copy/move tools, replace_match tool, required `path`, and strict input. Persists across sessions. |
|
|
350
354
|
| `/clear-anchors` | Clear the session's anchor claims. Anchors are re-claimed on the next `read`. |
|
|
351
355
|
|
|
352
356
|
Settings live in `~/.config/pi-hashline-edit-pro/config.json`, created when a setting is first changed in `/hashline-config`:
|
|
@@ -363,7 +367,7 @@ Settings live in `~/.config/pi-hashline-edit-pro/config.json`, created when a se
|
|
|
363
367
|
"strictInput": false,
|
|
364
368
|
"diffContextLines": 1,
|
|
365
369
|
"disableOnModels": [],
|
|
366
|
-
"
|
|
370
|
+
"readOnDisabledModels": "vanilla"
|
|
367
371
|
}
|
|
368
372
|
```
|
|
369
373
|
|
|
@@ -378,10 +382,10 @@ Settings live in `~/.config/pi-hashline-edit-pro/config.json`, created when a se
|
|
|
378
382
|
| `requirePath` | Require path | `false` | `replace`, `replace_match`, `insert`, `copy`, and `move` require a `path` argument that must match anchor ownership. |
|
|
379
383
|
| `strictInput` | Strict input | `false` | Reject auto-fixable slips (`[W_BAD_SHAPE]`, `[W_BAD_REF]`, `[W_INVALID_PATCH]`, `[W_BARE_HASH_PREFIX]`) with `[E_BAD_SHAPE]` instead of applying them with a warning. |
|
|
380
384
|
| `diffContextLines` | Diff context | `1` | Surrounding lines in post-edit diffs, 0-10 (needs Auto-read). |
|
|
381
|
-
| `disableOnModels` | Disable on models | `[]` | Model globs matched case-insensitively against `provider/id`, the bare `id`, and `api`, with `*` and `?` wildcards. A matching model gets no anchored edit tools, no auto-read, and no write-echo refusal; `read`
|
|
382
|
-
| `
|
|
385
|
+
| `disableOnModels` | Disable on models | `[]` | Model globs matched case-insensitively against `provider/id`, the bare `id`, and `api`, with `*` and `?` wildcards. A matching model gets no anchored edit tools, no auto-read, and no write-echo refusal; `read` stays available as pi's ordinary read unless Read on disabled models is `remove`. |
|
|
386
|
+
| `readOnDisabledModels` | Read on disabled models | `"vanilla"` | `read` for a model matched by `disableOnModels`: `"vanilla"` keeps pi's ordinary `read` active while the anchored edit tools stay off; `"remove"` disables `read` with the rest of the anchored tools. |
|
|
383
387
|
|
|
384
|
-
`disableOnModels` turns off the whole anchored surface for the models you name, so another edit tool (`apply_patch`, a shell read) can own the session without competing instructions. A match removes `replace`, `replace_match`, `insert`, `copy`, `move`, `anchor_grep`, and `undo_last_change` from the active tools, and also `read`
|
|
388
|
+
`disableOnModels` turns off the whole anchored surface for the models you name, so another edit tool (`apply_patch`, a shell read) can own the session without competing instructions. A match removes `replace`, `replace_match`, `insert`, `copy`, `move`, `anchor_grep`, and `undo_last_change` from the active tools, and also `read` when Read on disabled models is `remove`, restoring the built-in `grep` when it was active, and it also skips the auto-read-all injection, the auto-read block after `write`, the post-edit diff substitution, and the `write` hook that refuses a `write` echoing a served anchor. Because the extension's `read` replaces the built-in one, a matched model has no `read` at all under `remove` unless another extension provides one; the default `vanilla` gives those models pi's ordinary `read` while the anchored edit tools stay off. The list is checked on `session_start`, on `model_select`, and again on `before_agent_start`, so a model change inside a session switches the surface immediately and another extension cannot re-add the tools mid-session. The default `[]` leaves every model unchanged.
|
|
385
389
|
|
|
386
390
|
When `PI_HASHLINE_DIR` is unset or empty, non-Windows platforms honor `XDG_CONFIG_HOME` when set (falling back to `~/.config`); on Windows the directory always uses `~/.config`, where `~` is `%USERPROFILE%`. To move the directory explicitly, see [Isolated state](#isolated-state).
|
|
387
391
|
|
package/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { createReadToolDefinition, DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import { initHasher } from "./src/hashline";
|
|
4
4
|
import { regReplaceMatch } from "./src/replace-match";
|
|
5
5
|
import { regReplace } from "./src/replace";
|
|
@@ -16,7 +16,7 @@ import type { ReplaceDetails } from "./src/replace";
|
|
|
16
16
|
import { extractHints, extractWarnings } from "./src/replace-render";
|
|
17
17
|
import { MAX_HASH_LINES } from "./src/hashline";
|
|
18
18
|
import { withStructuredText } from "./src/structured";
|
|
19
|
-
import type { AutoReadAllMode } from "./src/config";
|
|
19
|
+
import type { AutoReadAllMode, ReadOnDisabledModels } from "./src/config";
|
|
20
20
|
import {
|
|
21
21
|
readConfig,
|
|
22
22
|
readConfigWithStatus,
|
|
@@ -25,7 +25,7 @@ import {
|
|
|
25
25
|
toggleAnchorGrep,
|
|
26
26
|
toggleCopyMove,
|
|
27
27
|
toggleReplaceMatch,
|
|
28
|
-
|
|
28
|
+
cycleReadOnDisabledModels,
|
|
29
29
|
toggleRequirePath,
|
|
30
30
|
toggleStrictInput,
|
|
31
31
|
adjustDiffContextLines,
|
|
@@ -62,7 +62,9 @@ export default function (pi: ExtensionAPI): void {
|
|
|
62
62
|
let autoReadAll: AutoReadAllMode = "off";
|
|
63
63
|
let autoReadAllIgnore: string[] = [];
|
|
64
64
|
let disableOnModels: string[] = [];
|
|
65
|
-
let
|
|
65
|
+
let readOnDisabledModels: ReadOnDisabledModels = "vanilla";
|
|
66
|
+
let readFlavor: "anchored" | "vanilla" = "anchored";
|
|
67
|
+
let readFlavorCwd: string | undefined;
|
|
66
68
|
let autoReadAllInjected = false;
|
|
67
69
|
let grepWasActive = false;
|
|
68
70
|
const baseAnchorTools = new Set<string>();
|
|
@@ -71,7 +73,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
71
73
|
async function refreshEditTools(): Promise<void> {
|
|
72
74
|
try {
|
|
73
75
|
const flags = await currentEditFlags(pi.getActiveTools().includes("codemode"));
|
|
74
|
-
regRead(pi, flags);
|
|
76
|
+
if (readFlavor !== "vanilla") regRead(pi, flags);
|
|
75
77
|
regReplace(pi, flags);
|
|
76
78
|
regReplaceMatch(pi, flags);
|
|
77
79
|
regInsert(pi, flags);
|
|
@@ -84,11 +86,24 @@ export default function (pi: ExtensionAPI): void {
|
|
|
84
86
|
}
|
|
85
87
|
}
|
|
86
88
|
|
|
87
|
-
async function syncModelGate(model: ModelLike | undefined): Promise<void> {
|
|
88
|
-
|
|
89
|
+
async function syncModelGate(model: ModelLike | undefined, cwd: string): Promise<void> {
|
|
90
|
+
const gated = disableOnModels.length > 0 && modelDisabled(model, disableOnModels);
|
|
91
|
+
if (gated && readOnDisabledModels === "vanilla") {
|
|
92
|
+
if (readFlavor !== "vanilla" || readFlavorCwd !== cwd) {
|
|
93
|
+
pi.registerTool(createReadToolDefinition(cwd));
|
|
94
|
+
readFlavor = "vanilla";
|
|
95
|
+
readFlavorCwd = cwd;
|
|
96
|
+
}
|
|
97
|
+
} else if (readFlavor === "vanilla") {
|
|
98
|
+
readFlavor = "anchored";
|
|
99
|
+
readFlavorCwd = undefined;
|
|
100
|
+
await refreshEditTools();
|
|
101
|
+
}
|
|
102
|
+
if (gated) {
|
|
89
103
|
const active = pi.getActiveTools();
|
|
90
|
-
const
|
|
91
|
-
const next = active.filter((tool) => !
|
|
104
|
+
const gatedTools = readOnDisabledModels === "vanilla" ? ANCHOR_TOOL_NAMES.filter((tool) => tool !== "read") : ANCHOR_TOOL_NAMES;
|
|
105
|
+
const next = active.filter((tool) => !gatedTools.includes(tool));
|
|
106
|
+
if (readOnDisabledModels === "vanilla" && baseAnchorTools.has("read") && !next.includes("read")) next.push("read");
|
|
92
107
|
if (grepWasActive && !next.includes("grep")) next.push("grep");
|
|
93
108
|
pi.setActiveTools(next);
|
|
94
109
|
gateApplied = true;
|
|
@@ -136,7 +151,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
136
151
|
autoReadAll = config.autoReadAll ?? "off";
|
|
137
152
|
autoReadAllIgnore = config.autoReadAllIgnore ?? [];
|
|
138
153
|
disableOnModels = config.disableOnModels ?? [];
|
|
139
|
-
|
|
154
|
+
readOnDisabledModels = config.readOnDisabledModels ?? "vanilla";
|
|
140
155
|
const sessionBranch = (ctx as { sessionManager?: { getBranch?: () => Array<{ type?: string; customType?: string }> } }).sessionManager?.getBranch?.() ?? [];
|
|
141
156
|
autoReadAllInjected = sessionBranch.some((entry) => entry.type === "custom_message" && entry.customType === AUTO_READ_ALL_CUSTOM_TYPE);
|
|
142
157
|
await refreshEditTools();
|
|
@@ -148,7 +163,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
148
163
|
return true;
|
|
149
164
|
}),
|
|
150
165
|
);
|
|
151
|
-
await syncModelGate(ctx.model);
|
|
166
|
+
await syncModelGate(ctx.model, ctx.cwd);
|
|
152
167
|
const debugValue = process.env.PI_HASHLINE_DEBUG;
|
|
153
168
|
if (debugValue === "1" || debugValue === "true") {
|
|
154
169
|
ctx.ui.notify(`Hashline Edit mode active`, "info");
|
|
@@ -165,11 +180,11 @@ export default function (pi: ExtensionAPI): void {
|
|
|
165
180
|
}
|
|
166
181
|
});
|
|
167
182
|
pi.on("model_select", async (event, ctx) => withAnchorSession(ctx, async () => {
|
|
168
|
-
await syncModelGate(event.model);
|
|
183
|
+
await syncModelGate(event.model, ctx.cwd);
|
|
169
184
|
}));
|
|
170
185
|
|
|
171
186
|
pi.on("before_agent_start", async (_event, ctx) => withAnchorSession(ctx, async () => {
|
|
172
|
-
await syncModelGate(ctx.model);
|
|
187
|
+
await syncModelGate(ctx.model, ctx.cwd);
|
|
173
188
|
if (autoReadAll === "off" || autoReadAllInjected) return;
|
|
174
189
|
if (modelDisabled(ctx.model, disableOnModels)) return;
|
|
175
190
|
autoReadAllInjected = true;
|
|
@@ -201,7 +216,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
201
216
|
else if (key === "autoReadAll") { autoReadAll = await cycleAutoReadAllMode(); autoReadAllInjected = false; }
|
|
202
217
|
else if (key === "autoReadAllIgnore") autoReadAllIgnore = await setAutoReadAllIgnoreFromText(value ?? "");
|
|
203
218
|
else if (key === "disableOnModels") disableOnModels = await setDisableOnModelsFromText(value ?? "");
|
|
204
|
-
else if (key === "
|
|
219
|
+
else if (key === "readOnDisabledModels") readOnDisabledModels = await cycleReadOnDisabledModels();
|
|
205
220
|
else if (key === "diffContextLines") await adjustDiffContextLines(delta ?? 1);
|
|
206
221
|
else if (key === "anchorGrepEnabled") {
|
|
207
222
|
const enabled = await toggleAnchorGrep();
|
|
@@ -229,7 +244,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
229
244
|
else if (key === "requirePath") await toggleRequirePath();
|
|
230
245
|
else if (key === "strictInput") await toggleStrictInput();
|
|
231
246
|
await refreshEditTools();
|
|
232
|
-
await syncModelGate(ctx.model);
|
|
247
|
+
await syncModelGate(ctx.model, ctx.cwd);
|
|
233
248
|
},
|
|
234
249
|
});
|
|
235
250
|
await overlay.load();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-hashline-edit-pro",
|
|
3
|
-
"version": "6.1.
|
|
3
|
+
"version": "6.1.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Hash-anchored read/replace/insert/grep tools for pi-coding-agent. Every line gets a unique 4-char tokenizer-friendly anchor that stays stable across edits; stale or ambiguous anchors are rejected, never fuzzy-matched. Undo persists across restarts.",
|
|
6
6
|
"main": "index.ts",
|
package/prompts/replace-match.md
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
Replace part of a line (or a range of lines) without retyping the rest. `replace_from` and `replace_to` are bare anchors from served `anchor│content` rows marking the first and last line of the range; use the same anchor for one line. `old_string` is the text to find inside that range, and `new_string` replaces every occurrence of it.
|
|
1
|
+
Replace part of a line (or a range of lines) without retyping the rest. `replace_from` and `replace_to` are bare anchors from served `anchor│content` rows marking the first and last line of the range; use the same anchor for one line. `old_string` is the text to find inside that range, and `new_string` replaces every occurrence of it. `remove_from`/`remove_to` are accepted as aliases for `replace_from`/`replace_to`.
|
|
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/prompts/replace.md
CHANGED
|
@@ -1,2 +1,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 the first and last line to remove, and `text` is one string holding the exact replacement text. The text is written exactly as given, and nothing else in the file changes.
|
|
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 the first and last line to remove, and `text` is one string holding the exact replacement text. The text is written exactly as given, and nothing else in the file changes. `replace_from`/`replace_to` are accepted as aliases for `remove_from`/`remove_to`.
|
|
2
2
|
To change only part of a line without retyping the rest, use `replace_match` instead; it preserves every character the request does not name. Deleting every line empties the file; the result names the new empty-line anchor, so a follow-up `replace` on it can seed content without a `read`.
|
package/src/batch.ts
CHANGED
|
@@ -233,7 +233,7 @@ export function resetBatchStateForTests(): void {
|
|
|
233
233
|
|
|
234
234
|
function normalizeEditArgs(name: string, args: unknown): NormalizedEditArgs | undefined {
|
|
235
235
|
if (!isRec(args)) return undefined;
|
|
236
|
-
const normalized = normReq(args);
|
|
236
|
+
const normalized = normReq(args, name === "replace_match" ? "replace" : name === "replace" ? "remove" : undefined);
|
|
237
237
|
if (!isRec(normalized)) return undefined;
|
|
238
238
|
const path = typeof normalized.path === "string" ? normalized.path : undefined;
|
|
239
239
|
if (name === "copy" || name === "move") {
|
|
@@ -787,7 +787,7 @@ async function finishBatch(member: PlannedMember, signal?: AbortSignal): Promise
|
|
|
787
787
|
if (!planned) continue;
|
|
788
788
|
if (executedOrders.has(planned.order)) continue;
|
|
789
789
|
try {
|
|
790
|
-
const normalized = normReq(planned.args);
|
|
790
|
+
const normalized = normReq(planned.args, planned.kind === "replace_match" ? "replace" : planned.kind === "replace" ? "remove" : undefined);
|
|
791
791
|
if (planned.kind === "insert") assertInsertReq(normalized);
|
|
792
792
|
else if (planned.kind === "replace") assertReq(normalized);
|
|
793
793
|
else if (planned.kind === "replace_match") assertReplaceMatchReq(normalized);
|
package/src/config-ui.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { Key, matchesKey, visibleWidth } from "@earendil-works/pi-tui";
|
|
|
2
2
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import { readConfig, type Config } from "./config";
|
|
4
4
|
|
|
5
|
-
export type ConfigToggleKey = "autoRead" | "autoReadAll" | "autoReadAllIgnore" | "anchorGrepEnabled" | "copyMoveEnabled" | "replaceMatchEnabled" | "requirePath" | "strictInput" | "diffContextLines" | "disableOnModels" | "
|
|
5
|
+
export type ConfigToggleKey = "autoRead" | "autoReadAll" | "autoReadAllIgnore" | "anchorGrepEnabled" | "copyMoveEnabled" | "replaceMatchEnabled" | "requirePath" | "strictInput" | "diffContextLines" | "disableOnModels" | "readOnDisabledModels";
|
|
6
6
|
|
|
7
7
|
export interface ConfigRow {
|
|
8
8
|
key: ConfigToggleKey;
|
|
@@ -28,7 +28,7 @@ export function configRows(config: Config): ConfigRow[] {
|
|
|
28
28
|
{ key: "strictInput", label: "Strict input", hint: "Reject auto-fixable slips instead of warnings", enabled: config.strictInput === true },
|
|
29
29
|
{ key: "replaceMatchEnabled", label: "Replace match", hint: "replace_match tool (off while disabled)", enabled: config.replaceMatchEnabled !== false },
|
|
30
30
|
{ key: "disableOnModels", label: "Disable on models", hint: "Model globs (provider/id, model id, or api) that turn off the anchored edit tools (comma-separated)", enabled: (config.disableOnModels ?? []).length > 0, entries: config.disableOnModels ?? [] },
|
|
31
|
-
{ key: "
|
|
31
|
+
{ key: "readOnDisabledModels", label: "Read on disabled models", hint: "Read for models matched by Disable on models: vanilla (plain pi read), or remove it", enabled: (config.readOnDisabledModels ?? "vanilla") !== "remove", mode: config.readOnDisabledModels ?? "vanilla", cycle: ["remove", "vanilla"] },
|
|
32
32
|
];
|
|
33
33
|
}
|
|
34
34
|
|
package/src/config.ts
CHANGED
|
@@ -6,6 +6,9 @@ import { writeAtomic } from "./fs-write";
|
|
|
6
6
|
export type AutoReadAllMode = "off" | "on" | "git";
|
|
7
7
|
const AUTO_READ_ALL_MODES: AutoReadAllMode[] = ["off", "on", "git"];
|
|
8
8
|
|
|
9
|
+
export type ReadOnDisabledModels = "remove" | "vanilla";
|
|
10
|
+
const READ_ON_DISABLED_MODELS: ReadOnDisabledModels[] = ["remove", "vanilla"];
|
|
11
|
+
|
|
9
12
|
export const DEFAULT_DIFF_CONTEXT_LINES = 1;
|
|
10
13
|
export const MIN_DIFF_CONTEXT_LINES = 0;
|
|
11
14
|
export const MAX_DIFF_CONTEXT_LINES = 10;
|
|
@@ -21,7 +24,7 @@ export interface Config {
|
|
|
21
24
|
strictInput?: boolean;
|
|
22
25
|
diffContextLines?: number;
|
|
23
26
|
disableOnModels?: string[];
|
|
24
|
-
|
|
27
|
+
readOnDisabledModels?: ReadOnDisabledModels;
|
|
25
28
|
}
|
|
26
29
|
|
|
27
30
|
const DEFAULT_CONFIG: Config = {
|
|
@@ -35,7 +38,7 @@ const DEFAULT_CONFIG: Config = {
|
|
|
35
38
|
strictInput: false,
|
|
36
39
|
diffContextLines: DEFAULT_DIFF_CONTEXT_LINES,
|
|
37
40
|
disableOnModels: [],
|
|
38
|
-
|
|
41
|
+
readOnDisabledModels: "vanilla",
|
|
39
42
|
};
|
|
40
43
|
|
|
41
44
|
function parseAutoReadAllMode(value: unknown): AutoReadAllMode {
|
|
@@ -45,6 +48,12 @@ function parseAutoReadAllMode(value: unknown): AutoReadAllMode {
|
|
|
45
48
|
return DEFAULT_CONFIG.autoReadAll ?? "off";
|
|
46
49
|
}
|
|
47
50
|
|
|
51
|
+
function parseReadOnDisabledModels(value: unknown, legacy: unknown): ReadOnDisabledModels {
|
|
52
|
+
if (value === "remove" || value === "vanilla") return value;
|
|
53
|
+
if (typeof legacy === "boolean") return legacy ? "remove" : "vanilla";
|
|
54
|
+
return DEFAULT_CONFIG.readOnDisabledModels ?? "vanilla";
|
|
55
|
+
}
|
|
56
|
+
|
|
48
57
|
export function normalizeAutoReadAllIgnoreEntry(entry: string): string {
|
|
49
58
|
return entry.trim().replace(/\\/g, "/").replace(/^\/+|\/+$/g, "").replace(/\/{2,}/g, "/");
|
|
50
59
|
}
|
|
@@ -96,7 +105,8 @@ function parseConfig(content: string): Config {
|
|
|
96
105
|
const diffContextLines = parsed.diffContextLines;
|
|
97
106
|
const autoReadAllIgnore = parsed.autoReadAllIgnore;
|
|
98
107
|
const disableOnModels = parsed.disableOnModels;
|
|
99
|
-
const
|
|
108
|
+
const readOnDisabledModels = parsed.readOnDisabledModels;
|
|
109
|
+
const legacyDisableReadOnModels = parsed.disableReadOnModels;
|
|
100
110
|
return {
|
|
101
111
|
autoRead: typeof autoRead === "boolean" ? autoRead : DEFAULT_CONFIG.autoRead,
|
|
102
112
|
anchorGrepEnabled: typeof anchorGrepEnabled === "boolean" ? anchorGrepEnabled : DEFAULT_CONFIG.anchorGrepEnabled,
|
|
@@ -108,7 +118,7 @@ function parseConfig(content: string): Config {
|
|
|
108
118
|
diffContextLines: normalizeDiffContextLines(diffContextLines),
|
|
109
119
|
autoReadAllIgnore: parseAutoReadAllIgnore(autoReadAllIgnore),
|
|
110
120
|
disableOnModels: parseDisableOnModels(disableOnModels),
|
|
111
|
-
|
|
121
|
+
readOnDisabledModels: parseReadOnDisabledModels(readOnDisabledModels, legacyDisableReadOnModels),
|
|
112
122
|
};
|
|
113
123
|
}
|
|
114
124
|
|
|
@@ -216,7 +226,7 @@ export async function writeConfig(config: Config): Promise<void> {
|
|
|
216
226
|
}
|
|
217
227
|
|
|
218
228
|
|
|
219
|
-
type ToggleKey = "autoRead" | "anchorGrepEnabled" | "copyMoveEnabled" | "replaceMatchEnabled" | "requirePath" | "strictInput"
|
|
229
|
+
type ToggleKey = "autoRead" | "anchorGrepEnabled" | "copyMoveEnabled" | "replaceMatchEnabled" | "requirePath" | "strictInput";
|
|
220
230
|
|
|
221
231
|
async function toggleFlag(key: ToggleKey): Promise<boolean> {
|
|
222
232
|
const config = await updateConfig((c) => { c[key] = !(c[key] === true); });
|
|
@@ -226,7 +236,15 @@ export const toggleAutoRead = (): Promise<boolean> => toggleFlag("autoRead");
|
|
|
226
236
|
export const toggleAnchorGrep = (): Promise<boolean> => toggleFlag("anchorGrepEnabled");
|
|
227
237
|
export const toggleCopyMove = (): Promise<boolean> => toggleFlag("copyMoveEnabled");
|
|
228
238
|
export const toggleReplaceMatch = (): Promise<boolean> => toggleFlag("replaceMatchEnabled");
|
|
229
|
-
export
|
|
239
|
+
export async function cycleReadOnDisabledModels(): Promise<ReadOnDisabledModels> {
|
|
240
|
+
let next: ReadOnDisabledModels = "vanilla";
|
|
241
|
+
await updateConfig((c) => {
|
|
242
|
+
const current = c.readOnDisabledModels ?? "vanilla";
|
|
243
|
+
next = READ_ON_DISABLED_MODELS[(READ_ON_DISABLED_MODELS.indexOf(current) + 1) % READ_ON_DISABLED_MODELS.length] ?? "remove";
|
|
244
|
+
c.readOnDisabledModels = next;
|
|
245
|
+
});
|
|
246
|
+
return next;
|
|
247
|
+
}
|
|
230
248
|
export async function cycleAutoReadAllMode(): Promise<AutoReadAllMode> {
|
|
231
249
|
let next: AutoReadAllMode = "off";
|
|
232
250
|
await updateConfig((c) => {
|
package/src/payload-contract.ts
CHANGED
|
@@ -85,7 +85,7 @@ export { normalizeRequest as normReq } from "./utils";
|
|
|
85
85
|
export function getPreviewInput(args: unknown): { path?: string; remove_from: string; remove_to: string; text: string } | null {
|
|
86
86
|
let normalized: unknown;
|
|
87
87
|
try {
|
|
88
|
-
normalized = normalizeRequest(args);
|
|
88
|
+
normalized = normalizeRequest(args, "remove");
|
|
89
89
|
} catch {
|
|
90
90
|
return null;
|
|
91
91
|
}
|
|
@@ -196,7 +196,12 @@ export function assertReplaceMatchReq(request: unknown): asserts request is Repl
|
|
|
196
196
|
}
|
|
197
197
|
|
|
198
198
|
export function getReplaceMatchInput(args: unknown): { path?: string; replace_from: string; replace_to: string; old_string: string; new_string: string } | null {
|
|
199
|
-
|
|
199
|
+
let normalized: unknown;
|
|
200
|
+
try {
|
|
201
|
+
normalized = normalizeRequest(args, "replace");
|
|
202
|
+
} catch {
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
200
205
|
if (!isRec(normalized)) return null;
|
|
201
206
|
if (
|
|
202
207
|
typeof normalized.replace_from !== "string" ||
|
package/src/replace-match.ts
CHANGED
|
@@ -11,9 +11,10 @@ import {
|
|
|
11
11
|
assertReplaceMatchReq,
|
|
12
12
|
buildReplaceMatchToolSchema,
|
|
13
13
|
getReplaceMatchInput,
|
|
14
|
+
normReq,
|
|
14
15
|
type ReplaceMatchReq,
|
|
15
16
|
} from "./payload-contract";
|
|
16
|
-
import { literalEscapeHints, splitLines } from "./utils";
|
|
17
|
+
import { literalEscapeHints, makePrepareArguments, splitLines } from "./utils";
|
|
17
18
|
import { toLF } from "./normalize";
|
|
18
19
|
import { MAX_RANGE_STALE_LINES } from "./constants";
|
|
19
20
|
import {
|
|
@@ -110,7 +111,7 @@ export function buildReplaceMatchEdit(
|
|
|
110
111
|
|
|
111
112
|
export async function replaceMatchPreview(request: unknown, cwd: string, signal?: AbortSignal): Promise<RPreview> {
|
|
112
113
|
try {
|
|
113
|
-
const normalized
|
|
114
|
+
const normalized = normReq(request, "replace");
|
|
114
115
|
assertReplaceMatchReq(normalized);
|
|
115
116
|
const req = normalized;
|
|
116
117
|
const { refs, warnings } = parseMatchAnchors(req);
|
|
@@ -158,6 +159,7 @@ export function buildReplaceMatchToolDef(flags: EditToolFlags = DEFAULT_EDIT_FLA
|
|
|
158
159
|
promptSnippet: prompted.snippet,
|
|
159
160
|
promptGuidelines: prompted.guidelines,
|
|
160
161
|
...editToolBase,
|
|
162
|
+
prepareArguments: makePrepareArguments("replace"),
|
|
161
163
|
parameters: buildReplaceMatchToolSchema(flags.requirePath),
|
|
162
164
|
outputSchema: editResultSchema,
|
|
163
165
|
renderCall: makeRenderCall(replaceMatchPreview, {
|
|
@@ -168,7 +170,7 @@ export function buildReplaceMatchToolDef(flags: EditToolFlags = DEFAULT_EDIT_FLA
|
|
|
168
170
|
renderResult: editRenderResultWrapper,
|
|
169
171
|
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
170
172
|
return withStructuredErrors(signal, { diff: "" }, () => withAnchorSession(ctx, async () => {
|
|
171
|
-
const normalized
|
|
173
|
+
const normalized = normReq(params, "replace");
|
|
172
174
|
assertReplaceMatchReq(normalized);
|
|
173
175
|
const req = normalized;
|
|
174
176
|
let refs: MatchRefs;
|
package/src/replace.ts
CHANGED
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
} from "./replace-diff";
|
|
11
11
|
import { readNormFile, type NormFile } from "./file-reader";
|
|
12
12
|
import { editToolSchema, buildEditToolSchema, type ReqParams, type RawReqParams, assertReq, normReq } from "./payload-contract";
|
|
13
|
-
import { literalEscapeHints, splitLines } from "./utils";
|
|
13
|
+
import { literalEscapeHints, makePrepareArguments, splitLines } from "./utils";
|
|
14
14
|
import { editResultSchema, withStructuredErrors } from "./structured";
|
|
15
15
|
import { loadP, loadGuide } from "./prompts";
|
|
16
16
|
import { type FileIdentity } from "./fs-write";
|
|
@@ -240,7 +240,7 @@ export async function compPreview(
|
|
|
240
240
|
signal?: AbortSignal,
|
|
241
241
|
): Promise<RPreview> {
|
|
242
242
|
try {
|
|
243
|
-
const normalized = normReq(request);
|
|
243
|
+
const normalized = normReq(request, "remove");
|
|
244
244
|
assertReq(normalized);
|
|
245
245
|
const targetPath = await resolveEditTargetWithRequirement({
|
|
246
246
|
removeFrom: (normalized as ReqParams).remove_from,
|
|
@@ -283,11 +283,12 @@ export function buildToolDef(flags: EditToolFlags = DEFAULT_EDIT_FLAGS): ToolDef
|
|
|
283
283
|
promptSnippet: prompted.snippet,
|
|
284
284
|
promptGuidelines: prompted.guidelines,
|
|
285
285
|
...editToolBase,
|
|
286
|
+
prepareArguments: makePrepareArguments("remove"),
|
|
286
287
|
renderCall: editRenderCallWrapper(compPreview),
|
|
287
288
|
renderResult: editRenderResultWrapper,
|
|
288
289
|
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
289
290
|
return withStructuredErrors(signal, { diff: "" }, () => withAnchorSession(ctx, async () => {
|
|
290
|
-
const canonical = normReq(params);
|
|
291
|
+
const canonical = normReq(params, "remove");
|
|
291
292
|
assertReq(canonical);
|
|
292
293
|
const normalizedParams = canonical;
|
|
293
294
|
const literalEscapes = literalEscapeHints([normalizedParams.text], "text");
|
package/src/utils.ts
CHANGED
|
@@ -8,15 +8,31 @@ export function isRec(value: unknown): value is Record<string, unknown> {
|
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
|
|
11
|
-
export
|
|
11
|
+
export type AnchorPairName = "remove" | "replace";
|
|
12
|
+
|
|
13
|
+
const ANCHOR_ALIASES: Record<AnchorPairName, Record<string, string>> = {
|
|
14
|
+
remove: { replace_from: "remove_from", replace_to: "remove_to" },
|
|
15
|
+
replace: { remove_from: "replace_from", remove_to: "replace_to" },
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
function normalizeAnchorAliases(record: Record<string, unknown>, pair: AnchorPairName): void {
|
|
19
|
+
for (const [alias, canonical] of Object.entries(ANCHOR_ALIASES[pair])) {
|
|
20
|
+
if (!(alias in record)) continue;
|
|
21
|
+
if (record[canonical] === undefined) record[canonical] = record[alias];
|
|
22
|
+
delete record[alias];
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function normalizeRequest(input: unknown, pair?: AnchorPairName): unknown {
|
|
12
27
|
if (!isRec(input)) return input;
|
|
13
28
|
const record: Record<string, unknown> = { ...input };
|
|
14
29
|
normalizeEditLines(record);
|
|
30
|
+
if (pair !== undefined) normalizeAnchorAliases(record, pair);
|
|
15
31
|
return record;
|
|
16
32
|
}
|
|
17
33
|
|
|
18
|
-
export function makePrepareArguments(): (args: unknown) => any {
|
|
19
|
-
return normalizeRequest;
|
|
34
|
+
export function makePrepareArguments(pair?: AnchorPairName): (args: unknown) => any {
|
|
35
|
+
return (args) => normalizeRequest(args, pair);
|
|
20
36
|
}
|
|
21
37
|
|
|
22
38
|
export function splitLines(text: string): string[] {
|