@trim21/personal-pi-extensions 0.0.385 → 0.0.388

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trim21/personal-pi-extensions",
3
- "version": "0.0.385",
3
+ "version": "0.0.388",
4
4
  "type": "module",
5
5
  "description": "Custom pi coding-agent extensions: bwrap sandbox, workspace guard, opencode edit, and more",
6
6
  "keywords": [
@@ -114,3 +114,12 @@ export function preserveQuoteStyle(
114
114
  if (hasSingleQuotes) result = applyCurlySingleQuotes(result);
115
115
  return result;
116
116
  }
117
+
118
+ /**
119
+ * patch 显示用:把行首 tab 转成 2 空格(对齐 Claude Code 的
120
+ * convertLeadingTabsToSpaces)。仅用于 details 里展示的 diff,不影响写盘内容。
121
+ */
122
+ export function convertLeadingTabsToSpaces(content: string): string {
123
+ if (!content.includes("\t")) return content;
124
+ return content.replaceAll(/^\t+/gm, (tabs) => " ".repeat(tabs.length));
125
+ }
@@ -26,7 +26,7 @@ import {
26
26
  snapshotsEqual,
27
27
  throwIfAborted,
28
28
  } from "./common.js";
29
- import { findActualString, preserveQuoteStyle } from "./edit-utils.js";
29
+ import { convertLeadingTabsToSpaces, findActualString, preserveQuoteStyle } from "./edit-utils.js";
30
30
 
31
31
  const SAMPLE_BYTES = 4096;
32
32
 
@@ -388,7 +388,7 @@ export function registerFileTools(
388
388
  const snapshot = snapshotOf(newString);
389
389
  const key = await readStateKey(filePath);
390
390
  state.reads.set(key, snapshot);
391
- const diff = generateDiffString("", newString);
391
+ const diff = generateDiffString("", convertLeadingTabsToSpaces(newString));
392
392
  throwIfAborted(signal);
393
393
  const diagnosticText = await service.lspDiagnosticsForFile(filePath, ctx.cwd, {
394
394
  notify: (message, level) => ctx.ui.notify(message, level),
@@ -398,7 +398,7 @@ export function registerFileTools(
398
398
  `The file ${filePath} has been updated successfully.`,
399
399
  {
400
400
  diff: diff.diff,
401
- patch: generateUnifiedPatch(filePath, "", newString),
401
+ patch: generateUnifiedPatch(filePath, "", convertLeadingTabsToSpaces(newString)),
402
402
  firstChangedLine: diff.firstChangedLine,
403
403
  reads: { [key]: snapshot },
404
404
  },
@@ -459,15 +459,30 @@ export function registerFileTools(
459
459
  );
460
460
  }
461
461
  const actualNewString = preserveQuoteStyle(oldString, actualOldString, newString);
462
+ // 删除场景(new_string 为空):old_string 不以换行结尾且文件里是
463
+ // "old_string\n" 时连换行一起删,避免留下空行(对齐 Claude Code
464
+ // applyEditToFile 的 stripTrailingNewline 语义)
465
+ let searchString = actualOldString;
466
+ if (
467
+ actualNewString === "" &&
468
+ !actualOldString.endsWith("\n") &&
469
+ normalized.includes(actualOldString + "\n")
470
+ ) {
471
+ searchString = actualOldString + "\n";
472
+ }
462
473
  // split/join 与函数替换:replacement 含 $ 时不会触发 $& 等特殊语义
463
474
  const updated = replaceAll
464
- ? normalized.split(actualOldString).join(actualNewString)
465
- : normalized.replace(actualOldString, () => actualNewString);
475
+ ? normalized.split(searchString).join(actualNewString)
476
+ : normalized.replace(searchString, () => actualNewString);
466
477
  const restored = lineEnding === "\r\n" ? updated.replaceAll("\n", "\r\n") : updated;
467
478
  await writeFile(filePath, restored, "utf8");
468
479
  const snapshot = snapshotOf(restored);
469
480
  state.reads.set(key, snapshot);
470
- const diff = generateDiffString(original, restored);
481
+ // patch/diff 仅供显示:前导 tab 转空格,避免 UI 渲染错位(对齐 Claude Code)
482
+ const diff = generateDiffString(
483
+ convertLeadingTabsToSpaces(original),
484
+ convertLeadingTabsToSpaces(restored),
485
+ );
471
486
  const text = replaceAll
472
487
  ? `The file ${filePath} has been updated. All occurrences were successfully replaced.`
473
488
  : `The file ${filePath} has been updated successfully.`;
@@ -479,7 +494,11 @@ export function registerFileTools(
479
494
  text,
480
495
  {
481
496
  diff: diff.diff,
482
- patch: generateUnifiedPatch(filePath, original, restored),
497
+ patch: generateUnifiedPatch(
498
+ filePath,
499
+ convertLeadingTabsToSpaces(original),
500
+ convertLeadingTabsToSpaces(restored),
501
+ ),
483
502
  firstChangedLine: diff.firstChangedLine,
484
503
  reads: { [key]: snapshot },
485
504
  },
@@ -88,9 +88,13 @@ export async function globFiles(
88
88
  stdout = result.stdout;
89
89
  } catch (error) {
90
90
  if (signal?.aborted) throwIfAborted(signal);
91
- throw new Error(`ripgrep failed: ${error instanceof Error ? error.message : String(error)}`, {
92
- cause: error,
93
- });
91
+ // rg exit code 1 = 搜索完成但无匹配,对齐 Claude Code ripGrep(正常空结果)
92
+ const code = (error as { code?: unknown }).code;
93
+ if (code === 1) return { files: [], truncated: false };
94
+ const detail =
95
+ (error as { stderr?: string }).stderr?.trim() ||
96
+ (error instanceof Error ? error.message : String(error));
97
+ throw new Error(`ripgrep failed: ${detail}`, { cause: error });
94
98
  }
95
99
  // rg 输出相对 searchDir 的路径,转成绝对路径
96
100
  const lines = stdout ? stdout.replace(/\n$/, "").split("\n") : [];
@@ -149,7 +153,7 @@ export function registerGlobTool(pi: ExtensionAPI): void {
149
153
  : filenames;
150
154
  return {
151
155
  content: [{ type: "text", text: lines.length > 0 ? lines.join("\n") : "No files found" }],
152
- details: { count: filenames.length },
156
+ details: undefined,
153
157
  };
154
158
  },
155
159
  });
@@ -284,7 +284,7 @@ export function registerGrepTool(pi: ExtensionAPI): void {
284
284
 
285
285
  if (mode === "files_with_matches") {
286
286
  if (stdout === "") {
287
- return { content: [{ type: "text", text: "No files found" }], details: { matches: 0 } };
287
+ return { content: [{ type: "text", text: "No files found" }], details: undefined };
288
288
  }
289
289
  const sorted = await sortFilesByMtime(stdout);
290
290
  const { lines, appliedLimit, appliedOffset } = pageGrepOutput(sorted, offset, headLimit);
@@ -293,7 +293,10 @@ export function registerGrepTool(pi: ExtensionAPI): void {
293
293
  const text = truncateOutput(
294
294
  `Found ${filenames.length} ${filenames.length === 1 ? "file" : "files"}${limitInfo ? ` ${limitInfo}` : ""}\n${filenames.join("\n")}`,
295
295
  );
296
- return { content: [{ type: "text", text }], details: { matches: filenames.length } };
296
+ return {
297
+ content: [{ type: "text", text }],
298
+ details: undefined,
299
+ };
297
300
  }
298
301
 
299
302
  if (mode === "count") {