@trim21/personal-pi-extensions 0.1.540 → 0.1.542
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 +1 -1
- package/src/claude-code/edit.md +1 -0
- package/src/claude-code/files.ts +227 -229
- package/src/claude-code/write.md +1 -0
- package/src/lib/lsp/diagnostic.ts +41 -14
- package/src/lib/lsp/lsp.ts +11 -31
- package/src/opencode/files.ts +55 -48
package/package.json
CHANGED
package/src/claude-code/edit.md
CHANGED
|
@@ -4,6 +4,7 @@ Performs exact string replacements in files.
|
|
|
4
4
|
|
|
5
5
|
Usage:
|
|
6
6
|
|
|
7
|
+
- You must use your Read tool at least once in the conversation before editing. This tool will error if you attempt an edit without reading the file.
|
|
7
8
|
- When editing text from Read tool output, ensure you preserve the exact indentation (tabs/spaces) as it appears AFTER the line number prefix. The line number prefix format is: line number + tab. Everything after that is the actual file content to match. Never include any part of the line number prefix in the old_string or new_string.
|
|
8
9
|
- ALWAYS prefer editing existing files in the codebase. NEVER write new files unless explicitly required.
|
|
9
10
|
- Only use emojis if the user explicitly requests it. Avoid adding emojis to files unless asked.
|
package/src/claude-code/files.ts
CHANGED
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
} from "@earendil-works/pi-coding-agent";
|
|
15
15
|
import { Type } from "typebox";
|
|
16
16
|
|
|
17
|
-
import { appendLspDiagnosticText } from "../lib/lsp/diagnostic.js";
|
|
17
|
+
import { appendLspDiagnosticText, type DiagnosticReport } from "../lib/lsp/diagnostic.js";
|
|
18
18
|
import { registerLspInspectTools } from "../lib/lsp/inspect-tool.js";
|
|
19
19
|
import { createLspManager, type LspService, type LspServiceOptions } from "../lib/lsp/lsp.js";
|
|
20
20
|
import { registerLspRenameTool } from "../lib/lsp/rename-tool.js";
|
|
@@ -199,9 +199,8 @@ async function readStateKey(filePath: string): Promise<string> {
|
|
|
199
199
|
}
|
|
200
200
|
|
|
201
201
|
/**
|
|
202
|
-
*
|
|
203
|
-
*
|
|
204
|
-
* 不强制先 Read;存在已读快照时校验文本可编辑且 digest 一致。
|
|
202
|
+
* 校验「已读且未变」。key 与 currentContent 由调用方提供:调用方每次工具调用
|
|
203
|
+
* 只 realpath / readFile 一次,避免重复 IO。
|
|
205
204
|
*/
|
|
206
205
|
function requireCurrentRead(
|
|
207
206
|
state: ClaudeCodeState,
|
|
@@ -210,7 +209,9 @@ function requireCurrentRead(
|
|
|
210
209
|
currentContent: Uint8Array,
|
|
211
210
|
): void {
|
|
212
211
|
const readSnapshot = state.reads.get(key);
|
|
213
|
-
if (!readSnapshot)
|
|
212
|
+
if (!readSnapshot) {
|
|
213
|
+
throw new Error("File has not been read yet. Read it first before writing to it.");
|
|
214
|
+
}
|
|
214
215
|
if (!readSnapshot.textEditable) {
|
|
215
216
|
throw new Error(`Cannot edit or overwrite a binary file with a text tool: ${filePath}`);
|
|
216
217
|
}
|
|
@@ -331,18 +332,26 @@ export function registerFileTools(
|
|
|
331
332
|
}
|
|
332
333
|
const snapshot = snapshotOf(buffer);
|
|
333
334
|
state.reads.set(key, snapshot);
|
|
334
|
-
//
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
// 后台通知失败不影响读取
|
|
339
|
-
});
|
|
335
|
+
// 与 Edit / Write 同一条驻留路径:didOpen 后等待该文件的诊断并报告
|
|
336
|
+
const diagnostics = await getService().lspDiagnosticsForFile(filePath, ctx.cwd, {
|
|
337
|
+
notify: (message, level) => ctx.ui.notify(message, level),
|
|
338
|
+
});
|
|
340
339
|
return {
|
|
341
|
-
content: [
|
|
340
|
+
content: [
|
|
341
|
+
{
|
|
342
|
+
type: "text",
|
|
343
|
+
text: appendLspDiagnosticText(formatted.text, diagnostics.text),
|
|
344
|
+
},
|
|
345
|
+
],
|
|
342
346
|
details: {
|
|
343
347
|
reads: { [key]: snapshot },
|
|
344
348
|
pendant: {
|
|
345
|
-
subtitle: formatSubtitlePath(
|
|
349
|
+
subtitle: formatSubtitlePath(
|
|
350
|
+
ctx.cwd,
|
|
351
|
+
filePath,
|
|
352
|
+
diagnostics.errorCount,
|
|
353
|
+
diagnostics.warningCount,
|
|
354
|
+
),
|
|
346
355
|
title: "Read",
|
|
347
356
|
} satisfies ToolPendant,
|
|
348
357
|
},
|
|
@@ -355,6 +364,7 @@ export function registerFileTools(
|
|
|
355
364
|
label: "Edit",
|
|
356
365
|
description: [
|
|
357
366
|
"Performs exact string replacements in files.",
|
|
367
|
+
"You must use Read on the file before editing it.",
|
|
358
368
|
"old_string must match exactly and must be unique unless replace_all is true.",
|
|
359
369
|
"This tool does not use regular expressions or fuzzy matching.",
|
|
360
370
|
].join("\n"),
|
|
@@ -385,178 +395,167 @@ export function registerFileTools(
|
|
|
385
395
|
replaceAll: params.replace_all,
|
|
386
396
|
},
|
|
387
397
|
});
|
|
388
|
-
const [message, details,
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
const content = await readFile(filePath, "utf8");
|
|
406
|
-
if (content.trim() !== "") {
|
|
407
|
-
throw new Error("Cannot create new file - file already exists.");
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
} catch (error) {
|
|
411
|
-
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
412
|
-
exists = false;
|
|
413
|
-
} else {
|
|
414
|
-
throw error;
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
if (!exists) await mkdir(dirname(filePath), { recursive: true });
|
|
418
|
-
await writeFile(filePath, newString, "utf8");
|
|
419
|
-
const snapshot = snapshotOf(newString);
|
|
420
|
-
const key = await readStateKey(filePath);
|
|
421
|
-
state.reads.set(key, snapshot);
|
|
422
|
-
const diff = generateDiffString("", convertLeadingTabsToSpaces(newString));
|
|
423
|
-
signal?.throwIfAborted();
|
|
424
|
-
const {
|
|
425
|
-
text: diagnosticText,
|
|
426
|
-
errorCount,
|
|
427
|
-
warningCount,
|
|
428
|
-
} = await getService().lspDiagnosticsForFile(filePath, ctx.cwd, {
|
|
429
|
-
notify: (message, level) => ctx.ui.notify(message, level),
|
|
430
|
-
signal,
|
|
431
|
-
});
|
|
432
|
-
return [
|
|
433
|
-
`The file ${filePath} has been updated successfully.`,
|
|
434
|
-
{
|
|
435
|
-
diff: diff.diff,
|
|
436
|
-
patch: generateUnifiedPatch(filePath, "", convertLeadingTabsToSpaces(newString)),
|
|
437
|
-
firstChangedLine: diff.firstChangedLine,
|
|
438
|
-
reads: { [key]: snapshot },
|
|
439
|
-
},
|
|
440
|
-
diagnosticText,
|
|
441
|
-
errorCount,
|
|
442
|
-
warningCount,
|
|
443
|
-
];
|
|
444
|
-
}
|
|
445
|
-
const replaceAll = params.replace_all ?? false;
|
|
446
|
-
// 防止 OOM 的大文件检查(对齐 Claude Code)
|
|
447
|
-
try {
|
|
448
|
-
const { size } = await stat(filePath);
|
|
449
|
-
if (size > MAX_EDIT_FILE_SIZE) {
|
|
450
|
-
throw new Error(
|
|
451
|
-
`File is too large to edit (${formatFileSize(size)}). Maximum editable file size is ${formatFileSize(MAX_EDIT_FILE_SIZE)}.`,
|
|
452
|
-
);
|
|
453
|
-
}
|
|
454
|
-
} catch (error) {
|
|
455
|
-
if (!(error instanceof Error) || !("code" in error) || error.code !== "ENOENT") {
|
|
456
|
-
throw error;
|
|
398
|
+
const [message, details, diagnostics] = await withFileMutationQueue<
|
|
399
|
+
[string, FileToolDetails, DiagnosticReport]
|
|
400
|
+
>(filePath, async () => {
|
|
401
|
+
const oldString = params.old_string;
|
|
402
|
+
const newString = params.new_string;
|
|
403
|
+
if (oldString === newString) {
|
|
404
|
+
throw new Error("No changes to make: old_string and new_string are exactly the same.");
|
|
405
|
+
}
|
|
406
|
+
// 空 old_string:创建新文件或填充空文件(不需要先 Read,对齐 Claude Code)
|
|
407
|
+
if (oldString === "") {
|
|
408
|
+
let exists = true;
|
|
409
|
+
try {
|
|
410
|
+
const value = await stat(filePath);
|
|
411
|
+
if (value.isFile()) {
|
|
412
|
+
const content = await readFile(filePath, "utf8");
|
|
413
|
+
if (content.trim() !== "") {
|
|
414
|
+
throw new Error("Cannot create new file - file already exists.");
|
|
457
415
|
}
|
|
458
416
|
}
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
}
|
|
463
|
-
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
464
|
-
const suggestion = await didYouMean(filePath, ctx.cwd);
|
|
465
|
-
throw new Error(
|
|
466
|
-
`File does not exist. Note: your current working directory is ${ctx.cwd}.${suggestion ? ` Did you mean ${suggestion}?` : ""}`,
|
|
467
|
-
{ cause: error },
|
|
468
|
-
);
|
|
469
|
-
}
|
|
417
|
+
} catch (error) {
|
|
418
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
419
|
+
exists = false;
|
|
420
|
+
} else {
|
|
470
421
|
throw error;
|
|
471
422
|
}
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
const
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
423
|
+
}
|
|
424
|
+
if (!exists) await mkdir(dirname(filePath), { recursive: true });
|
|
425
|
+
await writeFile(filePath, newString, "utf8");
|
|
426
|
+
const snapshot = snapshotOf(newString);
|
|
427
|
+
const key = await readStateKey(filePath);
|
|
428
|
+
state.reads.set(key, snapshot);
|
|
429
|
+
const diff = generateDiffString("", convertLeadingTabsToSpaces(newString));
|
|
430
|
+
signal?.throwIfAborted();
|
|
431
|
+
const diagnostics = await getService().lspDiagnosticsForFile(filePath, ctx.cwd, {
|
|
432
|
+
notify: (message, level) => ctx.ui.notify(message, level),
|
|
433
|
+
signal,
|
|
434
|
+
});
|
|
435
|
+
return [
|
|
436
|
+
`The file ${filePath} has been updated successfully.`,
|
|
437
|
+
{
|
|
438
|
+
diff: diff.diff,
|
|
439
|
+
patch: generateUnifiedPatch(filePath, "", convertLeadingTabsToSpaces(newString)),
|
|
440
|
+
firstChangedLine: diff.firstChangedLine,
|
|
441
|
+
reads: { [key]: snapshot },
|
|
442
|
+
},
|
|
443
|
+
diagnostics,
|
|
444
|
+
];
|
|
445
|
+
}
|
|
446
|
+
const replaceAll = params.replace_all ?? false;
|
|
447
|
+
// 防止 OOM 的大文件检查(对齐 Claude Code)
|
|
448
|
+
try {
|
|
449
|
+
const { size } = await stat(filePath);
|
|
450
|
+
if (size > MAX_EDIT_FILE_SIZE) {
|
|
451
|
+
throw new Error(
|
|
452
|
+
`File is too large to edit (${formatFileSize(size)}). Maximum editable file size is ${formatFileSize(MAX_EDIT_FILE_SIZE)}.`,
|
|
453
|
+
);
|
|
454
|
+
}
|
|
455
|
+
} catch (error) {
|
|
456
|
+
if (!(error instanceof Error) || !("code" in error) || error.code !== "ENOENT") {
|
|
457
|
+
throw error;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
let content: Buffer;
|
|
461
|
+
try {
|
|
462
|
+
content = await readFile(filePath);
|
|
463
|
+
} catch (error) {
|
|
464
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
465
|
+
const suggestion = await didYouMean(filePath, ctx.cwd);
|
|
466
|
+
throw new Error(
|
|
467
|
+
`File does not exist. Note: your current working directory is ${ctx.cwd}.${suggestion ? ` Did you mean ${suggestion}?` : ""}`,
|
|
468
|
+
{ cause: error },
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
throw error;
|
|
472
|
+
}
|
|
473
|
+
if (extname(filePath).toLowerCase() === ".ipynb") {
|
|
474
|
+
throw new Error(
|
|
475
|
+
"File is a Jupyter Notebook. Use the NotebookEditTool to edit this file.",
|
|
476
|
+
);
|
|
477
|
+
}
|
|
478
|
+
const key = await readStateKey(filePath);
|
|
479
|
+
requireCurrentRead(state, key, filePath, content);
|
|
480
|
+
await access(filePath, constants.R_OK | constants.W_OK);
|
|
481
|
+
signal?.throwIfAborted();
|
|
482
|
+
const original = content.toString("utf8");
|
|
483
|
+
|
|
484
|
+
// CRLF 规范化后匹配(old_string 不需要带 \r),写回时恢复原行尾
|
|
485
|
+
const crlfCount = (original.match(/\r\n/g) ?? []).length;
|
|
486
|
+
const lfCount = (original.match(/(?<!\r)\n/g) ?? []).length;
|
|
487
|
+
const lineEnding = crlfCount > lfCount ? "\r\n" : "\n";
|
|
488
|
+
const normalized = original.replaceAll("\r\n", "\n");
|
|
489
|
+
const actualOldString = findActualString(normalized, oldString) ?? oldString;
|
|
490
|
+
const matches = normalized.split(actualOldString).length - 1;
|
|
491
|
+
if (matches === 0) {
|
|
492
|
+
throw new Error(`String to replace not found in file.\nString: ${oldString}`);
|
|
493
|
+
}
|
|
494
|
+
if (!replaceAll && matches > 1) {
|
|
495
|
+
throw new Error(
|
|
496
|
+
`Found ${matches} matches of the string to replace, but replace_all is false. To replace all occurrences, set replace_all to true. To replace only one occurrence, please provide more context to uniquely identify the instance.\nString: ${oldString}`,
|
|
497
|
+
);
|
|
498
|
+
}
|
|
499
|
+
const actualNewString = preserveQuoteStyle(oldString, actualOldString, newString);
|
|
500
|
+
// 删除场景(new_string 为空):old_string 不以换行结尾且文件里是
|
|
501
|
+
// "old_string\n" 时连换行一起删,避免留下空行(对齐 Claude Code
|
|
502
|
+
// applyEditToFile 的 stripTrailingNewline 语义)
|
|
503
|
+
let searchString = actualOldString;
|
|
504
|
+
if (
|
|
505
|
+
actualNewString === "" &&
|
|
506
|
+
!actualOldString.endsWith("\n") &&
|
|
507
|
+
normalized.includes(actualOldString + "\n")
|
|
508
|
+
) {
|
|
509
|
+
searchString = actualOldString + "\n";
|
|
510
|
+
}
|
|
511
|
+
// split/join 与函数替换:replacement 含 $ 时不会触发 $& 等特殊语义
|
|
512
|
+
const updated = replaceAll
|
|
513
|
+
? normalized.split(searchString).join(actualNewString)
|
|
514
|
+
: normalized.replace(searchString, () => actualNewString);
|
|
515
|
+
const restored = lineEnding === "\r\n" ? updated.replaceAll("\n", "\r\n") : updated;
|
|
516
|
+
await writeFile(filePath, restored, "utf8");
|
|
517
|
+
const snapshot = snapshotOf(restored);
|
|
518
|
+
state.reads.set(key, snapshot);
|
|
519
|
+
// patch/diff 仅供显示:前导 tab 转空格,避免 UI 渲染错位(对齐 Claude Code)
|
|
520
|
+
const diff = generateDiffString(
|
|
521
|
+
convertLeadingTabsToSpaces(original),
|
|
522
|
+
convertLeadingTabsToSpaces(restored),
|
|
523
|
+
);
|
|
524
|
+
const text = replaceAll
|
|
525
|
+
? `The file ${filePath} has been updated. All occurrences were successfully replaced.`
|
|
526
|
+
: `The file ${filePath} has been updated successfully.`;
|
|
527
|
+
signal?.throwIfAborted();
|
|
528
|
+
const diagnostics = await getService().lspDiagnosticsForFile(filePath, ctx.cwd, {
|
|
529
|
+
notify: (message, level) => ctx.ui.notify(message, level),
|
|
530
|
+
});
|
|
531
|
+
return [
|
|
532
|
+
text,
|
|
533
|
+
{
|
|
534
|
+
diff: diff.diff,
|
|
535
|
+
patch: generateUnifiedPatch(
|
|
536
|
+
filePath,
|
|
520
537
|
convertLeadingTabsToSpaces(original),
|
|
521
538
|
convertLeadingTabsToSpaces(restored),
|
|
522
|
-
)
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
: `The file ${filePath} has been updated successfully.`;
|
|
526
|
-
signal?.throwIfAborted();
|
|
527
|
-
const {
|
|
528
|
-
text: diagnosticText,
|
|
529
|
-
errorCount,
|
|
530
|
-
warningCount,
|
|
531
|
-
} = await getService().lspDiagnosticsForFile(filePath, ctx.cwd, {
|
|
532
|
-
notify: (message, level) => ctx.ui.notify(message, level),
|
|
533
|
-
});
|
|
534
|
-
return [
|
|
535
|
-
text,
|
|
536
|
-
{
|
|
537
|
-
diff: diff.diff,
|
|
538
|
-
patch: generateUnifiedPatch(
|
|
539
|
-
filePath,
|
|
540
|
-
convertLeadingTabsToSpaces(original),
|
|
541
|
-
convertLeadingTabsToSpaces(restored),
|
|
542
|
-
),
|
|
543
|
-
firstChangedLine: diff.firstChangedLine,
|
|
544
|
-
reads: { [key]: snapshot },
|
|
545
|
-
},
|
|
546
|
-
diagnosticText,
|
|
547
|
-
errorCount,
|
|
548
|
-
warningCount,
|
|
549
|
-
];
|
|
539
|
+
),
|
|
540
|
+
firstChangedLine: diff.firstChangedLine,
|
|
541
|
+
reads: { [key]: snapshot },
|
|
550
542
|
},
|
|
551
|
-
|
|
543
|
+
diagnostics,
|
|
544
|
+
];
|
|
545
|
+
});
|
|
552
546
|
|
|
553
|
-
const text = appendLspDiagnosticText(message,
|
|
547
|
+
const text = appendLspDiagnosticText(message, diagnostics.text);
|
|
554
548
|
return {
|
|
555
549
|
content: [{ type: "text" as const, text }],
|
|
556
550
|
details: {
|
|
557
551
|
...details,
|
|
558
552
|
pendant: {
|
|
559
|
-
subtitle: formatSubtitlePath(
|
|
553
|
+
subtitle: formatSubtitlePath(
|
|
554
|
+
ctx.cwd,
|
|
555
|
+
filePath,
|
|
556
|
+
diagnostics.errorCount,
|
|
557
|
+
diagnostics.warningCount,
|
|
558
|
+
),
|
|
560
559
|
title: "Edit",
|
|
561
560
|
} satisfies ToolPendant,
|
|
562
561
|
},
|
|
@@ -570,7 +569,7 @@ export function registerFileTools(
|
|
|
570
569
|
description: [
|
|
571
570
|
"Writes a file to the local filesystem.",
|
|
572
571
|
"This tool overwrites an existing file with the full content provided.",
|
|
573
|
-
"Prefer Edit for partial changes.",
|
|
572
|
+
"If the file exists, you must use Read first. Prefer Edit for partial changes.",
|
|
574
573
|
].join("\n"),
|
|
575
574
|
promptSnippet: "Create or overwrite files",
|
|
576
575
|
promptGuidelines: [WRITE_PROMPT],
|
|
@@ -591,66 +590,65 @@ export function registerFileTools(
|
|
|
591
590
|
absolutePath: filePath,
|
|
592
591
|
change: { oldText: "", newText: params.content },
|
|
593
592
|
});
|
|
594
|
-
const [message, details,
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
{
|
|
636
|
-
diff: diff.diff,
|
|
637
|
-
patch: generateUnifiedPatch(filePath, original ?? "", params.content),
|
|
638
|
-
firstChangedLine: diff.firstChangedLine,
|
|
639
|
-
reads: { [resolvedKey]: snapshot },
|
|
640
|
-
},
|
|
641
|
-
diagnosticText,
|
|
642
|
-
errorCount,
|
|
643
|
-
warningCount,
|
|
644
|
-
];
|
|
593
|
+
const [message, details, diagnostics] = await withFileMutationQueue<
|
|
594
|
+
[string, FileToolDetails, DiagnosticReport]
|
|
595
|
+
>(filePath, async () => {
|
|
596
|
+
let original: string | undefined;
|
|
597
|
+
let key: string | undefined;
|
|
598
|
+
try {
|
|
599
|
+
const value = await stat(filePath);
|
|
600
|
+
if (value.isFile()) {
|
|
601
|
+
const content = await readFile(filePath);
|
|
602
|
+
key = await readStateKey(filePath);
|
|
603
|
+
requireCurrentRead(state, key, filePath, content);
|
|
604
|
+
original = content.toString("utf8");
|
|
605
|
+
}
|
|
606
|
+
} catch (error) {
|
|
607
|
+
if (!(error instanceof Error) || !("code" in error) || error.code !== "ENOENT") {
|
|
608
|
+
throw error;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
signal?.throwIfAborted();
|
|
612
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
613
|
+
await writeFile(filePath, params.content, "utf8");
|
|
614
|
+
const snapshot = snapshotOf(params.content);
|
|
615
|
+
// 新建文件:writeFile 之后 realpath 才能解析;覆盖写则复用上面的 key
|
|
616
|
+
const resolvedKey = key ?? (await readStateKey(filePath));
|
|
617
|
+
state.reads.set(resolvedKey, snapshot);
|
|
618
|
+
const diff = generateDiffString(original ?? "", params.content);
|
|
619
|
+
const text =
|
|
620
|
+
original === undefined
|
|
621
|
+
? `File created successfully at: ${filePath}`
|
|
622
|
+
: `The file ${filePath} has been updated successfully.`;
|
|
623
|
+
signal?.throwIfAborted();
|
|
624
|
+
const diagnostics = await getService().lspDiagnosticsForFile(filePath, ctx.cwd, {
|
|
625
|
+
notify: (message, level) => ctx.ui.notify(message, level),
|
|
626
|
+
});
|
|
627
|
+
return [
|
|
628
|
+
text,
|
|
629
|
+
{
|
|
630
|
+
diff: diff.diff,
|
|
631
|
+
patch: generateUnifiedPatch(filePath, original ?? "", params.content),
|
|
632
|
+
firstChangedLine: diff.firstChangedLine,
|
|
633
|
+
reads: { [resolvedKey]: snapshot },
|
|
645
634
|
},
|
|
646
|
-
|
|
635
|
+
diagnostics,
|
|
636
|
+
];
|
|
637
|
+
});
|
|
647
638
|
|
|
648
|
-
const text = appendLspDiagnosticText(message,
|
|
639
|
+
const text = appendLspDiagnosticText(message, diagnostics.text);
|
|
649
640
|
return {
|
|
650
641
|
content: [{ type: "text" as const, text }],
|
|
651
642
|
details: {
|
|
652
643
|
...details,
|
|
653
|
-
pendant: {
|
|
644
|
+
pendant: {
|
|
645
|
+
subtitle: formatSubtitlePath(
|
|
646
|
+
ctx.cwd,
|
|
647
|
+
filePath,
|
|
648
|
+
diagnostics.errorCount,
|
|
649
|
+
diagnostics.warningCount,
|
|
650
|
+
),
|
|
651
|
+
},
|
|
654
652
|
},
|
|
655
653
|
};
|
|
656
654
|
},
|
|
@@ -693,8 +691,8 @@ export default function claudeCodeFileTools(pi: ExtensionAPI, options?: LspServi
|
|
|
693
691
|
const state = createClaudeCodeState();
|
|
694
692
|
|
|
695
693
|
// LSP 专属工具(lsp-rename / inspect 族)仅在 lsp.json 存在 enabled 服务器时
|
|
696
|
-
// 注册(session_start
|
|
697
|
-
//
|
|
694
|
+
// 注册(session_start 校验后);本工具集跟踪 read-before-write 状态,rename
|
|
695
|
+
// 落盘的文件要标记为已读并随 details 持久化(restoreFileReads 依赖 details.reads)。
|
|
698
696
|
const manager = createLspManager(
|
|
699
697
|
pi,
|
|
700
698
|
{
|
package/src/claude-code/write.md
CHANGED
|
@@ -5,6 +5,7 @@ Writes a file to the local filesystem.
|
|
|
5
5
|
Usage:
|
|
6
6
|
|
|
7
7
|
- This tool will overwrite the existing file if there is one at the provided path.
|
|
8
|
+
- If this is an existing file, you MUST use the Read tool first to read the file's contents. This tool will fail if you did not read the file first.
|
|
8
9
|
- Prefer the Edit tool for modifying existing files — it only sends the diff. Only use this tool to create new files or for complete rewrites.
|
|
9
10
|
- NEVER create documentation files (*.md) or README files unless explicitly requested by the User.
|
|
10
11
|
- Only use emojis if the user explicitly requests it. Avoid writing emojis to files unless asked.
|
|
@@ -17,6 +17,20 @@ function severityOf(diagnostic: Diagnostic): number {
|
|
|
17
17
|
return diagnostic.severity ?? 1;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
/** 单个文件的诊断报告;计数覆盖该文件全部 ERROR / WARN,不受每文件上限影响。 */
|
|
21
|
+
export interface DiagnosticReport {
|
|
22
|
+
/** 空字符串表示没有 ERROR / WARN。 */
|
|
23
|
+
text: string;
|
|
24
|
+
errorCount: number;
|
|
25
|
+
warningCount: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const EMPTY_DIAGNOSTIC_REPORT: DiagnosticReport = {
|
|
29
|
+
text: "",
|
|
30
|
+
errorCount: 0,
|
|
31
|
+
warningCount: 0,
|
|
32
|
+
};
|
|
33
|
+
|
|
20
34
|
export function prettyDiagnostic(diagnostic: Diagnostic): string {
|
|
21
35
|
const severity = SEVERITY_LABELS[severityOf(diagnostic)] ?? "ERROR";
|
|
22
36
|
const line = diagnostic.range.start.line + 1;
|
|
@@ -27,25 +41,38 @@ export function prettyDiagnostic(diagnostic: Diagnostic): string {
|
|
|
27
41
|
return `${severity} [${line}:${col}] ${message}`;
|
|
28
42
|
}
|
|
29
43
|
|
|
30
|
-
|
|
31
|
-
|
|
44
|
+
function pluralize(count: number, noun: string): string {
|
|
45
|
+
return `${count} ${noun}${count === 1 ? "" : "s"}`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** 未列出部分的构成:`3 errors, 4 warnings`(为 0 的一类省略;单复数按数量)。 */
|
|
49
|
+
function hiddenSummary(hidden: Diagnostic[]): string {
|
|
50
|
+
const errors = hidden.filter((item) => severityOf(item) === 1).length;
|
|
51
|
+
const warnings = hidden.length - errors;
|
|
52
|
+
return [
|
|
53
|
+
...(errors > 0 ? [pluralize(errors, "error")] : []),
|
|
54
|
+
...(warnings > 0 ? [pluralize(warnings, "warning")] : []),
|
|
55
|
+
].join(", ");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** 返回空 text 表示没有 ERROR / WARN。ERROR 在前,WARN 在后,超出上限的部分只报数量。 */
|
|
59
|
+
export function report(file: string, issues: Diagnostic[]): DiagnosticReport {
|
|
32
60
|
const errors = issues.filter((item) => severityOf(item) === 1);
|
|
33
61
|
const warnings = issues.filter((item) => item.severity === 2);
|
|
34
62
|
const relevant = [...errors, ...warnings];
|
|
35
|
-
if (relevant.length === 0) return
|
|
63
|
+
if (relevant.length === 0) return EMPTY_DIAGNOSTIC_REPORT;
|
|
36
64
|
const limited = relevant.slice(0, MAX_PER_FILE);
|
|
37
|
-
const
|
|
38
|
-
const suffix =
|
|
39
|
-
return
|
|
65
|
+
const hidden = relevant.slice(MAX_PER_FILE);
|
|
66
|
+
const suffix = hidden.length > 0 ? `\n... and ${hiddenSummary(hidden)}` : "";
|
|
67
|
+
return {
|
|
68
|
+
text: `<diagnostics file="${file}">\n${limited.map((d) => prettyDiagnostic(d)).join("\n")}${suffix}\n</diagnostics>`,
|
|
69
|
+
errorCount: errors.length,
|
|
70
|
+
warningCount: warnings.length,
|
|
71
|
+
};
|
|
40
72
|
}
|
|
41
73
|
|
|
42
74
|
/** 把 LSP 诊断块接到工具成功文案后面;无诊断时原样返回。 */
|
|
43
|
-
export function appendLspDiagnosticText(
|
|
44
|
-
message
|
|
45
|
-
diagnosticText
|
|
46
|
-
errorCount: number,
|
|
47
|
-
): string {
|
|
48
|
-
if (!diagnosticText) return message;
|
|
49
|
-
const kind = errorCount > 0 ? "errors" : "warnings";
|
|
50
|
-
return `${message}\n\nLSP ${kind} detected in this file:\n${diagnosticText}`;
|
|
75
|
+
export function appendLspDiagnosticText(message: string, diagnosticText: string): string {
|
|
76
|
+
if (diagnosticText === "") return message;
|
|
77
|
+
return `${message}\n\nLSP diagnostics detected in this file\n${diagnosticText}`;
|
|
51
78
|
}
|
package/src/lib/lsp/lsp.ts
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* 的 id 直接忽略;
|
|
17
17
|
* - client 按 (root, serverID) 缓存,并发 spawn 去重,启动失败记入
|
|
18
18
|
* broken(冷却期内跳过,冷却过后下次触碰自动重试)并主动 notify;
|
|
19
|
-
* - 工具只与 touchFile /
|
|
19
|
+
* - 工具只与 touchFile / diagnostics / lspDiagnosticsForFile 三个方法打交道;通知回调按请求传入。
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
22
|
import { readFileSync } from "node:fs";
|
|
@@ -43,7 +43,7 @@ import {
|
|
|
43
43
|
WATCH_KIND_CREATE,
|
|
44
44
|
WATCH_KIND_DELETE,
|
|
45
45
|
} from "./client.js";
|
|
46
|
-
import { report } from "./diagnostic.js";
|
|
46
|
+
import { type DiagnosticReport, EMPTY_DIAGNOSTIC_REPORT, report } from "./diagnostic.js";
|
|
47
47
|
import {
|
|
48
48
|
createAdapters,
|
|
49
49
|
matchesInclude,
|
|
@@ -397,14 +397,13 @@ export interface LspService {
|
|
|
397
397
|
diagnostics?: "document" | "full",
|
|
398
398
|
options?: LspRequestOptions,
|
|
399
399
|
): Promise<void>;
|
|
400
|
-
/** read 用:只发文件事件通知服务器磁盘上有该文件,不驻留、不等诊断。 */
|
|
401
|
-
notifyFile(file: string, cwd: string, options?: LspRequestOptions): Promise<void>;
|
|
402
400
|
diagnostics(): Promise<Record<string, Diagnostic[]>>;
|
|
401
|
+
/** read / edit / write 用:等待文档诊断并返回该文件的 ERROR / WARN 报告与计数。 */
|
|
403
402
|
lspDiagnosticsForFile(
|
|
404
403
|
file: string,
|
|
405
404
|
cwd: string,
|
|
406
405
|
options?: LspRequestOptions,
|
|
407
|
-
): Promise<
|
|
406
|
+
): Promise<DiagnosticReport>;
|
|
408
407
|
/**
|
|
409
408
|
* 符号重命名:只面向 kind 为 "language" 的服务器(linter 不参与符号级
|
|
410
409
|
* 功能);多 client 按配置顺序取第一个成功结果,全部失败时抛聚合错误。
|
|
@@ -736,7 +735,7 @@ export function createLspService(
|
|
|
736
735
|
/**
|
|
737
736
|
* 记录一次启动失败:进入 broken(冷却期内跳过)、渲染 status,并按节流
|
|
738
737
|
* 间隔主动 notify。错误上报优先走会话级 sessionNotify——不依赖触发请求
|
|
739
|
-
* 恰好携带 notify(否则
|
|
738
|
+
* 恰好携带 notify(否则 opencode 工具集等不带请求级 notify 的通道会把失败吞掉);未注入
|
|
740
739
|
* 会话通知时退回请求级 notify 兜底。
|
|
741
740
|
*/
|
|
742
741
|
function reportStartupFailure(
|
|
@@ -881,7 +880,7 @@ export function createLspService(
|
|
|
881
880
|
|
|
882
881
|
/**
|
|
883
882
|
* 打开文档让服务器索引 / 产出诊断。diagnostics 传 "document" 时最多等 5s,
|
|
884
|
-
* "full" 最多等 10s
|
|
883
|
+
* "full" 最多等 10s;不传则只 didOpen 不等待。
|
|
885
884
|
*/
|
|
886
885
|
async function touchFile(
|
|
887
886
|
file: string,
|
|
@@ -923,37 +922,19 @@ export function createLspService(
|
|
|
923
922
|
}
|
|
924
923
|
|
|
925
924
|
/**
|
|
926
|
-
* read
|
|
927
|
-
*
|
|
928
|
-
*/
|
|
929
|
-
async function notifyFile(file: string, cwd: string, options?: LspRequestOptions): Promise<void> {
|
|
930
|
-
const clients = await getClients(file, cwd, options?.notify);
|
|
931
|
-
await Promise.all(
|
|
932
|
-
clients.map((client) =>
|
|
933
|
-
client.notify.watchedFiles([{ path: file, type: "changed", isDirectory: false }]),
|
|
934
|
-
),
|
|
935
|
-
).catch(() => {
|
|
936
|
-
// 文件事件通知失败不影响读取
|
|
937
|
-
});
|
|
938
|
-
}
|
|
939
|
-
|
|
940
|
-
/**
|
|
941
|
-
* edit/write 用:等待文档诊断并返回该文件的 ERROR / WARN 报告(text 空串表示无此类诊断)
|
|
942
|
-
* 与数量。内部所有 LSP 失败都会被吞掉,不干扰写操作本身。
|
|
925
|
+
* read / edit / write 用:等待文档诊断并返回该文件的 ERROR / WARN 报告(text 空串表示无此类诊断)
|
|
926
|
+
* 与数量。内部所有 LSP 失败都会被吞掉,不干扰读取或写操作本身。
|
|
943
927
|
*/
|
|
944
928
|
async function lspDiagnosticsForFile(
|
|
945
929
|
file: string,
|
|
946
930
|
cwd: string,
|
|
947
931
|
options?: LspRequestOptions,
|
|
948
|
-
): Promise<
|
|
949
|
-
if (options?.signal?.aborted) return
|
|
932
|
+
): Promise<DiagnosticReport> {
|
|
933
|
+
if (options?.signal?.aborted) return EMPTY_DIAGNOSTIC_REPORT;
|
|
950
934
|
await touchFile(file, cwd, "document", options);
|
|
951
935
|
const all = await diagnostics();
|
|
952
936
|
const normalized = normalize(file);
|
|
953
|
-
|
|
954
|
-
const errorCount = issues.filter((item) => (item.severity ?? 1) === 1).length;
|
|
955
|
-
const warningCount = issues.filter((item) => item.severity === 2).length;
|
|
956
|
-
return { text: report(normalized, issues), errorCount, warningCount };
|
|
937
|
+
return report(normalized, all[normalized] ?? []);
|
|
957
938
|
}
|
|
958
939
|
|
|
959
940
|
/** 符号重命名:只面向 language 类服务器;按配置顺序取第一个成功结果。 */
|
|
@@ -1184,7 +1165,6 @@ export function createLspService(
|
|
|
1184
1165
|
|
|
1185
1166
|
return {
|
|
1186
1167
|
touchFile,
|
|
1187
|
-
notifyFile,
|
|
1188
1168
|
diagnostics,
|
|
1189
1169
|
lspDiagnosticsForFile,
|
|
1190
1170
|
rename,
|
package/src/opencode/files.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*
|
|
8
8
|
* 对齐官方 v1(packages/opencode/src/tool/{read,edit,write}.ts):
|
|
9
9
|
* - read:流式分行(LF / CRLF / CR)、每行 `N: ` 行号前缀、单行 2000
|
|
10
|
-
* 字符截断、1 起始 offset
|
|
10
|
+
* 字符截断、1 起始 offset、目录排序;读取后等待并报告该文档的诊断。
|
|
11
11
|
* 不接 PDF、不接 <system-reminder>;图片 magic 检测保留。
|
|
12
12
|
* - edit:匹配引擎 + 把 old/new 转到文件换行后再替换;写后等待文档诊断。
|
|
13
13
|
* - write:BOM 保留(source.bom || next.bom);写后同 edit 的诊断输出。
|
|
@@ -431,20 +431,29 @@ function registerReadTool(pi: ExtensionAPI, getService: () => LspService): void
|
|
|
431
431
|
outputText = `${header}${numbered}\n\n(End of file - total ${page.count} lines)${footer}`;
|
|
432
432
|
}
|
|
433
433
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
434
|
+
// opencode: 与 edit / write 同一条驻留路径:didOpen 后等待该文件的诊断并报告
|
|
435
|
+
const diagnostics = await getService().lspDiagnosticsForFile(absolutePath, ctx.cwd, {
|
|
436
|
+
signal,
|
|
437
|
+
});
|
|
438
|
+
content = [
|
|
439
|
+
{
|
|
440
|
+
type: "text",
|
|
441
|
+
text: appendLspDiagnosticText(outputText, diagnostics.text),
|
|
442
|
+
},
|
|
443
|
+
];
|
|
442
444
|
|
|
443
445
|
return {
|
|
444
446
|
content,
|
|
445
447
|
details: {
|
|
446
448
|
...details,
|
|
447
|
-
pendant: {
|
|
449
|
+
pendant: {
|
|
450
|
+
subtitle: formatSubtitlePath(
|
|
451
|
+
ctx.cwd,
|
|
452
|
+
absolutePath,
|
|
453
|
+
diagnostics.errorCount,
|
|
454
|
+
diagnostics.warningCount,
|
|
455
|
+
),
|
|
456
|
+
},
|
|
448
457
|
},
|
|
449
458
|
};
|
|
450
459
|
},
|
|
@@ -497,8 +506,9 @@ function registerEditTool(pi: ExtensionAPI, getService: () => LspService): void
|
|
|
497
506
|
change: { oldText: oldString, newText: newString, replaceAll },
|
|
498
507
|
});
|
|
499
508
|
|
|
500
|
-
const [message, details,
|
|
501
|
-
|
|
509
|
+
const [message, details, diagnostics] = await withFileMutationQueue(
|
|
510
|
+
absolutePath,
|
|
511
|
+
async () => {
|
|
502
512
|
signal?.throwIfAborted();
|
|
503
513
|
|
|
504
514
|
// opencode: 前置校验,先于空 oldString 分支
|
|
@@ -523,17 +533,13 @@ function registerEditTool(pi: ExtensionAPI, getService: () => LspService): void
|
|
|
523
533
|
await mkdir(dirname(absolutePath), { recursive: true });
|
|
524
534
|
signal?.throwIfAborted();
|
|
525
535
|
await writeFile(absolutePath, newString, "utf8");
|
|
526
|
-
const {
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
warningCount,
|
|
530
|
-
} = await getService().lspDiagnosticsForFile(absolutePath, ctx.cwd, { signal });
|
|
536
|
+
const diagnostics = await getService().lspDiagnosticsForFile(absolutePath, ctx.cwd, {
|
|
537
|
+
signal,
|
|
538
|
+
});
|
|
531
539
|
return [
|
|
532
540
|
"Edit applied successfully.",
|
|
533
541
|
{ diff: "", patch: "", firstChangedLine: 0 },
|
|
534
|
-
|
|
535
|
-
errorCount,
|
|
536
|
-
warningCount,
|
|
542
|
+
diagnostics,
|
|
537
543
|
] as const;
|
|
538
544
|
}
|
|
539
545
|
|
|
@@ -565,27 +571,29 @@ function registerEditTool(pi: ExtensionAPI, getService: () => LspService): void
|
|
|
565
571
|
const diffNew = normalizeToLF(applied.contentNew);
|
|
566
572
|
const diffResult = generateDiffString(diffOld, diffNew);
|
|
567
573
|
const patch = generateUnifiedPatch(filePath, diffOld, diffNew);
|
|
568
|
-
const {
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
warningCount,
|
|
572
|
-
} = await getService().lspDiagnosticsForFile(absolutePath, ctx.cwd, { signal });
|
|
574
|
+
const diagnostics = await getService().lspDiagnosticsForFile(absolutePath, ctx.cwd, {
|
|
575
|
+
signal,
|
|
576
|
+
});
|
|
573
577
|
return [
|
|
574
578
|
"Edit applied successfully.",
|
|
575
579
|
{ diff: diffResult.diff, patch, firstChangedLine: diffResult.firstChangedLine },
|
|
576
|
-
|
|
577
|
-
errorCount,
|
|
578
|
-
warningCount,
|
|
580
|
+
diagnostics,
|
|
579
581
|
] as const;
|
|
580
|
-
}
|
|
582
|
+
},
|
|
583
|
+
);
|
|
581
584
|
|
|
582
|
-
const text = appendLspDiagnosticText(message,
|
|
585
|
+
const text = appendLspDiagnosticText(message, diagnostics.text);
|
|
583
586
|
return {
|
|
584
587
|
content: [{ type: "text" as const, text }],
|
|
585
588
|
details: {
|
|
586
589
|
...details,
|
|
587
590
|
pendant: {
|
|
588
|
-
subtitle: formatSubtitlePath(
|
|
591
|
+
subtitle: formatSubtitlePath(
|
|
592
|
+
ctx.cwd,
|
|
593
|
+
absolutePath,
|
|
594
|
+
diagnostics.errorCount,
|
|
595
|
+
diagnostics.warningCount,
|
|
596
|
+
),
|
|
589
597
|
},
|
|
590
598
|
},
|
|
591
599
|
};
|
|
@@ -641,8 +649,9 @@ function registerWriteTool(pi: ExtensionAPI, getService: () => LspService): void
|
|
|
641
649
|
});
|
|
642
650
|
const dir = dirname(absolutePath);
|
|
643
651
|
|
|
644
|
-
const [message, details,
|
|
645
|
-
|
|
652
|
+
const [message, details, diagnostics] = await withFileMutationQueue(
|
|
653
|
+
absolutePath,
|
|
654
|
+
async () => {
|
|
646
655
|
signal?.throwIfAborted();
|
|
647
656
|
|
|
648
657
|
// opencode: desiredBom = source.bom || next.bom —— 保留原文件 BOM,
|
|
@@ -665,28 +674,26 @@ function registerWriteTool(pi: ExtensionAPI, getService: () => LspService): void
|
|
|
665
674
|
await mkdir(dir, { recursive: true });
|
|
666
675
|
signal?.throwIfAborted();
|
|
667
676
|
await writeFile(absolutePath, desiredBom + nextText, "utf8");
|
|
668
|
-
const {
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
warningCount,
|
|
672
|
-
} = await getService().lspDiagnosticsForFile(absolutePath, ctx.cwd, { signal });
|
|
677
|
+
const diagnostics = await getService().lspDiagnosticsForFile(absolutePath, ctx.cwd, {
|
|
678
|
+
signal,
|
|
679
|
+
});
|
|
673
680
|
|
|
674
|
-
return [
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
diagnosticText,
|
|
678
|
-
errorCount,
|
|
679
|
-
warningCount,
|
|
680
|
-
] as const;
|
|
681
|
-
});
|
|
681
|
+
return ["Wrote file successfully.", {}, diagnostics] as const;
|
|
682
|
+
},
|
|
683
|
+
);
|
|
682
684
|
|
|
683
|
-
const text = appendLspDiagnosticText(message,
|
|
685
|
+
const text = appendLspDiagnosticText(message, diagnostics.text);
|
|
684
686
|
return {
|
|
685
687
|
content: [{ type: "text" as const, text }],
|
|
686
688
|
details: {
|
|
687
689
|
...details,
|
|
688
690
|
pendant: {
|
|
689
|
-
subtitle: formatSubtitlePath(
|
|
691
|
+
subtitle: formatSubtitlePath(
|
|
692
|
+
ctx.cwd,
|
|
693
|
+
absolutePath,
|
|
694
|
+
diagnostics.errorCount,
|
|
695
|
+
diagnostics.warningCount,
|
|
696
|
+
),
|
|
690
697
|
},
|
|
691
698
|
},
|
|
692
699
|
};
|