@trim21/personal-pi-extensions 0.0.406 → 0.0.407

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.406",
3
+ "version": "0.0.407",
4
4
  "type": "module",
5
5
  "description": "Custom pi coding-agent extensions: bwrap sandbox, workspace guard, opencode edit, and more",
6
6
  "keywords": [
package/src/aft/tools.ts CHANGED
@@ -8,7 +8,7 @@
8
8
  import { readFileSync } from "node:fs";
9
9
  import { stat } from "node:fs/promises";
10
10
  import { homedir } from "node:os";
11
- import { basename, isAbsolute, join, resolve } from "node:path";
11
+ import { isAbsolute, join, resolve } from "node:path";
12
12
  import { fileURLToPath } from "node:url";
13
13
 
14
14
  import {
@@ -23,7 +23,7 @@ import {
23
23
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
24
24
  import { Type } from "typebox";
25
25
 
26
- import { formatDisplayPath } from "../lib/path.js";
26
+ import { formatDisplayPath, formatSubtitlePath } from "../lib/path.js";
27
27
  import { type ToolPendant } from "../lib/pendant.js";
28
28
  import { callAftTool, SEMANTIC_INDEX_WAIT_TIMEOUT_MS } from "./bridge.js";
29
29
 
@@ -51,16 +51,6 @@ export function resolvePathArg(cwd: string, input: string): string {
51
51
  return isAbsolute(input) ? input : resolve(cwd, input);
52
52
  }
53
53
 
54
- /** subtitle 中路径的最大显示长度,超过时退化为仅文件名。 */
55
- export const MAX_SUBTITLE_PATH_LENGTH = 20;
56
-
57
- /** subtitle 用的显示路径:优先 `./…` / `~/…` / 绝对路径,结果过长时只显示文件名。 */
58
- export function formatSubtitlePath(cwd: string, filePath: string): string {
59
- const display = formatDisplayPath(cwd, filePath);
60
- if (display.length <= MAX_SUBTITLE_PATH_LENGTH) return display;
61
- return basename(filePath);
62
- }
63
-
64
54
  export interface AftToolContext {
65
55
  cwd: string;
66
56
  pool: AftTransportPool;
@@ -15,6 +15,7 @@ import {
15
15
  import { Type } from "typebox";
16
16
 
17
17
  import { type LspService, registerLsp } from "../lib/lsp/lsp.js";
18
+ import { formatSubtitlePath } from "../lib/path.js";
18
19
  import { guardWriteAccess } from "../lib/write-guard.js";
19
20
  import {
20
21
  type ClaudeCodeState,
@@ -295,7 +296,13 @@ export function registerFileTools(
295
296
  const snapshot = snapshotOf(image, false);
296
297
  const key = await readStateKey(filePath);
297
298
  state.reads.set(key, snapshot);
298
- return { content, details: { reads: { [key]: snapshot } } };
299
+ return {
300
+ content,
301
+ details: {
302
+ reads: { [key]: snapshot },
303
+ pendant: { subtitle: formatSubtitlePath(ctx.cwd, filePath) },
304
+ },
305
+ };
299
306
  }
300
307
 
301
308
  const offset = params.offset ?? 1;
@@ -313,7 +320,10 @@ export function registerFileTools(
313
320
  previous.limit === limit &&
314
321
  snapshotsEqual(previous, snapshotOf(buffer))
315
322
  ) {
316
- return { content: [{ type: "text", text: FILE_UNCHANGED_STUB }], details: {} };
323
+ return {
324
+ content: [{ type: "text", text: FILE_UNCHANGED_STUB }],
325
+ details: { pendant: { subtitle: formatSubtitlePath(ctx.cwd, filePath) } },
326
+ };
317
327
  }
318
328
  if (isBinary(buffer.subarray(0, SAMPLE_BYTES)))
319
329
  throw new Error(`Cannot read binary file: ${filePath}`);
@@ -340,7 +350,10 @@ export function registerFileTools(
340
350
  });
341
351
  return {
342
352
  content: [{ type: "text", text: formatted.text }],
343
- details: { reads: { [key]: snapshot } },
353
+ details: {
354
+ reads: { [key]: snapshot },
355
+ pendant: { subtitle: formatSubtitlePath(ctx.cwd, filePath) },
356
+ },
344
357
  };
345
358
  },
346
359
  });
@@ -381,8 +394,8 @@ export function registerFileTools(
381
394
  replaceAll: params.replace_all,
382
395
  },
383
396
  });
384
- const [message, details, diagnosticText] = await withFileMutationQueue<
385
- [string, FileToolDetails, string]
397
+ const [message, details, diagnosticText, errorCount] = await withFileMutationQueue<
398
+ [string, FileToolDetails, string, number]
386
399
  >(filePath, async () => {
387
400
  const oldString = params.old_string;
388
401
  const newString = params.new_string;
@@ -414,10 +427,14 @@ export function registerFileTools(
414
427
  state.reads.set(key, snapshot);
415
428
  const diff = generateDiffString("", convertLeadingTabsToSpaces(newString));
416
429
  throwIfAborted(signal);
417
- const diagnosticText = await service.lspDiagnosticsForFile(filePath, ctx.cwd, {
418
- notify: (message, level) => ctx.ui.notify(message, level),
419
- signal,
420
- });
430
+ const { text: diagnosticText, errorCount } = await service.lspDiagnosticsForFile(
431
+ filePath,
432
+ ctx.cwd,
433
+ {
434
+ notify: (message, level) => ctx.ui.notify(message, level),
435
+ signal,
436
+ },
437
+ );
421
438
  return [
422
439
  `The file ${filePath} has been updated successfully.`,
423
440
  {
@@ -427,6 +444,7 @@ export function registerFileTools(
427
444
  reads: { [key]: snapshot },
428
445
  },
429
446
  diagnosticText,
447
+ errorCount,
430
448
  ];
431
449
  }
432
450
  const replaceAll = params.replace_all ?? false;
@@ -511,9 +529,13 @@ export function registerFileTools(
511
529
  ? `The file ${filePath} has been updated. All occurrences were successfully replaced.`
512
530
  : `The file ${filePath} has been updated successfully.`;
513
531
  throwIfAborted(signal);
514
- const diagnosticText = await service.lspDiagnosticsForFile(filePath, ctx.cwd, {
515
- notify: (message, level) => ctx.ui.notify(message, level),
516
- });
532
+ const { text: diagnosticText, errorCount } = await service.lspDiagnosticsForFile(
533
+ filePath,
534
+ ctx.cwd,
535
+ {
536
+ notify: (message, level) => ctx.ui.notify(message, level),
537
+ },
538
+ );
517
539
  return [
518
540
  text,
519
541
  {
@@ -527,13 +549,20 @@ export function registerFileTools(
527
549
  reads: { [key]: snapshot },
528
550
  },
529
551
  diagnosticText,
552
+ errorCount,
530
553
  ];
531
554
  });
532
555
 
533
556
  const text = diagnosticText
534
557
  ? `${message}\n\nLSP errors detected in this file, please fix:\n${diagnosticText}`
535
558
  : message;
536
- return { content: [{ type: "text" as const, text }], details };
559
+ return {
560
+ content: [{ type: "text" as const, text }],
561
+ details: {
562
+ ...details,
563
+ pendant: { subtitle: formatSubtitlePath(ctx.cwd, filePath, errorCount) },
564
+ },
565
+ };
537
566
  },
538
567
  });
539
568
 
@@ -564,8 +593,8 @@ export function registerFileTools(
564
593
  absolutePath: filePath,
565
594
  change: { oldText: "", newText: params.content },
566
595
  });
567
- const [message, details, diagnosticText] = await withFileMutationQueue<
568
- [string, FileToolDetails, string]
596
+ const [message, details, diagnosticText, errorCount] = await withFileMutationQueue<
597
+ [string, FileToolDetails, string, number]
569
598
  >(filePath, async () => {
570
599
  let original: string | undefined;
571
600
  let key: string | undefined;
@@ -595,9 +624,13 @@ export function registerFileTools(
595
624
  ? `File created successfully at: ${filePath}`
596
625
  : `The file ${filePath} has been updated successfully.`;
597
626
  throwIfAborted(signal);
598
- const diagnosticText = await service.lspDiagnosticsForFile(filePath, ctx.cwd, {
599
- notify: (message, level) => ctx.ui.notify(message, level),
600
- });
627
+ const { text: diagnosticText, errorCount } = await service.lspDiagnosticsForFile(
628
+ filePath,
629
+ ctx.cwd,
630
+ {
631
+ notify: (message, level) => ctx.ui.notify(message, level),
632
+ },
633
+ );
601
634
  return [
602
635
  text,
603
636
  {
@@ -607,13 +640,20 @@ export function registerFileTools(
607
640
  reads: { [resolvedKey]: snapshot },
608
641
  },
609
642
  diagnosticText,
643
+ errorCount,
610
644
  ];
611
645
  });
612
646
 
613
647
  const text = diagnosticText
614
648
  ? `${message}\n\nLSP errors detected in this file, please fix:\n${diagnosticText}`
615
649
  : message;
616
- return { content: [{ type: "text" as const, text }], details };
650
+ return {
651
+ content: [{ type: "text" as const, text }],
652
+ details: {
653
+ ...details,
654
+ pendant: { subtitle: formatSubtitlePath(ctx.cwd, filePath, errorCount) },
655
+ },
656
+ };
617
657
  },
618
658
  });
619
659
  }
@@ -208,7 +208,11 @@ export interface LspService {
208
208
  options?: LspRequestOptions,
209
209
  ): Promise<void>;
210
210
  diagnostics(): Promise<Record<string, Diagnostic[]>>;
211
- lspDiagnosticsForFile(file: string, cwd: string, options?: LspRequestOptions): Promise<string>;
211
+ lspDiagnosticsForFile(
212
+ file: string,
213
+ cwd: string,
214
+ options?: LspRequestOptions,
215
+ ): Promise<{ text: string; errorCount: number }>;
212
216
  shutdownAll(): Promise<void>;
213
217
  /** 注入 status 渲染回调;传入 undefined 表示不再渲染。 */
214
218
  attachStatus(render: StatusRenderer | undefined): void;
@@ -404,19 +408,21 @@ export function createLspService(
404
408
  }
405
409
 
406
410
  /**
407
- * edit/write 用:等待文档诊断并返回该文件的 ERROR 报告(空串表示无错误)。
408
- * 内部所有 LSP 失败都会被吞掉,不干扰写操作本身。
411
+ * edit/write 用:等待文档诊断并返回该文件的 ERROR 报告(text 空串表示无错误)
412
+ * 与 ERROR 数量。内部所有 LSP 失败都会被吞掉,不干扰写操作本身。
409
413
  */
410
414
  async function lspDiagnosticsForFile(
411
415
  file: string,
412
416
  cwd: string,
413
417
  options?: LspRequestOptions,
414
- ): Promise<string> {
415
- if (options?.signal?.aborted) return "";
418
+ ): Promise<{ text: string; errorCount: number }> {
419
+ if (options?.signal?.aborted) return { text: "", errorCount: 0 };
416
420
  await touchFile(file, cwd, "document", options);
417
421
  const all = await diagnostics();
418
422
  const normalized = normalize(file);
419
- return report(normalized, all[normalized] ?? []);
423
+ const issues = all[normalized] ?? [];
424
+ const errorCount = issues.filter((item) => item.severity === 1).length;
425
+ return { text: report(normalized, issues), errorCount };
420
426
  }
421
427
 
422
428
  /** 终止全部服务器进程(session_shutdown 时调用)。 */
package/src/lib/path.ts CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  import { stat } from "node:fs/promises";
6
6
  import { homedir } from "node:os";
7
- import { isAbsolute, join, relative, resolve } from "node:path";
7
+ import { basename, isAbsolute, join, relative, resolve } from "node:path";
8
8
 
9
9
  /**
10
10
  * Expand a leading `~` to the user's home directory.
@@ -59,3 +59,14 @@ export function formatDisplayPath(cwd: string, filePath: string): string {
59
59
  }
60
60
  return filePath;
61
61
  }
62
+
63
+ /** subtitle 中路径的最大显示长度,超过时退化为仅文件名。 */
64
+ export const MAX_SUBTITLE_PATH_LENGTH = 20;
65
+
66
+ /** subtitle 用的显示路径:优先 `./…` / `~/…` / 绝对路径,结果过长时只显示文件名。 */
67
+ export function formatSubtitlePath(cwd: string, filePath: string, errorCount?: number): string {
68
+ const display = formatDisplayPath(cwd, filePath);
69
+ const base = display.length <= MAX_SUBTITLE_PATH_LENGTH ? display : basename(filePath);
70
+ if (errorCount === undefined || errorCount === 0) return base;
71
+ return `${base} (ⓧ ${errorCount})`;
72
+ }
@@ -30,6 +30,7 @@ import {
30
30
  import { Type } from "typebox";
31
31
 
32
32
  import { type LspService, registerLsp } from "../lib/lsp/lsp.js";
33
+ import { formatSubtitlePath } from "../lib/path.js";
33
34
  import { guardWriteAccess } from "../lib/write-guard.js";
34
35
  import { applyEdit, normalizeToLF, stripBom } from "./edit-engine.js";
35
36
 
@@ -356,7 +357,7 @@ function registerReadTool(pi: ExtensionAPI, service: LspService): void {
356
357
 
357
358
  return {
358
359
  content: [{ type: "text", text: output }],
359
- details: undefined,
360
+ details: { pendant: { subtitle: formatSubtitlePath(ctx.cwd, absolutePath) } },
360
361
  };
361
362
  }
362
363
 
@@ -384,7 +385,10 @@ function registerReadTool(pi: ExtensionAPI, service: LspService): void {
384
385
  { type: "text", text: "Image read successfully" },
385
386
  { type: "image", data: base64, mimeType },
386
387
  ];
387
- return { content, details: undefined };
388
+ return {
389
+ content,
390
+ details: { pendant: { subtitle: formatSubtitlePath(ctx.cwd, absolutePath) } },
391
+ };
388
392
  }
389
393
 
390
394
  if (isBinaryExtension(absolutePath) || isBinaryFileBySample(await readSample(absolutePath))) {
@@ -431,7 +435,13 @@ function registerReadTool(pi: ExtensionAPI, service: LspService): void {
431
435
  // 后台 warm-up 失败不影响读取
432
436
  });
433
437
 
434
- return { content, details };
438
+ return {
439
+ content,
440
+ details: {
441
+ ...details,
442
+ pendant: { subtitle: formatSubtitlePath(ctx.cwd, absolutePath) },
443
+ },
444
+ };
435
445
  },
436
446
  });
437
447
  }
@@ -482,7 +492,7 @@ function registerEditTool(pi: ExtensionAPI, service: LspService): void {
482
492
  change: { oldText: oldString, newText: newString, replaceAll },
483
493
  });
484
494
 
485
- const [message, details, diagnosticText] = await withFileMutationQueue(
495
+ const [message, details, diagnosticText, errorCount] = await withFileMutationQueue(
486
496
  absolutePath,
487
497
  async () => {
488
498
  signal?.throwIfAborted();
@@ -509,13 +519,16 @@ function registerEditTool(pi: ExtensionAPI, service: LspService): void {
509
519
  await mkdir(dirname(absolutePath), { recursive: true });
510
520
  signal?.throwIfAborted();
511
521
  await writeFile(absolutePath, newString, "utf8");
512
- const diagnosticText = await service.lspDiagnosticsForFile(absolutePath, ctx.cwd, {
513
- signal,
514
- });
522
+ const { text: diagnosticText, errorCount } = await service.lspDiagnosticsForFile(
523
+ absolutePath,
524
+ ctx.cwd,
525
+ { signal },
526
+ );
515
527
  return [
516
528
  "Edit applied successfully.",
517
529
  { diff: "", patch: "", firstChangedLine: 0 },
518
530
  diagnosticText,
531
+ errorCount,
519
532
  ] as const;
520
533
  }
521
534
 
@@ -547,13 +560,16 @@ function registerEditTool(pi: ExtensionAPI, service: LspService): void {
547
560
  const diffNew = normalizeToLF(applied.contentNew);
548
561
  const diffResult = generateDiffString(diffOld, diffNew);
549
562
  const patch = generateUnifiedPatch(filePath, diffOld, diffNew);
550
- const diagnosticText = await service.lspDiagnosticsForFile(absolutePath, ctx.cwd, {
551
- signal,
552
- });
563
+ const { text: diagnosticText, errorCount } = await service.lspDiagnosticsForFile(
564
+ absolutePath,
565
+ ctx.cwd,
566
+ { signal },
567
+ );
553
568
  return [
554
569
  "Edit applied successfully.",
555
570
  { diff: diffResult.diff, patch, firstChangedLine: diffResult.firstChangedLine },
556
571
  diagnosticText,
572
+ errorCount,
557
573
  ] as const;
558
574
  },
559
575
  );
@@ -561,7 +577,13 @@ function registerEditTool(pi: ExtensionAPI, service: LspService): void {
561
577
  const text = diagnosticText
562
578
  ? `${message}\n\nLSP errors detected in this file, please fix:\n${diagnosticText}`
563
579
  : message;
564
- return { content: [{ type: "text" as const, text }], details };
580
+ return {
581
+ content: [{ type: "text" as const, text }],
582
+ details: {
583
+ ...details,
584
+ pendant: { subtitle: formatSubtitlePath(ctx.cwd, absolutePath, errorCount) },
585
+ },
586
+ };
565
587
  },
566
588
  });
567
589
  }
@@ -614,7 +636,7 @@ function registerWriteTool(pi: ExtensionAPI, service: LspService): void {
614
636
  });
615
637
  const dir = dirname(absolutePath);
616
638
 
617
- const [message, details, diagnosticText] = await withFileMutationQueue(
639
+ const [message, details, diagnosticText, errorCount] = await withFileMutationQueue(
618
640
  absolutePath,
619
641
  async () => {
620
642
  signal?.throwIfAborted();
@@ -639,18 +661,26 @@ function registerWriteTool(pi: ExtensionAPI, service: LspService): void {
639
661
  await mkdir(dir, { recursive: true });
640
662
  signal?.throwIfAborted();
641
663
  await writeFile(absolutePath, desiredBom + nextText, "utf8");
642
- const diagnosticText = await service.lspDiagnosticsForFile(absolutePath, ctx.cwd, {
643
- signal,
644
- });
664
+ const { text: diagnosticText, errorCount } = await service.lspDiagnosticsForFile(
665
+ absolutePath,
666
+ ctx.cwd,
667
+ { signal },
668
+ );
645
669
 
646
- return ["Wrote file successfully.", undefined, diagnosticText] as const;
670
+ return ["Wrote file successfully.", {}, diagnosticText, errorCount] as const;
647
671
  },
648
672
  );
649
673
 
650
674
  const text = diagnosticText
651
675
  ? `${message}\n\nLSP errors detected in this file, please fix:\n${diagnosticText}`
652
676
  : message;
653
- return { content: [{ type: "text" as const, text }], details };
677
+ return {
678
+ content: [{ type: "text" as const, text }],
679
+ details: {
680
+ ...details,
681
+ pendant: { subtitle: formatSubtitlePath(ctx.cwd, absolutePath, errorCount) },
682
+ },
683
+ };
654
684
  },
655
685
  });
656
686
  }