@trim21/personal-pi-extensions 0.1.541 → 0.1.544
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/files.ts +210 -221
- package/src/gh-readonly.ts +38 -14
- package/src/lib/lsp/diagnostic.ts +41 -14
- package/src/lib/lsp/lsp.ts +6 -9
- package/src/opencode/files.ts +46 -45
package/package.json
CHANGED
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";
|
|
@@ -333,24 +333,25 @@ export function registerFileTools(
|
|
|
333
333
|
const snapshot = snapshotOf(buffer);
|
|
334
334
|
state.reads.set(key, snapshot);
|
|
335
335
|
// 与 Edit / Write 同一条驻留路径:didOpen 后等待该文件的诊断并报告
|
|
336
|
-
const {
|
|
337
|
-
text: diagnosticText,
|
|
338
|
-
errorCount,
|
|
339
|
-
warningCount,
|
|
340
|
-
} = await getService().lspDiagnosticsForFile(filePath, ctx.cwd, {
|
|
336
|
+
const diagnostics = await getService().lspDiagnosticsForFile(filePath, ctx.cwd, {
|
|
341
337
|
notify: (message, level) => ctx.ui.notify(message, level),
|
|
342
338
|
});
|
|
343
339
|
return {
|
|
344
340
|
content: [
|
|
345
341
|
{
|
|
346
342
|
type: "text",
|
|
347
|
-
text: appendLspDiagnosticText(formatted.text,
|
|
343
|
+
text: appendLspDiagnosticText(formatted.text, diagnostics.text),
|
|
348
344
|
},
|
|
349
345
|
],
|
|
350
346
|
details: {
|
|
351
347
|
reads: { [key]: snapshot },
|
|
352
348
|
pendant: {
|
|
353
|
-
subtitle: formatSubtitlePath(
|
|
349
|
+
subtitle: formatSubtitlePath(
|
|
350
|
+
ctx.cwd,
|
|
351
|
+
filePath,
|
|
352
|
+
diagnostics.errorCount,
|
|
353
|
+
diagnostics.warningCount,
|
|
354
|
+
),
|
|
354
355
|
title: "Read",
|
|
355
356
|
} satisfies ToolPendant,
|
|
356
357
|
},
|
|
@@ -394,178 +395,167 @@ export function registerFileTools(
|
|
|
394
395
|
replaceAll: params.replace_all,
|
|
395
396
|
},
|
|
396
397
|
});
|
|
397
|
-
const [message, details,
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
const content = await readFile(filePath, "utf8");
|
|
415
|
-
if (content.trim() !== "") {
|
|
416
|
-
throw new Error("Cannot create new file - file already exists.");
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
} catch (error) {
|
|
420
|
-
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
421
|
-
exists = false;
|
|
422
|
-
} else {
|
|
423
|
-
throw error;
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
if (!exists) await mkdir(dirname(filePath), { recursive: true });
|
|
427
|
-
await writeFile(filePath, newString, "utf8");
|
|
428
|
-
const snapshot = snapshotOf(newString);
|
|
429
|
-
const key = await readStateKey(filePath);
|
|
430
|
-
state.reads.set(key, snapshot);
|
|
431
|
-
const diff = generateDiffString("", convertLeadingTabsToSpaces(newString));
|
|
432
|
-
signal?.throwIfAborted();
|
|
433
|
-
const {
|
|
434
|
-
text: diagnosticText,
|
|
435
|
-
errorCount,
|
|
436
|
-
warningCount,
|
|
437
|
-
} = await getService().lspDiagnosticsForFile(filePath, ctx.cwd, {
|
|
438
|
-
notify: (message, level) => ctx.ui.notify(message, level),
|
|
439
|
-
signal,
|
|
440
|
-
});
|
|
441
|
-
return [
|
|
442
|
-
`The file ${filePath} has been updated successfully.`,
|
|
443
|
-
{
|
|
444
|
-
diff: diff.diff,
|
|
445
|
-
patch: generateUnifiedPatch(filePath, "", convertLeadingTabsToSpaces(newString)),
|
|
446
|
-
firstChangedLine: diff.firstChangedLine,
|
|
447
|
-
reads: { [key]: snapshot },
|
|
448
|
-
},
|
|
449
|
-
diagnosticText,
|
|
450
|
-
errorCount,
|
|
451
|
-
warningCount,
|
|
452
|
-
];
|
|
453
|
-
}
|
|
454
|
-
const replaceAll = params.replace_all ?? false;
|
|
455
|
-
// 防止 OOM 的大文件检查(对齐 Claude Code)
|
|
456
|
-
try {
|
|
457
|
-
const { size } = await stat(filePath);
|
|
458
|
-
if (size > MAX_EDIT_FILE_SIZE) {
|
|
459
|
-
throw new Error(
|
|
460
|
-
`File is too large to edit (${formatFileSize(size)}). Maximum editable file size is ${formatFileSize(MAX_EDIT_FILE_SIZE)}.`,
|
|
461
|
-
);
|
|
462
|
-
}
|
|
463
|
-
} catch (error) {
|
|
464
|
-
if (!(error instanceof Error) || !("code" in error) || error.code !== "ENOENT") {
|
|
465
|
-
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.");
|
|
466
415
|
}
|
|
467
416
|
}
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
}
|
|
472
|
-
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
473
|
-
const suggestion = await didYouMean(filePath, ctx.cwd);
|
|
474
|
-
throw new Error(
|
|
475
|
-
`File does not exist. Note: your current working directory is ${ctx.cwd}.${suggestion ? ` Did you mean ${suggestion}?` : ""}`,
|
|
476
|
-
{ cause: error },
|
|
477
|
-
);
|
|
478
|
-
}
|
|
417
|
+
} catch (error) {
|
|
418
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
419
|
+
exists = false;
|
|
420
|
+
} else {
|
|
479
421
|
throw error;
|
|
480
422
|
}
|
|
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
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
const
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
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,
|
|
529
537
|
convertLeadingTabsToSpaces(original),
|
|
530
538
|
convertLeadingTabsToSpaces(restored),
|
|
531
|
-
)
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
: `The file ${filePath} has been updated successfully.`;
|
|
535
|
-
signal?.throwIfAborted();
|
|
536
|
-
const {
|
|
537
|
-
text: diagnosticText,
|
|
538
|
-
errorCount,
|
|
539
|
-
warningCount,
|
|
540
|
-
} = await getService().lspDiagnosticsForFile(filePath, ctx.cwd, {
|
|
541
|
-
notify: (message, level) => ctx.ui.notify(message, level),
|
|
542
|
-
});
|
|
543
|
-
return [
|
|
544
|
-
text,
|
|
545
|
-
{
|
|
546
|
-
diff: diff.diff,
|
|
547
|
-
patch: generateUnifiedPatch(
|
|
548
|
-
filePath,
|
|
549
|
-
convertLeadingTabsToSpaces(original),
|
|
550
|
-
convertLeadingTabsToSpaces(restored),
|
|
551
|
-
),
|
|
552
|
-
firstChangedLine: diff.firstChangedLine,
|
|
553
|
-
reads: { [key]: snapshot },
|
|
554
|
-
},
|
|
555
|
-
diagnosticText,
|
|
556
|
-
errorCount,
|
|
557
|
-
warningCount,
|
|
558
|
-
];
|
|
539
|
+
),
|
|
540
|
+
firstChangedLine: diff.firstChangedLine,
|
|
541
|
+
reads: { [key]: snapshot },
|
|
559
542
|
},
|
|
560
|
-
|
|
543
|
+
diagnostics,
|
|
544
|
+
];
|
|
545
|
+
});
|
|
561
546
|
|
|
562
|
-
const text = appendLspDiagnosticText(message,
|
|
547
|
+
const text = appendLspDiagnosticText(message, diagnostics.text);
|
|
563
548
|
return {
|
|
564
549
|
content: [{ type: "text" as const, text }],
|
|
565
550
|
details: {
|
|
566
551
|
...details,
|
|
567
552
|
pendant: {
|
|
568
|
-
subtitle: formatSubtitlePath(
|
|
553
|
+
subtitle: formatSubtitlePath(
|
|
554
|
+
ctx.cwd,
|
|
555
|
+
filePath,
|
|
556
|
+
diagnostics.errorCount,
|
|
557
|
+
diagnostics.warningCount,
|
|
558
|
+
),
|
|
569
559
|
title: "Edit",
|
|
570
560
|
} satisfies ToolPendant,
|
|
571
561
|
},
|
|
@@ -600,66 +590,65 @@ export function registerFileTools(
|
|
|
600
590
|
absolutePath: filePath,
|
|
601
591
|
change: { oldText: "", newText: params.content },
|
|
602
592
|
});
|
|
603
|
-
const [message, details,
|
|
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
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
{
|
|
645
|
-
diff: diff.diff,
|
|
646
|
-
patch: generateUnifiedPatch(filePath, original ?? "", params.content),
|
|
647
|
-
firstChangedLine: diff.firstChangedLine,
|
|
648
|
-
reads: { [resolvedKey]: snapshot },
|
|
649
|
-
},
|
|
650
|
-
diagnosticText,
|
|
651
|
-
errorCount,
|
|
652
|
-
warningCount,
|
|
653
|
-
];
|
|
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 },
|
|
654
634
|
},
|
|
655
|
-
|
|
635
|
+
diagnostics,
|
|
636
|
+
];
|
|
637
|
+
});
|
|
656
638
|
|
|
657
|
-
const text = appendLspDiagnosticText(message,
|
|
639
|
+
const text = appendLspDiagnosticText(message, diagnostics.text);
|
|
658
640
|
return {
|
|
659
641
|
content: [{ type: "text" as const, text }],
|
|
660
642
|
details: {
|
|
661
643
|
...details,
|
|
662
|
-
pendant: {
|
|
644
|
+
pendant: {
|
|
645
|
+
subtitle: formatSubtitlePath(
|
|
646
|
+
ctx.cwd,
|
|
647
|
+
filePath,
|
|
648
|
+
diagnostics.errorCount,
|
|
649
|
+
diagnostics.warningCount,
|
|
650
|
+
),
|
|
651
|
+
},
|
|
663
652
|
},
|
|
664
653
|
};
|
|
665
654
|
},
|
package/src/gh-readonly.ts
CHANGED
|
@@ -425,6 +425,11 @@ export function stepsDetail(
|
|
|
425
425
|
// 缓存检查避免重复网络请求。闭包状态不与其他扩展共享,key 无需全局前缀。
|
|
426
426
|
const seq = createSeqState();
|
|
427
427
|
|
|
428
|
+
/** Absolute path of the raw job log cache file written by `getJobLog`. */
|
|
429
|
+
export function jobLogPath(runId: string, jobId: number): string {
|
|
430
|
+
return join(homedir(), ".cache", "pi", "ci-logs", runId, `${jobId}.log`);
|
|
431
|
+
}
|
|
432
|
+
|
|
428
433
|
async function getJobLog(
|
|
429
434
|
runId: string,
|
|
430
435
|
jobId: number,
|
|
@@ -433,8 +438,8 @@ async function getJobLog(
|
|
|
433
438
|
cwd: string | undefined,
|
|
434
439
|
input?: unknown,
|
|
435
440
|
): Promise<string> {
|
|
436
|
-
const
|
|
437
|
-
const
|
|
441
|
+
const cacheFile = jobLogPath(runId, jobId);
|
|
442
|
+
const cacheDir = dirname(cacheFile);
|
|
438
443
|
|
|
439
444
|
// 同一 runId:jobId 的请求串行执行:后一个进入时缓存已写入,直接命中缓存,
|
|
440
445
|
// 不会重复发网络请求;串行也保证不会有两个并发写同一 cache 文件。
|
|
@@ -682,6 +687,15 @@ export function stripAnsi(text: string): string {
|
|
|
682
687
|
return text.replace(/^\uFEFF/, "").replaceAll(ANSI_RE, "");
|
|
683
688
|
}
|
|
684
689
|
|
|
690
|
+
/**
|
|
691
|
+
* Appended to `read-github-ci-logs` content so the raw log file path reaches
|
|
692
|
+
* the model: tool `details` is not part of the LLM context, so the path has to
|
|
693
|
+
* live in the returned text.
|
|
694
|
+
*/
|
|
695
|
+
function rawLogNotice(path: string): string {
|
|
696
|
+
return `[Raw job log (whole job): ${path}]`;
|
|
697
|
+
}
|
|
698
|
+
|
|
685
699
|
export interface StepLogParams {
|
|
686
700
|
runId: string;
|
|
687
701
|
job?: string;
|
|
@@ -772,6 +786,7 @@ export async function renderStepLog(
|
|
|
772
786
|
});
|
|
773
787
|
|
|
774
788
|
const rawLog = await fetchJobLog(targetJob.id);
|
|
789
|
+
const rawLogFile = jobLogPath(params.runId, targetJob.id);
|
|
775
790
|
|
|
776
791
|
const stepLog = extractStepFromLog(rawLog, stepNum, targetJob.steps);
|
|
777
792
|
if (stepLog === null) {
|
|
@@ -779,10 +794,10 @@ export async function renderStepLog(
|
|
|
779
794
|
content: [
|
|
780
795
|
{
|
|
781
796
|
type: "text",
|
|
782
|
-
text: `Could not extract step ${stepNum} from job "${targetJob.name}" logs. The log may be malformed or empty
|
|
797
|
+
text: `Could not extract step ${stepNum} from job "${targetJob.name}" logs. The log may be malformed or empty — read the raw file instead.\n\n${rawLogNotice(rawLogFile)}`,
|
|
783
798
|
},
|
|
784
799
|
],
|
|
785
|
-
details: {},
|
|
800
|
+
details: { rawLogFile },
|
|
786
801
|
};
|
|
787
802
|
}
|
|
788
803
|
|
|
@@ -792,11 +807,12 @@ export async function renderStepLog(
|
|
|
792
807
|
if (full) {
|
|
793
808
|
const fullLines = clean.split("\n").length;
|
|
794
809
|
return {
|
|
795
|
-
content: [{ type: "text", text: clean }],
|
|
810
|
+
content: [{ type: "text", text: `${clean}\n\n${rawLogNotice(rawLogFile)}` }],
|
|
796
811
|
details: {
|
|
797
812
|
summary: `Step ${stepNum} — ${targetJob.name} / ${found.name}: complete output (${fullLines} lines)`,
|
|
798
813
|
truncated: false,
|
|
799
814
|
full: true,
|
|
815
|
+
rawLogFile,
|
|
800
816
|
job: {
|
|
801
817
|
name: targetJob.name,
|
|
802
818
|
conclusion: targetJob.conclusion,
|
|
@@ -818,10 +834,10 @@ export async function renderStepLog(
|
|
|
818
834
|
content: [
|
|
819
835
|
{
|
|
820
836
|
type: "text",
|
|
821
|
-
text: `Offset ${offset} exceeds step log length (${totalLines} lines)
|
|
837
|
+
text: `Offset ${offset} exceeds step log length (${totalLines} lines).\n\n${rawLogNotice(rawLogFile)}`,
|
|
822
838
|
},
|
|
823
839
|
],
|
|
824
|
-
details: {},
|
|
840
|
+
details: { rawLogFile },
|
|
825
841
|
};
|
|
826
842
|
}
|
|
827
843
|
logToShow = clean
|
|
@@ -837,10 +853,11 @@ export async function renderStepLog(
|
|
|
837
853
|
const shownLines = text.split("\n").length;
|
|
838
854
|
|
|
839
855
|
return {
|
|
840
|
-
content: [{ type: "text", text }],
|
|
856
|
+
content: [{ type: "text", text: `${text}\n\n${rawLogNotice(rawLogFile)}` }],
|
|
841
857
|
details: {
|
|
842
858
|
summary: `Step ${stepNum} — ${targetJob.name} / ${found.name}: ${shownLines} of ${totalLines} lines${tr ? " (truncated)" : ""}`,
|
|
843
859
|
truncated: tr,
|
|
860
|
+
rawLogFile,
|
|
844
861
|
job: {
|
|
845
862
|
name: targetJob.name,
|
|
846
863
|
conclusion: targetJob.conclusion,
|
|
@@ -869,15 +886,18 @@ export interface JobLogsStep {
|
|
|
869
886
|
|
|
870
887
|
export interface JobLogsOutput {
|
|
871
888
|
name: string;
|
|
889
|
+
/** Absolute path of this job's raw log on disk, set when its log was fetched. */
|
|
890
|
+
log_file?: string;
|
|
872
891
|
steps: JobLogsStep[];
|
|
873
892
|
}
|
|
874
893
|
|
|
875
894
|
/**
|
|
876
895
|
* Render the result of `read-github-ci-logs` without a `step`: a JSON array of
|
|
877
|
-
* jobs `[{ name, steps: [{ name, output? }] }]`. Every step is listed
|
|
878
|
-
* only failed steps carry an `output` (their log as plain text).
|
|
879
|
-
*
|
|
880
|
-
*
|
|
896
|
+
* jobs `[{ name, log_file?, steps: [{ name, output? }] }]`. Every step is listed
|
|
897
|
+
* by name; only failed steps carry an `output` (their log as plain text).
|
|
898
|
+
* `log_file` is the raw on-disk log path for jobs whose log was fetched. `job`
|
|
899
|
+
* is an optional filter; `offset`/`limit` control the size of each `output`
|
|
900
|
+
* text. Pure — no network, no `gh`.
|
|
881
901
|
*/
|
|
882
902
|
export async function renderJobLogs(
|
|
883
903
|
params: JobLogsParams,
|
|
@@ -961,7 +981,11 @@ export async function renderJobLogs(
|
|
|
961
981
|
}
|
|
962
982
|
}
|
|
963
983
|
|
|
964
|
-
output.push({
|
|
984
|
+
output.push({
|
|
985
|
+
name: j.name,
|
|
986
|
+
...(rawLog !== null && { log_file: jobLogPath(params.runId, j.id) }),
|
|
987
|
+
steps,
|
|
988
|
+
});
|
|
965
989
|
}
|
|
966
990
|
|
|
967
991
|
const totalJobs = output.length;
|
|
@@ -1856,7 +1880,7 @@ export default function ghReadonlyTools(pi: ExtensionAPI) {
|
|
|
1856
1880
|
name: "read-github-ci-logs",
|
|
1857
1881
|
label: "GitHub CI Logs",
|
|
1858
1882
|
description:
|
|
1859
|
-
"Get CI logs from a GitHub Actions workflow run. Without step: returns a JSON array of jobs [{name, steps:[{name, output?}]}] where every step is listed by name and failed steps carry their log as plain text in `output`. With step (requires job): returns that step's complete log as plain text. offset/limit control the size of every expanded output. Use run_id from list-github-workflow-runs. Note: queued jobs have no logs yet; use watch-github-run to wait for completion. Set full=true for complete untruncated outputs (every step when step is omitted; caution: very large outputs consume a lot of LLM context). Set output_file=/path to write the complete log to
|
|
1883
|
+
"Get CI logs from a GitHub Actions workflow run. Without step: returns a JSON array of jobs [{name, log_file?, steps:[{name, output?}]}] where every step is listed by name and failed steps carry their log as plain text in `output`. With step (requires job): returns that step's complete log as plain text. Every fetched job log is also saved raw (timestamps and ANSI kept) to a local file whose path is reported — `log_file` on the job, or a trailing `[Raw job log (whole job): <path>]` line with step — so read/grep that file for the complete, untruncated log. offset/limit control the size of every expanded output. Use run_id from list-github-workflow-runs. Note: queued jobs have no logs yet; use watch-github-run to wait for completion. Set full=true for complete untruncated outputs (every step when step is omitted; caution: very large outputs consume a lot of LLM context). Set output_file=/path to write the complete cleaned log to this file instead of returning it (requires job when the run has multiple jobs); the tool returns the file path to read.",
|
|
1860
1884
|
promptSnippet: "Read GitHub CI logs",
|
|
1861
1885
|
parameters: Type.Object({
|
|
1862
1886
|
run_id: Type.Union([Type.Number(), Type.String()], { description: "Workflow run ID" }),
|
|
@@ -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
|
@@ -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,
|
|
@@ -398,12 +398,12 @@ export interface LspService {
|
|
|
398
398
|
options?: LspRequestOptions,
|
|
399
399
|
): Promise<void>;
|
|
400
400
|
diagnostics(): Promise<Record<string, Diagnostic[]>>;
|
|
401
|
-
/** read / edit / write 用:等待文档诊断并返回该文件的 ERROR / WARN
|
|
401
|
+
/** read / edit / write 用:等待文档诊断并返回该文件的 ERROR / WARN 报告与计数。 */
|
|
402
402
|
lspDiagnosticsForFile(
|
|
403
403
|
file: string,
|
|
404
404
|
cwd: string,
|
|
405
405
|
options?: LspRequestOptions,
|
|
406
|
-
): Promise<
|
|
406
|
+
): Promise<DiagnosticReport>;
|
|
407
407
|
/**
|
|
408
408
|
* 符号重命名:只面向 kind 为 "language" 的服务器(linter 不参与符号级
|
|
409
409
|
* 功能);多 client 按配置顺序取第一个成功结果,全部失败时抛聚合错误。
|
|
@@ -929,15 +929,12 @@ export function createLspService(
|
|
|
929
929
|
file: string,
|
|
930
930
|
cwd: string,
|
|
931
931
|
options?: LspRequestOptions,
|
|
932
|
-
): Promise<
|
|
933
|
-
if (options?.signal?.aborted) return
|
|
932
|
+
): Promise<DiagnosticReport> {
|
|
933
|
+
if (options?.signal?.aborted) return EMPTY_DIAGNOSTIC_REPORT;
|
|
934
934
|
await touchFile(file, cwd, "document", options);
|
|
935
935
|
const all = await diagnostics();
|
|
936
936
|
const normalized = normalize(file);
|
|
937
|
-
|
|
938
|
-
const errorCount = issues.filter((item) => (item.severity ?? 1) === 1).length;
|
|
939
|
-
const warningCount = issues.filter((item) => item.severity === 2).length;
|
|
940
|
-
return { text: report(normalized, issues), errorCount, warningCount };
|
|
937
|
+
return report(normalized, all[normalized] ?? []);
|
|
941
938
|
}
|
|
942
939
|
|
|
943
940
|
/** 符号重命名:只面向 language 类服务器;按配置顺序取第一个成功结果。 */
|
package/src/opencode/files.ts
CHANGED
|
@@ -432,15 +432,13 @@ function registerReadTool(pi: ExtensionAPI, getService: () => LspService): void
|
|
|
432
432
|
}
|
|
433
433
|
|
|
434
434
|
// opencode: 与 edit / write 同一条驻留路径:didOpen 后等待该文件的诊断并报告
|
|
435
|
-
const {
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
warningCount,
|
|
439
|
-
} = await getService().lspDiagnosticsForFile(absolutePath, ctx.cwd, { signal });
|
|
435
|
+
const diagnostics = await getService().lspDiagnosticsForFile(absolutePath, ctx.cwd, {
|
|
436
|
+
signal,
|
|
437
|
+
});
|
|
440
438
|
content = [
|
|
441
439
|
{
|
|
442
440
|
type: "text",
|
|
443
|
-
text: appendLspDiagnosticText(outputText,
|
|
441
|
+
text: appendLspDiagnosticText(outputText, diagnostics.text),
|
|
444
442
|
},
|
|
445
443
|
];
|
|
446
444
|
|
|
@@ -449,7 +447,12 @@ function registerReadTool(pi: ExtensionAPI, getService: () => LspService): void
|
|
|
449
447
|
details: {
|
|
450
448
|
...details,
|
|
451
449
|
pendant: {
|
|
452
|
-
subtitle: formatSubtitlePath(
|
|
450
|
+
subtitle: formatSubtitlePath(
|
|
451
|
+
ctx.cwd,
|
|
452
|
+
absolutePath,
|
|
453
|
+
diagnostics.errorCount,
|
|
454
|
+
diagnostics.warningCount,
|
|
455
|
+
),
|
|
453
456
|
},
|
|
454
457
|
},
|
|
455
458
|
};
|
|
@@ -503,8 +506,9 @@ function registerEditTool(pi: ExtensionAPI, getService: () => LspService): void
|
|
|
503
506
|
change: { oldText: oldString, newText: newString, replaceAll },
|
|
504
507
|
});
|
|
505
508
|
|
|
506
|
-
const [message, details,
|
|
507
|
-
|
|
509
|
+
const [message, details, diagnostics] = await withFileMutationQueue(
|
|
510
|
+
absolutePath,
|
|
511
|
+
async () => {
|
|
508
512
|
signal?.throwIfAborted();
|
|
509
513
|
|
|
510
514
|
// opencode: 前置校验,先于空 oldString 分支
|
|
@@ -529,17 +533,13 @@ function registerEditTool(pi: ExtensionAPI, getService: () => LspService): void
|
|
|
529
533
|
await mkdir(dirname(absolutePath), { recursive: true });
|
|
530
534
|
signal?.throwIfAborted();
|
|
531
535
|
await writeFile(absolutePath, newString, "utf8");
|
|
532
|
-
const {
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
warningCount,
|
|
536
|
-
} = await getService().lspDiagnosticsForFile(absolutePath, ctx.cwd, { signal });
|
|
536
|
+
const diagnostics = await getService().lspDiagnosticsForFile(absolutePath, ctx.cwd, {
|
|
537
|
+
signal,
|
|
538
|
+
});
|
|
537
539
|
return [
|
|
538
540
|
"Edit applied successfully.",
|
|
539
541
|
{ diff: "", patch: "", firstChangedLine: 0 },
|
|
540
|
-
|
|
541
|
-
errorCount,
|
|
542
|
-
warningCount,
|
|
542
|
+
diagnostics,
|
|
543
543
|
] as const;
|
|
544
544
|
}
|
|
545
545
|
|
|
@@ -571,27 +571,29 @@ function registerEditTool(pi: ExtensionAPI, getService: () => LspService): void
|
|
|
571
571
|
const diffNew = normalizeToLF(applied.contentNew);
|
|
572
572
|
const diffResult = generateDiffString(diffOld, diffNew);
|
|
573
573
|
const patch = generateUnifiedPatch(filePath, diffOld, diffNew);
|
|
574
|
-
const {
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
warningCount,
|
|
578
|
-
} = await getService().lspDiagnosticsForFile(absolutePath, ctx.cwd, { signal });
|
|
574
|
+
const diagnostics = await getService().lspDiagnosticsForFile(absolutePath, ctx.cwd, {
|
|
575
|
+
signal,
|
|
576
|
+
});
|
|
579
577
|
return [
|
|
580
578
|
"Edit applied successfully.",
|
|
581
579
|
{ diff: diffResult.diff, patch, firstChangedLine: diffResult.firstChangedLine },
|
|
582
|
-
|
|
583
|
-
errorCount,
|
|
584
|
-
warningCount,
|
|
580
|
+
diagnostics,
|
|
585
581
|
] as const;
|
|
586
|
-
}
|
|
582
|
+
},
|
|
583
|
+
);
|
|
587
584
|
|
|
588
|
-
const text = appendLspDiagnosticText(message,
|
|
585
|
+
const text = appendLspDiagnosticText(message, diagnostics.text);
|
|
589
586
|
return {
|
|
590
587
|
content: [{ type: "text" as const, text }],
|
|
591
588
|
details: {
|
|
592
589
|
...details,
|
|
593
590
|
pendant: {
|
|
594
|
-
subtitle: formatSubtitlePath(
|
|
591
|
+
subtitle: formatSubtitlePath(
|
|
592
|
+
ctx.cwd,
|
|
593
|
+
absolutePath,
|
|
594
|
+
diagnostics.errorCount,
|
|
595
|
+
diagnostics.warningCount,
|
|
596
|
+
),
|
|
595
597
|
},
|
|
596
598
|
},
|
|
597
599
|
};
|
|
@@ -647,8 +649,9 @@ function registerWriteTool(pi: ExtensionAPI, getService: () => LspService): void
|
|
|
647
649
|
});
|
|
648
650
|
const dir = dirname(absolutePath);
|
|
649
651
|
|
|
650
|
-
const [message, details,
|
|
651
|
-
|
|
652
|
+
const [message, details, diagnostics] = await withFileMutationQueue(
|
|
653
|
+
absolutePath,
|
|
654
|
+
async () => {
|
|
652
655
|
signal?.throwIfAborted();
|
|
653
656
|
|
|
654
657
|
// opencode: desiredBom = source.bom || next.bom —— 保留原文件 BOM,
|
|
@@ -671,28 +674,26 @@ function registerWriteTool(pi: ExtensionAPI, getService: () => LspService): void
|
|
|
671
674
|
await mkdir(dir, { recursive: true });
|
|
672
675
|
signal?.throwIfAborted();
|
|
673
676
|
await writeFile(absolutePath, desiredBom + nextText, "utf8");
|
|
674
|
-
const {
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
warningCount,
|
|
678
|
-
} = await getService().lspDiagnosticsForFile(absolutePath, ctx.cwd, { signal });
|
|
677
|
+
const diagnostics = await getService().lspDiagnosticsForFile(absolutePath, ctx.cwd, {
|
|
678
|
+
signal,
|
|
679
|
+
});
|
|
679
680
|
|
|
680
|
-
return [
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
diagnosticText,
|
|
684
|
-
errorCount,
|
|
685
|
-
warningCount,
|
|
686
|
-
] as const;
|
|
687
|
-
});
|
|
681
|
+
return ["Wrote file successfully.", {}, diagnostics] as const;
|
|
682
|
+
},
|
|
683
|
+
);
|
|
688
684
|
|
|
689
|
-
const text = appendLspDiagnosticText(message,
|
|
685
|
+
const text = appendLspDiagnosticText(message, diagnostics.text);
|
|
690
686
|
return {
|
|
691
687
|
content: [{ type: "text" as const, text }],
|
|
692
688
|
details: {
|
|
693
689
|
...details,
|
|
694
690
|
pendant: {
|
|
695
|
-
subtitle: formatSubtitlePath(
|
|
691
|
+
subtitle: formatSubtitlePath(
|
|
692
|
+
ctx.cwd,
|
|
693
|
+
absolutePath,
|
|
694
|
+
diagnostics.errorCount,
|
|
695
|
+
diagnostics.warningCount,
|
|
696
|
+
),
|
|
696
697
|
},
|
|
697
698
|
},
|
|
698
699
|
};
|