@oh-my-pi/pi-coding-agent 13.10.0 → 13.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +52 -0
- package/package.json +7 -7
- package/src/commit/agentic/agent.ts +3 -1
- package/src/commit/agentic/index.ts +7 -1
- package/src/commit/analysis/conventional.ts +5 -1
- package/src/commit/analysis/summary.ts +5 -1
- package/src/commit/changelog/generate.ts +5 -1
- package/src/commit/changelog/index.ts +4 -0
- package/src/commit/map-reduce/index.ts +5 -0
- package/src/commit/map-reduce/map-phase.ts +17 -2
- package/src/commit/map-reduce/reduce-phase.ts +5 -1
- package/src/commit/model-selection.ts +38 -26
- package/src/commit/pipeline.ts +22 -11
- package/src/config/settings-schema.ts +20 -0
- package/src/config.ts +10 -3
- package/src/discovery/helpers.ts +7 -3
- package/src/internal-urls/docs-index.generated.ts +1 -1
- package/src/lsp/index.ts +4 -4
- package/src/lsp/utils.ts +81 -0
- package/src/main.ts +25 -14
- package/src/mcp/manager.ts +40 -2
- package/src/mcp/oauth-flow.ts +41 -0
- package/src/mcp/transports/http.ts +23 -0
- package/src/mcp/types.ts +6 -0
- package/src/modes/components/mcp-add-wizard.ts +12 -0
- package/src/modes/components/settings-defs.ts +2 -1
- package/src/modes/components/todo-reminder.ts +8 -1
- package/src/modes/controllers/command-controller.ts +75 -3
- package/src/modes/controllers/input-controller.ts +2 -3
- package/src/modes/controllers/mcp-command-controller.ts +9 -1
- package/src/modes/interactive-mode.ts +11 -7
- package/src/modes/theme/theme.ts +30 -27
- package/src/modes/types.ts +2 -1
- package/src/patch/hashline.ts +3 -6
- package/src/prompts/system/eager-todo.md +13 -0
- package/src/prompts/tools/ast-edit.md +1 -1
- package/src/prompts/tools/ast-grep.md +1 -1
- package/src/prompts/tools/find.md +1 -0
- package/src/prompts/tools/grep.md +1 -0
- package/src/prompts/tools/hashline.md +23 -111
- package/src/prompts/tools/todo-write.md +11 -1
- package/src/sdk.ts +1 -1
- package/src/session/agent-session.ts +85 -7
- package/src/session/session-manager.ts +5 -9
- package/src/slash-commands/builtin-registry.ts +10 -2
- package/src/task/executor.ts +9 -18
- package/src/task/index.ts +8 -4
- package/src/task/render.ts +5 -10
- package/src/task/template.ts +4 -1
- package/src/task/types.ts +2 -0
- package/src/tools/ast-edit.ts +26 -7
- package/src/tools/ast-grep.ts +26 -9
- package/src/tools/fetch.ts +36 -5
- package/src/tools/find.ts +13 -64
- package/src/tools/grep.ts +27 -10
- package/src/tools/json-tree.ts +1 -1
- package/src/tools/output-meta.ts +2 -1
- package/src/tools/path-utils.ts +348 -0
- package/src/tools/todo-write.ts +27 -4
- package/src/utils/commit-message-generator.ts +27 -22
- package/src/utils/image-input.ts +1 -1
- package/src/utils/image-resize.ts +4 -4
- package/src/utils/title-generator.ts +36 -23
- package/src/utils/tool-choice.ts +28 -0
- package/src/web/parallel.ts +346 -0
- package/src/web/scrapers/youtube.ts +29 -0
- package/src/web/search/provider.ts +4 -1
- package/src/web/search/providers/parallel.ts +63 -0
- package/src/web/search/types.ts +1 -0
package/src/tools/fetch.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { ptree, truncate } from "@oh-my-pi/pi-utils";
|
|
|
7
7
|
import { type Static, Type } from "@sinclair/typebox";
|
|
8
8
|
import { parseHTML } from "linkedom";
|
|
9
9
|
import { renderPromptTemplate } from "../config/prompt-templates";
|
|
10
|
+
import type { Settings } from "../config/settings";
|
|
10
11
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
11
12
|
import { type Theme, theme } from "../modes/theme/theme";
|
|
12
13
|
import fetchDescription from "../prompts/tools/fetch.md" with { type: "text" };
|
|
@@ -15,6 +16,7 @@ import { renderStatusLine } from "../tui";
|
|
|
15
16
|
import { CachedOutputBlock } from "../tui/output-block";
|
|
16
17
|
import { formatDimensionNote, resizeImage } from "../utils/image-resize";
|
|
17
18
|
import { ensureTool } from "../utils/tools-manager";
|
|
19
|
+
import { extractWithParallel, findParallelApiKey, getParallelExtractContent } from "../web/parallel";
|
|
18
20
|
import { specialHandlers } from "../web/scrapers";
|
|
19
21
|
import type { RenderResult } from "../web/scrapers/types";
|
|
20
22
|
import { finalizeOutput, loadPage, MAX_OUTPUT_CHARS } from "../web/scrapers/types";
|
|
@@ -465,12 +467,13 @@ function parseFeedToMarkdown(content: string, maxItems = 10): string {
|
|
|
465
467
|
}
|
|
466
468
|
|
|
467
469
|
/**
|
|
468
|
-
* Render HTML to markdown using jina, trafilatura, lynx (in order of preference)
|
|
470
|
+
* Render HTML to markdown using Parallel, jina, trafilatura, lynx (in order of preference)
|
|
469
471
|
*/
|
|
470
472
|
async function renderHtmlToText(
|
|
471
473
|
url: string,
|
|
472
474
|
html: string,
|
|
473
475
|
timeout: number,
|
|
476
|
+
settings: Settings,
|
|
474
477
|
userSignal?: AbortSignal,
|
|
475
478
|
): Promise<{ content: string; ok: boolean; method: string }> {
|
|
476
479
|
const signal = ptree.combineSignals(userSignal, timeout * 1000);
|
|
@@ -482,6 +485,28 @@ async function renderHtmlToText(
|
|
|
482
485
|
signal,
|
|
483
486
|
};
|
|
484
487
|
|
|
488
|
+
// Try Parallel extract first when credentials are configured
|
|
489
|
+
if (settings.get("providers.parallelFetch") && (await findParallelApiKey())) {
|
|
490
|
+
try {
|
|
491
|
+
const parallelResult = await extractWithParallel([url], {
|
|
492
|
+
objective: "Extract the main content",
|
|
493
|
+
excerpts: true,
|
|
494
|
+
fullContent: false,
|
|
495
|
+
signal,
|
|
496
|
+
});
|
|
497
|
+
const firstDocument = parallelResult.results[0];
|
|
498
|
+
if (firstDocument) {
|
|
499
|
+
const content = getParallelExtractContent(firstDocument);
|
|
500
|
+
if (content.trim().length > 100 && !isLowQualityOutput(content)) {
|
|
501
|
+
return { content, ok: true, method: "parallel" };
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
} catch {
|
|
505
|
+
// Parallel extract failed, continue to next method
|
|
506
|
+
signal?.throwIfAborted();
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
485
510
|
// Try jina first (reader API)
|
|
486
511
|
try {
|
|
487
512
|
const jinaUrl = `https://r.jina.ai/${url}`;
|
|
@@ -608,7 +633,13 @@ async function handleSpecialUrls(
|
|
|
608
633
|
/**
|
|
609
634
|
* Main render function implementing the full pipeline
|
|
610
635
|
*/
|
|
611
|
-
async function renderUrl(
|
|
636
|
+
async function renderUrl(
|
|
637
|
+
url: string,
|
|
638
|
+
timeout: number,
|
|
639
|
+
raw: boolean,
|
|
640
|
+
settings: Settings,
|
|
641
|
+
signal?: AbortSignal,
|
|
642
|
+
): Promise<FetchRenderResult> {
|
|
612
643
|
const notes: string[] = [];
|
|
613
644
|
const fetchedAt = new Date().toISOString();
|
|
614
645
|
if (signal?.aborted) {
|
|
@@ -714,7 +745,7 @@ async function renderUrl(url: string, timeout: number, raw: boolean, signal?: Ab
|
|
|
714
745
|
}
|
|
715
746
|
|
|
716
747
|
const resized = await resizeImage(
|
|
717
|
-
{ type: "image", data: binary.buffer.toBase64(), mimeType: imageMimeType },
|
|
748
|
+
{ type: "image", data: Buffer.from(binary.buffer).toBase64(), mimeType: imageMimeType },
|
|
718
749
|
{ maxBytes: MAX_INLINE_IMAGE_OUTPUT_BYTES },
|
|
719
750
|
);
|
|
720
751
|
const isDecodedImage =
|
|
@@ -952,7 +983,7 @@ async function renderUrl(url: string, timeout: number, raw: boolean, signal?: Ab
|
|
|
952
983
|
}
|
|
953
984
|
|
|
954
985
|
// 5E: Render HTML with lynx or html2text
|
|
955
|
-
const htmlResult = await renderHtmlToText(finalUrl, rawContent, timeout, signal);
|
|
986
|
+
const htmlResult = await renderHtmlToText(finalUrl, rawContent, timeout, settings, signal);
|
|
956
987
|
if (!htmlResult.ok) {
|
|
957
988
|
notes.push("html rendering failed (lynx/html2text unavailable)");
|
|
958
989
|
const output = finalizeOutput(rawContent);
|
|
@@ -1092,7 +1123,7 @@ export class FetchTool implements AgentTool<typeof fetchSchema, FetchToolDetails
|
|
|
1092
1123
|
throw new ToolAbortError();
|
|
1093
1124
|
}
|
|
1094
1125
|
|
|
1095
|
-
const result = await renderUrl(url, effectiveTimeout, raw, signal);
|
|
1126
|
+
const result = await renderUrl(url, effectiveTimeout, raw, this.session.settings, signal);
|
|
1096
1127
|
const truncation = truncateHead(result.content, {
|
|
1097
1128
|
maxBytes: DEFAULT_MAX_BYTES,
|
|
1098
1129
|
maxLines: FETCH_DEFAULT_MAX_LINES,
|
package/src/tools/find.ts
CHANGED
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
import type { ToolSession } from ".";
|
|
25
25
|
import { applyListLimit } from "./list-limit";
|
|
26
26
|
import { formatFullOutputReference, type OutputMeta } from "./output-meta";
|
|
27
|
-
import { resolveToCwd } from "./path-utils";
|
|
27
|
+
import { parseFindPattern, resolveMultiFindPattern, resolveToCwd } from "./path-utils";
|
|
28
28
|
import { formatCount, formatEmptyMessage, formatErrorMessage, PREVIEW_LIMITS } from "./render-utils";
|
|
29
29
|
import { ToolAbortError, ToolError, throwIfAborted } from "./tool-errors";
|
|
30
30
|
import { toolResult } from "./tool-result";
|
|
@@ -40,56 +40,6 @@ export type FindToolInput = Static<typeof findSchema>;
|
|
|
40
40
|
const DEFAULT_LIMIT = 1000;
|
|
41
41
|
const GLOB_TIMEOUT_MS = 5000;
|
|
42
42
|
|
|
43
|
-
/**
|
|
44
|
-
* Parse a pattern to extract the base directory path and glob pattern.
|
|
45
|
-
* Examples:
|
|
46
|
-
* "src/app/**\/*.tsx" → { basePath: "src/app", globPattern: "**\/*.tsx" }
|
|
47
|
-
* "src/app/*.tsx" → { basePath: "src/app", globPattern: "*.tsx" }
|
|
48
|
-
* "*.ts" → { basePath: ".", globPattern: "**\/*.ts" }
|
|
49
|
-
* "**\/*.json" → { basePath: ".", globPattern: "**\/*.json" }
|
|
50
|
-
* "/abs/path/**\/*.ts" → { basePath: "/abs/path", globPattern: "**\/*.ts" }
|
|
51
|
-
*/
|
|
52
|
-
function parsePatternPath(pattern: string): { basePath: string; globPattern: string } {
|
|
53
|
-
// Find the first segment containing glob characters
|
|
54
|
-
const segments = pattern.split("/");
|
|
55
|
-
const globChars = ["*", "?", "[", "{"];
|
|
56
|
-
|
|
57
|
-
let firstGlobIndex = -1;
|
|
58
|
-
for (let i = 0; i < segments.length; i++) {
|
|
59
|
-
if (globChars.some(c => segments[i].includes(c))) {
|
|
60
|
-
firstGlobIndex = i;
|
|
61
|
-
break;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// No glob characters found - treat as literal path with implicit **/*
|
|
66
|
-
if (firstGlobIndex === -1) {
|
|
67
|
-
// Pattern is a directory path like "src/app" - search recursively in it
|
|
68
|
-
return { basePath: pattern, globPattern: "**/*" };
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// Glob starts at first segment - no base path
|
|
72
|
-
if (firstGlobIndex === 0) {
|
|
73
|
-
// Simple pattern like "*.ts" needs **/ prefix for recursive search
|
|
74
|
-
const needsRecursive = !pattern.startsWith("**/");
|
|
75
|
-
return {
|
|
76
|
-
basePath: ".",
|
|
77
|
-
globPattern: needsRecursive ? `**/${pattern}` : pattern,
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// Split at the glob boundary
|
|
82
|
-
const basePath = segments.slice(0, firstGlobIndex).join("/");
|
|
83
|
-
const globPattern = segments.slice(firstGlobIndex).join("/");
|
|
84
|
-
|
|
85
|
-
return { basePath, globPattern };
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function hasGlobChars(pattern: string): boolean {
|
|
89
|
-
const globChars = ["*", "?", "[", "{"];
|
|
90
|
-
return globChars.some(char => pattern.includes(char));
|
|
91
|
-
}
|
|
92
|
-
|
|
93
43
|
export interface FindToolDetails {
|
|
94
44
|
truncation?: TruncationResult;
|
|
95
45
|
resultLimitReached?: number;
|
|
@@ -149,27 +99,26 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
|
|
|
149
99
|
const { pattern, limit, hidden } = params;
|
|
150
100
|
|
|
151
101
|
return untilAborted(signal, async () => {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
102
|
+
const formatScopePath = (targetPath: string): string => {
|
|
103
|
+
const relative = path.relative(this.session.cwd, targetPath).replace(/\\/g, "/");
|
|
104
|
+
return relative.length === 0 ? "." : relative;
|
|
105
|
+
};
|
|
155
106
|
const normalizedPattern = pattern.trim().replace(/\\/g, "/");
|
|
156
107
|
if (!normalizedPattern) {
|
|
157
108
|
throw new ToolError("Pattern must not be empty");
|
|
158
109
|
}
|
|
159
110
|
|
|
160
|
-
const
|
|
161
|
-
const
|
|
162
|
-
const
|
|
111
|
+
const multiPattern = await resolveMultiFindPattern(normalizedPattern, this.session.cwd);
|
|
112
|
+
const parsedPattern = multiPattern ? null : parseFindPattern(normalizedPattern);
|
|
113
|
+
const hasGlob = multiPattern ? true : (parsedPattern?.hasGlob ?? false);
|
|
114
|
+
const globPattern = multiPattern?.globPattern ?? parsedPattern?.globPattern ?? "**/*";
|
|
115
|
+
const searchPath = resolveToCwd(multiPattern?.basePath ?? parsedPattern?.basePath ?? ".", this.session.cwd);
|
|
116
|
+
const scopePath = multiPattern?.scopePath ?? formatScopePath(searchPath);
|
|
163
117
|
|
|
164
118
|
if (searchPath === "/") {
|
|
165
119
|
throw new ToolError("Searching from root directory '/' is not allowed");
|
|
166
120
|
}
|
|
167
121
|
|
|
168
|
-
const scopePath = (() => {
|
|
169
|
-
const relative = path.relative(this.session.cwd, searchPath).replace(/\\/g, "/");
|
|
170
|
-
return relative.length === 0 ? "." : relative;
|
|
171
|
-
})();
|
|
172
|
-
|
|
173
122
|
const rawLimit = limit ?? DEFAULT_LIMIT;
|
|
174
123
|
const effectiveLimit = Number.isFinite(rawLimit) ? Math.floor(rawLimit) : Number.NaN;
|
|
175
124
|
if (!Number.isFinite(effectiveLimit) || effectiveLimit <= 0) {
|
|
@@ -180,7 +129,7 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
|
|
|
180
129
|
// If custom operations provided with glob, use that instead of fd
|
|
181
130
|
if (this.#customOps?.glob) {
|
|
182
131
|
if (!(await this.#customOps.exists(searchPath))) {
|
|
183
|
-
throw new ToolError(`Path not found: ${
|
|
132
|
+
throw new ToolError(`Path not found: ${scopePath}`);
|
|
184
133
|
}
|
|
185
134
|
|
|
186
135
|
if (!hasGlob && this.#customOps.stat) {
|
|
@@ -245,7 +194,7 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
|
|
|
245
194
|
searchStat = await fs.promises.stat(searchPath);
|
|
246
195
|
} catch (err) {
|
|
247
196
|
if (isEnoent(err)) {
|
|
248
|
-
throw new ToolError(`Path not found: ${
|
|
197
|
+
throw new ToolError(`Path not found: ${scopePath}`);
|
|
249
198
|
}
|
|
250
199
|
throw err;
|
|
251
200
|
}
|
package/src/tools/grep.ts
CHANGED
|
@@ -16,7 +16,13 @@ import { Ellipsis, Hasher, type RenderCache, renderStatusLine, renderTreeList, t
|
|
|
16
16
|
import { resolveFileDisplayMode } from "../utils/file-display-mode";
|
|
17
17
|
import type { ToolSession } from ".";
|
|
18
18
|
import { formatFullOutputReference, type OutputMeta } from "./output-meta";
|
|
19
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
combineSearchGlobs,
|
|
21
|
+
hasGlobPathChars,
|
|
22
|
+
parseSearchPath,
|
|
23
|
+
resolveMultiSearchPath,
|
|
24
|
+
resolveToCwd,
|
|
25
|
+
} from "./path-utils";
|
|
20
26
|
import { formatCount, formatEmptyMessage, formatErrorMessage, PREVIEW_LIMITS } from "./render-utils";
|
|
21
27
|
import { ToolError } from "./tool-errors";
|
|
22
28
|
import { toolResult } from "./tool-result";
|
|
@@ -107,7 +113,12 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
|
|
|
107
113
|
const effectiveMultiline = multiline ?? patternHasNewline;
|
|
108
114
|
|
|
109
115
|
const useHashLines = resolveFileDisplayMode(this.session).hashLines;
|
|
116
|
+
const formatScopePath = (targetPath: string): string => {
|
|
117
|
+
const relative = path.relative(this.session.cwd, targetPath).replace(/\\/g, "/");
|
|
118
|
+
return relative.length === 0 ? "." : relative;
|
|
119
|
+
};
|
|
110
120
|
let searchPath: string;
|
|
121
|
+
let scopePath: string;
|
|
111
122
|
let globFilter = glob?.trim() || undefined;
|
|
112
123
|
const internalRouter = this.session.internalRouter;
|
|
113
124
|
if (searchDir?.trim()) {
|
|
@@ -121,27 +132,33 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
|
|
|
121
132
|
throw new ToolError(`Cannot grep internal URL without a backing file: ${rawPath}`);
|
|
122
133
|
}
|
|
123
134
|
searchPath = resource.sourcePath;
|
|
135
|
+
scopePath = formatScopePath(searchPath);
|
|
124
136
|
} else {
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
globFilter =
|
|
137
|
+
const multiSearchPath = await resolveMultiSearchPath(rawPath, this.session.cwd, globFilter);
|
|
138
|
+
if (multiSearchPath) {
|
|
139
|
+
searchPath = multiSearchPath.basePath;
|
|
140
|
+
globFilter = multiSearchPath.glob;
|
|
141
|
+
scopePath = multiSearchPath.scopePath;
|
|
142
|
+
} else {
|
|
143
|
+
const parsedPath = parseSearchPath(rawPath);
|
|
144
|
+
searchPath = resolveToCwd(parsedPath.basePath, this.session.cwd);
|
|
145
|
+
if (parsedPath.glob) {
|
|
146
|
+
globFilter = combineSearchGlobs(parsedPath.glob, globFilter);
|
|
147
|
+
}
|
|
148
|
+
scopePath = formatScopePath(searchPath);
|
|
129
149
|
}
|
|
130
150
|
}
|
|
131
151
|
} else {
|
|
132
152
|
searchPath = resolveToCwd(".", this.session.cwd);
|
|
153
|
+
scopePath = ".";
|
|
133
154
|
}
|
|
134
|
-
const scopePath = (() => {
|
|
135
|
-
const relative = path.relative(this.session.cwd, searchPath).replace(/\\/g, "/");
|
|
136
|
-
return relative.length === 0 ? "." : relative;
|
|
137
|
-
})();
|
|
138
155
|
|
|
139
156
|
let isDirectory: boolean;
|
|
140
157
|
try {
|
|
141
158
|
const stat = await Bun.file(searchPath).stat();
|
|
142
159
|
isDirectory = stat.isDirectory();
|
|
143
160
|
} catch {
|
|
144
|
-
throw new ToolError(`Path not found: ${
|
|
161
|
+
throw new ToolError(`Path not found: ${scopePath}`);
|
|
145
162
|
}
|
|
146
163
|
|
|
147
164
|
const effectiveOutputMode = "content";
|
package/src/tools/json-tree.ts
CHANGED
|
@@ -14,7 +14,7 @@ export const JSON_TREE_SCALAR_LEN_COLLAPSED = 60;
|
|
|
14
14
|
export const JSON_TREE_SCALAR_LEN_EXPANDED = 2000;
|
|
15
15
|
|
|
16
16
|
/** Keys injected by the harness that should not be displayed to users */
|
|
17
|
-
const HIDDEN_ARG_KEYS = new Set([INTENT_FIELD]);
|
|
17
|
+
const HIDDEN_ARG_KEYS = new Set([INTENT_FIELD, "__partialJson"]);
|
|
18
18
|
|
|
19
19
|
/** Strip harness-internal keys from tool args for display */
|
|
20
20
|
export function stripInternalArgs(args: Record<string, unknown>): Record<string, unknown> {
|
package/src/tools/output-meta.ts
CHANGED
|
@@ -12,6 +12,7 @@ import type {
|
|
|
12
12
|
AgentToolUpdateCallback,
|
|
13
13
|
} from "@oh-my-pi/pi-agent-core";
|
|
14
14
|
import type { ImageContent, TextContent } from "@oh-my-pi/pi-ai";
|
|
15
|
+
import { formatGroupedDiagnosticMessages } from "../lsp/utils";
|
|
15
16
|
import type { Theme } from "../modes/theme/theme";
|
|
16
17
|
import type { OutputSummary, TruncationResult } from "../session/streaming-output";
|
|
17
18
|
import { formatBytes, wrapBrackets } from "./render-utils";
|
|
@@ -386,7 +387,7 @@ export function formatOutputNotice(meta: OutputMeta | undefined): string {
|
|
|
386
387
|
let diagnosticsNotice = "";
|
|
387
388
|
if (meta.diagnostics && meta.diagnostics.messages.length > 0) {
|
|
388
389
|
const d = meta.diagnostics;
|
|
389
|
-
diagnosticsNotice = `\n\nLSP Diagnostics (${d.summary}):\n
|
|
390
|
+
diagnosticsNotice = `\n\nLSP Diagnostics (${d.summary}):\n${formatGroupedDiagnosticMessages(d.messages)}`;
|
|
390
391
|
}
|
|
391
392
|
|
|
392
393
|
const notice = parts.length ? `\n\n[${parts.join(". ")}]` : "";
|