pi-hashline-edit-pro 6.1.0 → 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 +10 -4
- package/index.ts +30 -10
- 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 +3 -2
- package/src/config.ts +24 -1
- 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, diff context lines, `anchor_grep` tool, copy/move tools, replace_match tool, required `path`, and strict input. Persists across sessions. |
|
|
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`:
|
|
@@ -362,7 +366,8 @@ Settings live in `~/.config/pi-hashline-edit-pro/config.json`, created when a se
|
|
|
362
366
|
"requirePath": false,
|
|
363
367
|
"strictInput": false,
|
|
364
368
|
"diffContextLines": 1,
|
|
365
|
-
"disableOnModels": []
|
|
369
|
+
"disableOnModels": [],
|
|
370
|
+
"readOnDisabledModels": "vanilla"
|
|
366
371
|
}
|
|
367
372
|
```
|
|
368
373
|
|
|
@@ -377,9 +382,10 @@ Settings live in `~/.config/pi-hashline-edit-pro/config.json`, created when a se
|
|
|
377
382
|
| `requirePath` | Require path | `false` | `replace`, `replace_match`, `insert`, `copy`, and `move` require a `path` argument that must match anchor ownership. |
|
|
378
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. |
|
|
379
384
|
| `diffContextLines` | Diff context | `1` | Surrounding lines in post-edit diffs, 0-10 (needs Auto-read). |
|
|
380
|
-
| `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
|
|
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. |
|
|
381
387
|
|
|
382
|
-
`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 `
|
|
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.
|
|
383
389
|
|
|
384
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).
|
|
385
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,6 +25,7 @@ import {
|
|
|
25
25
|
toggleAnchorGrep,
|
|
26
26
|
toggleCopyMove,
|
|
27
27
|
toggleReplaceMatch,
|
|
28
|
+
cycleReadOnDisabledModels,
|
|
28
29
|
toggleRequirePath,
|
|
29
30
|
toggleStrictInput,
|
|
30
31
|
adjustDiffContextLines,
|
|
@@ -61,6 +62,9 @@ export default function (pi: ExtensionAPI): void {
|
|
|
61
62
|
let autoReadAll: AutoReadAllMode = "off";
|
|
62
63
|
let autoReadAllIgnore: string[] = [];
|
|
63
64
|
let disableOnModels: string[] = [];
|
|
65
|
+
let readOnDisabledModels: ReadOnDisabledModels = "vanilla";
|
|
66
|
+
let readFlavor: "anchored" | "vanilla" = "anchored";
|
|
67
|
+
let readFlavorCwd: string | undefined;
|
|
64
68
|
let autoReadAllInjected = false;
|
|
65
69
|
let grepWasActive = false;
|
|
66
70
|
const baseAnchorTools = new Set<string>();
|
|
@@ -69,7 +73,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
69
73
|
async function refreshEditTools(): Promise<void> {
|
|
70
74
|
try {
|
|
71
75
|
const flags = await currentEditFlags(pi.getActiveTools().includes("codemode"));
|
|
72
|
-
regRead(pi, flags);
|
|
76
|
+
if (readFlavor !== "vanilla") regRead(pi, flags);
|
|
73
77
|
regReplace(pi, flags);
|
|
74
78
|
regReplaceMatch(pi, flags);
|
|
75
79
|
regInsert(pi, flags);
|
|
@@ -82,10 +86,24 @@ export default function (pi: ExtensionAPI): void {
|
|
|
82
86
|
}
|
|
83
87
|
}
|
|
84
88
|
|
|
85
|
-
async function syncModelGate(model: ModelLike | undefined): Promise<void> {
|
|
86
|
-
|
|
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) {
|
|
87
103
|
const active = pi.getActiveTools();
|
|
88
|
-
const
|
|
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");
|
|
89
107
|
if (grepWasActive && !next.includes("grep")) next.push("grep");
|
|
90
108
|
pi.setActiveTools(next);
|
|
91
109
|
gateApplied = true;
|
|
@@ -133,6 +151,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
133
151
|
autoReadAll = config.autoReadAll ?? "off";
|
|
134
152
|
autoReadAllIgnore = config.autoReadAllIgnore ?? [];
|
|
135
153
|
disableOnModels = config.disableOnModels ?? [];
|
|
154
|
+
readOnDisabledModels = config.readOnDisabledModels ?? "vanilla";
|
|
136
155
|
const sessionBranch = (ctx as { sessionManager?: { getBranch?: () => Array<{ type?: string; customType?: string }> } }).sessionManager?.getBranch?.() ?? [];
|
|
137
156
|
autoReadAllInjected = sessionBranch.some((entry) => entry.type === "custom_message" && entry.customType === AUTO_READ_ALL_CUSTOM_TYPE);
|
|
138
157
|
await refreshEditTools();
|
|
@@ -144,7 +163,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
144
163
|
return true;
|
|
145
164
|
}),
|
|
146
165
|
);
|
|
147
|
-
await syncModelGate(ctx.model);
|
|
166
|
+
await syncModelGate(ctx.model, ctx.cwd);
|
|
148
167
|
const debugValue = process.env.PI_HASHLINE_DEBUG;
|
|
149
168
|
if (debugValue === "1" || debugValue === "true") {
|
|
150
169
|
ctx.ui.notify(`Hashline Edit mode active`, "info");
|
|
@@ -161,11 +180,11 @@ export default function (pi: ExtensionAPI): void {
|
|
|
161
180
|
}
|
|
162
181
|
});
|
|
163
182
|
pi.on("model_select", async (event, ctx) => withAnchorSession(ctx, async () => {
|
|
164
|
-
await syncModelGate(event.model);
|
|
183
|
+
await syncModelGate(event.model, ctx.cwd);
|
|
165
184
|
}));
|
|
166
185
|
|
|
167
186
|
pi.on("before_agent_start", async (_event, ctx) => withAnchorSession(ctx, async () => {
|
|
168
|
-
await syncModelGate(ctx.model);
|
|
187
|
+
await syncModelGate(ctx.model, ctx.cwd);
|
|
169
188
|
if (autoReadAll === "off" || autoReadAllInjected) return;
|
|
170
189
|
if (modelDisabled(ctx.model, disableOnModels)) return;
|
|
171
190
|
autoReadAllInjected = true;
|
|
@@ -197,6 +216,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
197
216
|
else if (key === "autoReadAll") { autoReadAll = await cycleAutoReadAllMode(); autoReadAllInjected = false; }
|
|
198
217
|
else if (key === "autoReadAllIgnore") autoReadAllIgnore = await setAutoReadAllIgnoreFromText(value ?? "");
|
|
199
218
|
else if (key === "disableOnModels") disableOnModels = await setDisableOnModelsFromText(value ?? "");
|
|
219
|
+
else if (key === "readOnDisabledModels") readOnDisabledModels = await cycleReadOnDisabledModels();
|
|
200
220
|
else if (key === "diffContextLines") await adjustDiffContextLines(delta ?? 1);
|
|
201
221
|
else if (key === "anchorGrepEnabled") {
|
|
202
222
|
const enabled = await toggleAnchorGrep();
|
|
@@ -224,7 +244,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
224
244
|
else if (key === "requirePath") await toggleRequirePath();
|
|
225
245
|
else if (key === "strictInput") await toggleStrictInput();
|
|
226
246
|
await refreshEditTools();
|
|
227
|
-
await syncModelGate(ctx.model);
|
|
247
|
+
await syncModelGate(ctx.model, ctx.cwd);
|
|
228
248
|
},
|
|
229
249
|
});
|
|
230
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;
|
|
@@ -27,7 +27,8 @@ export function configRows(config: Config): ConfigRow[] {
|
|
|
27
27
|
{ key: "requirePath", label: "Require path", hint: "replace, insert, copy, move need path (RPC visibility)", enabled: config.requirePath === true },
|
|
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
|
-
{ key: "disableOnModels", label: "Disable on models", hint: "Model globs (provider/id, model id, or api) that turn off
|
|
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: "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"] },
|
|
31
32
|
];
|
|
32
33
|
}
|
|
33
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,6 +24,7 @@ export interface Config {
|
|
|
21
24
|
strictInput?: boolean;
|
|
22
25
|
diffContextLines?: number;
|
|
23
26
|
disableOnModels?: string[];
|
|
27
|
+
readOnDisabledModels?: ReadOnDisabledModels;
|
|
24
28
|
}
|
|
25
29
|
|
|
26
30
|
const DEFAULT_CONFIG: Config = {
|
|
@@ -33,7 +37,8 @@ const DEFAULT_CONFIG: Config = {
|
|
|
33
37
|
requirePath: false,
|
|
34
38
|
strictInput: false,
|
|
35
39
|
diffContextLines: DEFAULT_DIFF_CONTEXT_LINES,
|
|
36
|
-
disableOnModels: []
|
|
40
|
+
disableOnModels: [],
|
|
41
|
+
readOnDisabledModels: "vanilla",
|
|
37
42
|
};
|
|
38
43
|
|
|
39
44
|
function parseAutoReadAllMode(value: unknown): AutoReadAllMode {
|
|
@@ -43,6 +48,12 @@ function parseAutoReadAllMode(value: unknown): AutoReadAllMode {
|
|
|
43
48
|
return DEFAULT_CONFIG.autoReadAll ?? "off";
|
|
44
49
|
}
|
|
45
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
|
+
|
|
46
57
|
export function normalizeAutoReadAllIgnoreEntry(entry: string): string {
|
|
47
58
|
return entry.trim().replace(/\\/g, "/").replace(/^\/+|\/+$/g, "").replace(/\/{2,}/g, "/");
|
|
48
59
|
}
|
|
@@ -94,6 +105,8 @@ function parseConfig(content: string): Config {
|
|
|
94
105
|
const diffContextLines = parsed.diffContextLines;
|
|
95
106
|
const autoReadAllIgnore = parsed.autoReadAllIgnore;
|
|
96
107
|
const disableOnModels = parsed.disableOnModels;
|
|
108
|
+
const readOnDisabledModels = parsed.readOnDisabledModels;
|
|
109
|
+
const legacyDisableReadOnModels = parsed.disableReadOnModels;
|
|
97
110
|
return {
|
|
98
111
|
autoRead: typeof autoRead === "boolean" ? autoRead : DEFAULT_CONFIG.autoRead,
|
|
99
112
|
anchorGrepEnabled: typeof anchorGrepEnabled === "boolean" ? anchorGrepEnabled : DEFAULT_CONFIG.anchorGrepEnabled,
|
|
@@ -105,6 +118,7 @@ function parseConfig(content: string): Config {
|
|
|
105
118
|
diffContextLines: normalizeDiffContextLines(diffContextLines),
|
|
106
119
|
autoReadAllIgnore: parseAutoReadAllIgnore(autoReadAllIgnore),
|
|
107
120
|
disableOnModels: parseDisableOnModels(disableOnModels),
|
|
121
|
+
readOnDisabledModels: parseReadOnDisabledModels(readOnDisabledModels, legacyDisableReadOnModels),
|
|
108
122
|
};
|
|
109
123
|
}
|
|
110
124
|
|
|
@@ -222,6 +236,15 @@ export const toggleAutoRead = (): Promise<boolean> => toggleFlag("autoRead");
|
|
|
222
236
|
export const toggleAnchorGrep = (): Promise<boolean> => toggleFlag("anchorGrepEnabled");
|
|
223
237
|
export const toggleCopyMove = (): Promise<boolean> => toggleFlag("copyMoveEnabled");
|
|
224
238
|
export const toggleReplaceMatch = (): Promise<boolean> => toggleFlag("replaceMatchEnabled");
|
|
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
|
+
}
|
|
225
248
|
export async function cycleAutoReadAllMode(): Promise<AutoReadAllMode> {
|
|
226
249
|
let next: AutoReadAllMode = "off";
|
|
227
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[] {
|