pi-hashline-edit-pro 4.0.1 → 4.2.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/src/read.ts CHANGED
@@ -15,18 +15,16 @@ import { lineHashes, fmtRegion, fmtRow, HASH_SEP, MAX_HASH_LINES } from "./hashl
15
15
  import { toCwd } from "./paths";
16
16
  import { abortIf, makePrepareArguments, numberedRead, visLines, splitLines } from "./utils";
17
17
  import { loadP, loadGuide } from "./prompts";
18
+ import { withReadPrompts, DEFAULT_EDIT_FLAGS, type EditToolFlags } from "./edit-common";
18
19
  import { valAccess } from "./validation";
19
20
  import { markServed as markServedScoped } from "./anchor-registry";
20
21
  import { buildServedMap } from "./served";
21
22
  import { Text } from "@earendil-works/pi-tui";
22
23
  const R_DESC = loadP("../prompts/read.md");
23
-
24
24
  const R_SNIPPET = loadP("../prompts/read-snippet.md");
25
-
26
25
  function readGuide(): string[] {
27
- return loadGuide("../prompts/read-guidelines.md");
26
+ return loadGuide("../prompts/read-guidelines.md");
28
27
  }
29
-
30
28
  function normPosInt(
31
29
  value: number | undefined,
32
30
  name: "offset" | "limit",
@@ -166,13 +164,14 @@ export async function fmtReadPreview(
166
164
  };
167
165
  }
168
166
 
169
- export function regRead(pi: ExtensionAPI): void {
170
- pi.registerTool({
171
- name: "read",
172
- label: "Read",
173
- description: R_DESC,
174
- promptSnippet: R_SNIPPET,
175
- promptGuidelines: readGuide(),
167
+ export function regRead(pi: ExtensionAPI, flags: EditToolFlags = DEFAULT_EDIT_FLAGS): void {
168
+ const prompted = withReadPrompts({ description: R_DESC, snippet: R_SNIPPET, guidelines: readGuide() }, flags);
169
+ pi.registerTool({
170
+ name: "read",
171
+ label: "Read",
172
+ description: prompted.description,
173
+ promptSnippet: prompted.snippet,
174
+ promptGuidelines: prompted.guidelines,
176
175
  prepareArguments: makePrepareArguments(),
177
176
  parameters: Type.Object({
178
177
  path: Type.String({
@@ -128,7 +128,7 @@ export function isApplied(
128
128
 
129
129
  const RESULT_PREVIEW_LINES = 16;
130
130
 
131
- function expandHint(): string {
131
+ export function expandHint(): string {
132
132
  try {
133
133
  return keyHint("app.tools.expand", "to expand");
134
134
  } catch {
@@ -165,7 +165,7 @@ export function buildAppliedText(
165
165
  : "";
166
166
  sections.push(`${diffSection}${hint}`);
167
167
  }
168
- const warnings = extractWarnings(text);
168
+ const warnings = details?.warnings?.length ? `Warnings:\n${details.warnings.join("\n")}` : extractWarnings(text);
169
169
  if (warnings) sections.push(warnings);
170
170
  return sections.length > 0 ? sections.join("\n\n") : undefined;
171
171
  }
@@ -125,7 +125,8 @@ export function buildNoop(input: NoopInput, noopNoun = "Replacement"): TResult {
125
125
  firstChangedLine: undefined,
126
126
  snapshotId,
127
127
  classification: "noop" as const,
128
- metrics,
128
+ metrics,
129
+ ...(warnings?.length ? { warnings: [...warnings] } : {}),
129
130
  },
130
131
  };
131
132
  }
@@ -210,6 +211,7 @@ export function buildChanged(input: SuccessInput, verb = "replaced"): TResult {
210
211
  snapshotId,
211
212
  metrics,
212
213
  diffLineNumbers: diffResult.lineNumbers,
214
+ ...(warnings?.length ? { warnings: [...warnings] } : {}),
213
215
  },
214
216
  };
215
217
  }
package/src/replace.ts CHANGED
@@ -9,7 +9,7 @@ import {
9
9
  type LineEnding,
10
10
  } from "./replace-diff";
11
11
  import { readNormFile, type NormFile } from "./file-reader";
12
- import { editToolSchema, type ReqParams, assertReq, normReq } from "./payload-contract";
12
+ import { editToolSchema, buildEditToolSchema, type ReqParams, assertReq, normReq } from "./payload-contract";
13
13
  import { decodeStringArray } from "./utils";
14
14
  import { loadP, loadGuide } from "./prompts";
15
15
  import { type FileIdentity } from "./fs-write";
@@ -22,7 +22,6 @@ import { applyEdit,
22
22
  type HEdit,
23
23
  type NEdit,
24
24
  } from "./hashline";
25
- import { commitEdit } from "./commit";
26
25
  import { withDedupRows, type RMetrics } from "./replace-response";
27
26
  import {
28
27
  type RPreview,
@@ -33,7 +32,9 @@ import { adoptAnchors, servedForPath } from "./anchor-registry";
33
32
  import { resolveTarget } from "./fs-write";
34
33
  import { toCwd } from "./paths";
35
34
  import { noopPayloadKey, markBoundaryNoop, consumeBoundaryBypass, clearBoundaryBypass } from "./boundary-bypass";
36
- import { queuedEdit, editToolBase, editRenderCallWrapper, editRenderResultWrapper, resolveEditTarget } from "./edit-common";
35
+ import { queuedEdit, editToolBase, editRenderCallWrapper, editRenderResultWrapper, resolveEditTargetWithRequirement, throwIfStrictInput, getBoundaryDedupMode, withReplacePrompts, DEFAULT_EDIT_FLAGS, type EditToolFlags } from "./edit-common";
36
+ import { commitEdit } from "./commit";
37
+ import { batchMemberFor, executeBatchMember, noteBatchFailure } from "./batch";
37
38
 
38
39
  export { editToolSchema, type ReqParams, assertReq };
39
40
 
@@ -46,6 +47,8 @@ export type ReplaceDetails = {
46
47
  classification?: "noop";
47
48
  metrics?: RMetrics;
48
49
  diffLineNumbers?: (number|undefined)[];
50
+ warnings?: string[];
51
+ batch?: { id: number; size: number; last: boolean; total: number };
49
52
  };
50
53
 
51
54
  export interface PipelineResult {
@@ -111,13 +114,7 @@ function countLineChanges(
111
114
  };
112
115
  }
113
116
 
114
- export async function execPipeline(
115
- targetPath: string,
116
- params: ReqParams,
117
- cwd: string,
118
- options?: ExecPipelineOptions,
119
- ): Promise<PipelineResult> {
120
-
117
+ export function buildReplaceHEdit(params: ReqParams): { edit: HEdit; warnings: string[] } {
121
118
  const editWarnings: string[] = [];
122
119
  let replacementLines = params.replacement_lines;
123
120
  const expandedReplacement = decodeStringArray(replacementLines);
@@ -133,7 +130,17 @@ export async function execPipeline(
133
130
  },
134
131
  editWarnings,
135
132
  );
133
+ return { edit, warnings: editWarnings };
134
+ }
136
135
 
136
+ export async function execPipeline(
137
+ targetPath: string,
138
+ params: ReqParams,
139
+ cwd: string,
140
+ options?: ExecPipelineOptions,
141
+ ): Promise<PipelineResult> {
142
+
143
+ const { edit, warnings: editWarnings } = buildReplaceHEdit(params);
137
144
  const hashStore = options?.store ?? await loadHashStore();
138
145
  const preResolvedPath = await resolveTarget(toCwd(targetPath, cwd));
139
146
  const served = servedForPath(preResolvedPath);
@@ -142,6 +149,9 @@ export async function execPipeline(
142
149
  );
143
150
  const displayPath = relative(cwd, absolutePath).replace(/\\/g, "/") || targetPath;
144
151
 
152
+ const dedupMode = await getBoundaryDedupMode();
153
+ const effectiveSkipBoundaryDedup = options?.skipBoundaryDedup === true || dedupMode === "off";
154
+ const strictBoundaryDedup = options?.skipBoundaryDedup !== true && dedupMode === "strict";
145
155
  let anchorResult: ReturnType<typeof applyEdit>;
146
156
  try {
147
157
  anchorResult = applyEdit(
@@ -151,7 +161,8 @@ export async function execPipeline(
151
161
  originalHashes,
152
162
  displayPath,
153
163
  served,
154
- options?.skipBoundaryDedup,
164
+ effectiveSkipBoundaryDedup,
165
+ strictBoundaryDedup,
155
166
  );
156
167
  } catch (error) {
157
168
  await noteAnchorError(absolutePath, error, originalHashes, options?.noPersist);
@@ -168,6 +179,7 @@ export async function execPipeline(
168
179
  hashes: originalHashes,
169
180
  }, hashStore, false, true);
170
181
  const warnings = [...editWarnings, ...(anchorResult.warnings ?? [])];
182
+ await throwIfStrictInput(warnings);
171
183
  const { totalAddedLines, totalRemovedLines } = countLineChanges(
172
184
  edit, originalHashes, isNoop, anchorResult.autoFixes?.length ?? 0,
173
185
  );
@@ -220,7 +232,12 @@ export async function compPreview(
220
232
  try {
221
233
  const normalized = normReq(request);
222
234
  assertReq(normalized);
223
- const targetPath = resolveEditTarget(normalized.remove_from, normalized.remove_to);
235
+ const targetPath = await resolveEditTargetWithRequirement({
236
+ removeFrom: (normalized as ReqParams).remove_from,
237
+ removeTo: (normalized as ReqParams).remove_to,
238
+ providedPath: (normalized as ReqParams).path,
239
+ cwd,
240
+ });
224
241
  const pipe = await execPipeline(
225
242
  targetPath,
226
243
  normalized,
@@ -240,18 +257,20 @@ type ToolDef = ToolDefinition<
240
257
  RRState
241
258
  > & { renderShell?: "default" | "self" };
242
259
 
243
- export function buildToolDef(): ToolDef {
244
- const E_DESC = loadP("../prompts/replace.md");
245
- const E_SNIPPET = loadP("../prompts/replace-snippet.md");
246
- const E_GUIDE = loadGuide("../prompts/replace-guidelines.md");
247
- const parameters = editToolSchema;
260
+ export function buildToolDef(flags: EditToolFlags = DEFAULT_EDIT_FLAGS): ToolDef {
261
+ const prompted = withReplacePrompts({
262
+ description: loadP("../prompts/replace.md"),
263
+ snippet: loadP("../prompts/replace-snippet.md"),
264
+ guidelines: loadGuide("../prompts/replace-guidelines.md"),
265
+ }, flags);
266
+ const parameters = buildEditToolSchema(flags.requirePath);
248
267
  return {
249
268
  name: "replace",
250
269
  label: "Replace",
251
- description: E_DESC,
270
+ description: prompted.description,
252
271
  parameters,
253
- promptSnippet: E_SNIPPET,
254
- promptGuidelines: E_GUIDE,
272
+ promptSnippet: prompted.snippet,
273
+ promptGuidelines: prompted.guidelines,
255
274
  ...editToolBase,
256
275
  renderCall: editRenderCallWrapper(compPreview),
257
276
  renderResult: editRenderResultWrapper,
@@ -259,34 +278,72 @@ export function buildToolDef(): ToolDef {
259
278
  const canonical = normReq(params);
260
279
  assertReq(canonical);
261
280
  const normalizedParams = canonical;
262
- const targetPath = resolveEditTarget(normalizedParams.remove_from, normalizedParams.remove_to);
281
+ const targetPath = await resolveEditTargetWithRequirement({
282
+ removeFrom: normalizedParams.remove_from,
283
+ removeTo: normalizedParams.remove_to,
284
+ providedPath: normalizedParams.path,
285
+ cwd: ctx.cwd,
286
+ }).catch((error: unknown) => {
287
+ const member = batchMemberFor(_toolCallId);
288
+ if (member) noteBatchFailure(member, error);
289
+ throw error;
290
+ });
263
291
  return queuedEdit(targetPath, ctx.cwd, signal, async (absolutePath, mutationTargetPath) => {
292
+ const dedupMode = await getBoundaryDedupMode();
293
+ const dedupOn = dedupMode !== "off";
264
294
  const noopPayload = noopPayloadKey(mutationTargetPath, normalizedParams.remove_from, normalizedParams.remove_to, normalizedParams.replacement_lines);
265
- const boundaryBypass = consumeBoundaryBypass(mutationTargetPath, noopPayload);
266
- const pipe = await execPipeline(
267
- targetPath,
268
- normalizedParams,
269
- ctx.cwd,
270
- { accessMode: constants.R_OK | constants.W_OK, signal, skipBoundaryDedup: boundaryBypass },
271
- );
295
+ const boundaryBypass = dedupOn ? consumeBoundaryBypass(mutationTargetPath, noopPayload) : false;
296
+ const strictBoundaryDedup = dedupMode === "strict" && !boundaryBypass;
297
+ const member = batchMemberFor(_toolCallId);
298
+ if (!member) {
299
+ const pipe = await execPipeline(
300
+ targetPath,
301
+ normalizedParams,
302
+ ctx.cwd,
303
+ { accessMode: constants.R_OK | constants.W_OK, signal, skipBoundaryDedup: boundaryBypass },
304
+ );
305
+ const appliedWarnings = boundaryBypass
306
+ ? ["[W_BOUNDARY_BYPASS] Boundary dedup was off for this call and is back on."]
307
+ : [];
308
+ return commitEdit(pipe, {
309
+ path: pipe.path,
310
+ absolutePath,
311
+ mutationTargetPath,
312
+ editAnchors: [normalizedParams.remove_from, normalizedParams.remove_to],
313
+ signal,
314
+ appliedWarnings,
315
+ onApplied: () => { if (dedupOn) clearBoundaryBypass(mutationTargetPath); },
316
+ onNoopDedup: dedupOn ? () => markBoundaryNoop(mutationTargetPath, noopPayload) : undefined,
317
+ });
318
+ }
319
+ let built: { edit: HEdit; warnings: string[] };
320
+ try {
321
+ built = buildReplaceHEdit(normalizedParams);
322
+ } catch (error) {
323
+ noteBatchFailure(member, error);
324
+ throw error;
325
+ }
272
326
  const appliedWarnings = boundaryBypass
273
327
  ? ["[W_BOUNDARY_BYPASS] Boundary dedup was off for this call and is back on."]
274
328
  : [];
275
- return commitEdit(pipe, {
276
- path: pipe.path,
277
- absolutePath,
329
+ return executeBatchMember({
330
+ kind: "replace",
331
+ member,
332
+ targetPath,
278
333
  mutationTargetPath,
279
- editAnchors: [normalizedParams.remove_from, normalizedParams.remove_to],
334
+ cwd: ctx.cwd,
280
335
  signal,
281
- appliedWarnings,
282
- onApplied: () => clearBoundaryBypass(mutationTargetPath),
283
- onNoopDedup: () => markBoundaryNoop(mutationTargetPath, noopPayload),
336
+ hedit: built.edit,
337
+ extraWarnings: [...built.warnings, ...appliedWarnings],
338
+ skipBoundaryDedup: boundaryBypass,
339
+ strictBoundaryDedup,
340
+ noopPayload,
284
341
  });
285
342
  });
286
343
  },
287
344
  };
288
345
  }
289
346
 
290
- export function regReplace(pi: ExtensionAPI): void {
291
- pi.registerTool(buildToolDef());
347
+ export function regReplace(pi: ExtensionAPI, flags: EditToolFlags = DEFAULT_EDIT_FLAGS): void {
348
+ pi.registerTool(buildToolDef(flags));
292
349
  }