@sean.holung/minicode 0.3.5 → 0.3.7
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/README.md +22 -45
- package/dist/scripts/run-benchmarks.js +1 -0
- package/dist/src/agent/config.js +53 -66
- package/dist/src/agent/editable-config.js +56 -58
- package/dist/src/agent/home-env.js +74 -0
- package/dist/src/cli/config-slash-command.js +15 -13
- package/dist/src/serve/agent-bridge.js +87 -28
- package/dist/src/serve/mcp-server.js +19 -13
- package/dist/src/serve/server.js +190 -4
- package/dist/src/session/session-preview.js +14 -0
- package/dist/src/shared/graph-search.js +80 -0
- package/dist/src/shared/graph-selection.js +40 -0
- package/dist/src/shared/symbol-search.js +156 -0
- package/dist/src/tools/search-code-map.js +27 -35
- package/dist/src/web/app.js +582 -64
- package/dist/src/web/index.html +84 -6
- package/dist/src/web/style.css +256 -1
- package/dist/tests/config-api.test.js +10 -5
- package/dist/tests/config-integration.test.js +130 -56
- package/dist/tests/config-slash-command.test.js +12 -11
- package/dist/tests/config.test.js +21 -4
- package/dist/tests/editable-config.test.js +15 -12
- package/dist/tests/graph-onboarding.test.js +22 -1
- package/dist/tests/graph-search.test.js +66 -0
- package/dist/tests/graph-selection.test.js +58 -0
- package/dist/tests/home-env.test.js +56 -0
- package/dist/tests/mcp-and-plugin.test.js +3 -0
- package/dist/tests/search-code-map.test.js +9 -0
- package/dist/tests/serve.integration.test.js +255 -6
- package/dist/tests/session-preview.test.js +56 -0
- package/dist/tests/session-ui.test.js +2 -0
- package/dist/tests/settings-ui.test.js +18 -0
- package/dist/tests/system-prompt.test.js +1 -0
- package/dist/tests/test-utils.js +1 -0
- package/node_modules/@minicode/agent-sdk/dist/src/agent/types.d.ts +1 -0
- package/node_modules/@minicode/agent-sdk/dist/src/agent/types.d.ts.map +1 -1
- package/node_modules/@minicode/agent-sdk/dist/src/model/client.d.ts +8 -1
- package/node_modules/@minicode/agent-sdk/dist/src/model/client.d.ts.map +1 -1
- package/node_modules/@minicode/agent-sdk/dist/src/model/client.js +143 -27
- package/node_modules/@minicode/agent-sdk/dist/src/model/client.js.map +1 -1
- package/node_modules/@minicode/agent-sdk/dist/tests/model-client-openai.test.js +87 -0
- package/node_modules/@minicode/agent-sdk/dist/tests/model-client-openai.test.js.map +1 -1
- package/node_modules/@minicode/agent-sdk/dist/tests/test-utils.d.ts.map +1 -1
- package/node_modules/@minicode/agent-sdk/dist/tests/test-utils.js +1 -0
- package/node_modules/@minicode/agent-sdk/dist/tests/test-utils.js.map +1 -1
- package/node_modules/@minicode/agent-sdk/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { compareGraphNodeIds, getGraphNodeLabel, matchesGraphNodeQuery, } from "./graph-symbols.js";
|
|
2
|
+
export function getGraphNodeFilePath(node) {
|
|
3
|
+
return node.filePath || node.file || "";
|
|
4
|
+
}
|
|
5
|
+
export function buildGraphFileIndex(nodes) {
|
|
6
|
+
const files = new Map();
|
|
7
|
+
for (const [id, node] of nodes) {
|
|
8
|
+
const filePath = getGraphNodeFilePath(node);
|
|
9
|
+
if (!filePath)
|
|
10
|
+
continue;
|
|
11
|
+
const existing = files.get(filePath);
|
|
12
|
+
if (existing) {
|
|
13
|
+
existing.push(id);
|
|
14
|
+
}
|
|
15
|
+
else {
|
|
16
|
+
files.set(filePath, [id]);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
for (const symbolIds of files.values()) {
|
|
20
|
+
symbolIds.sort((a, b) => compareGraphNodeIds(a, b, nodes));
|
|
21
|
+
}
|
|
22
|
+
return files;
|
|
23
|
+
}
|
|
24
|
+
export function compareGraphFilePaths(a, b, fileIndex) {
|
|
25
|
+
const countDifference = (fileIndex.get(b)?.length ?? 0) - (fileIndex.get(a)?.length ?? 0);
|
|
26
|
+
if (countDifference !== 0) {
|
|
27
|
+
return countDifference;
|
|
28
|
+
}
|
|
29
|
+
return a.localeCompare(b, undefined, { sensitivity: "base" });
|
|
30
|
+
}
|
|
31
|
+
export function matchesGraphFileQuery(query, filePath) {
|
|
32
|
+
const normalizedQuery = query.trim().toLowerCase();
|
|
33
|
+
if (normalizedQuery.length === 0) {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
return filePath.toLowerCase().includes(normalizedQuery);
|
|
37
|
+
}
|
|
38
|
+
export function buildGraphSearchResults({ query, symbolIds, nodes, fileIndex, symbolLimit = 12, fileLimit = 8, }) {
|
|
39
|
+
const normalizedQuery = query.trim().toLowerCase();
|
|
40
|
+
const showDefaultResults = normalizedQuery.length < 2;
|
|
41
|
+
const rankedFiles = [...fileIndex.keys()].sort((a, b) => compareGraphFilePaths(a, b, fileIndex));
|
|
42
|
+
const symbolResults = symbolIds
|
|
43
|
+
.filter((id) => {
|
|
44
|
+
if (showDefaultResults) {
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
return matchesGraphNodeQuery(normalizedQuery, nodes.get(id) || {}, id);
|
|
48
|
+
})
|
|
49
|
+
.slice(0, symbolLimit)
|
|
50
|
+
.map((id) => {
|
|
51
|
+
const node = nodes.get(id) || {};
|
|
52
|
+
return {
|
|
53
|
+
type: "symbol",
|
|
54
|
+
id,
|
|
55
|
+
label: getGraphNodeLabel(node, id),
|
|
56
|
+
subtitle: getGraphNodeFilePath(node),
|
|
57
|
+
kind: (node.kind || "symbol").toLowerCase(),
|
|
58
|
+
};
|
|
59
|
+
});
|
|
60
|
+
const fileResults = rankedFiles
|
|
61
|
+
.filter((filePath) => {
|
|
62
|
+
if (showDefaultResults) {
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
return matchesGraphFileQuery(normalizedQuery, filePath);
|
|
66
|
+
})
|
|
67
|
+
.slice(0, fileLimit)
|
|
68
|
+
.map((filePath) => {
|
|
69
|
+
const symbolCount = fileIndex.get(filePath)?.length ?? 0;
|
|
70
|
+
return {
|
|
71
|
+
type: "file",
|
|
72
|
+
id: filePath,
|
|
73
|
+
label: filePath,
|
|
74
|
+
subtitle: `${symbolCount} symbol${symbolCount === 1 ? "" : "s"}`,
|
|
75
|
+
kind: "file",
|
|
76
|
+
symbolCount,
|
|
77
|
+
};
|
|
78
|
+
});
|
|
79
|
+
return [...symbolResults, ...fileResults];
|
|
80
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export function buildGraphEdgeId(edge) {
|
|
2
|
+
return `${edge.source}->${edge.target}:${edge.kind}`;
|
|
3
|
+
}
|
|
4
|
+
export function buildGraphEdgeIndex(edges) {
|
|
5
|
+
const edgeIndex = new Map();
|
|
6
|
+
for (const edge of edges) {
|
|
7
|
+
const sourceEdges = edgeIndex.get(edge.source);
|
|
8
|
+
if (sourceEdges) {
|
|
9
|
+
sourceEdges.push(edge);
|
|
10
|
+
}
|
|
11
|
+
else {
|
|
12
|
+
edgeIndex.set(edge.source, [edge]);
|
|
13
|
+
}
|
|
14
|
+
const targetEdges = edgeIndex.get(edge.target);
|
|
15
|
+
if (targetEdges) {
|
|
16
|
+
targetEdges.push(edge);
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
edgeIndex.set(edge.target, [edge]);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return edgeIndex;
|
|
23
|
+
}
|
|
24
|
+
export function buildFileFocusedSelection({ filePath, fileIndex, edgeIndex, }) {
|
|
25
|
+
const fileSymbolIds = fileIndex.get(filePath) || [];
|
|
26
|
+
const nodeIds = new Set();
|
|
27
|
+
const edges = new Map();
|
|
28
|
+
for (const symbolId of fileSymbolIds) {
|
|
29
|
+
nodeIds.add(symbolId);
|
|
30
|
+
for (const edge of edgeIndex.get(symbolId) || []) {
|
|
31
|
+
nodeIds.add(edge.source);
|
|
32
|
+
nodeIds.add(edge.target);
|
|
33
|
+
edges.set(buildGraphEdgeId(edge), edge);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
nodeIds: [...nodeIds],
|
|
38
|
+
edges: [...edges.values()],
|
|
39
|
+
};
|
|
40
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
const MIN_SIMILARITY_QUERY_LENGTH = 3;
|
|
2
|
+
const MIN_SIMILARITY_SCORE = 0.62;
|
|
3
|
+
function normalizeSearchText(value) {
|
|
4
|
+
return value.toLowerCase().replace(/[^a-z0-9]+/g, "");
|
|
5
|
+
}
|
|
6
|
+
function tokenizeSearchText(value) {
|
|
7
|
+
return value
|
|
8
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
|
|
9
|
+
.toLowerCase()
|
|
10
|
+
.split(/[^a-z0-9]+/)
|
|
11
|
+
.filter(Boolean);
|
|
12
|
+
}
|
|
13
|
+
function levenshteinDistance(a, b) {
|
|
14
|
+
if (a === b) {
|
|
15
|
+
return 0;
|
|
16
|
+
}
|
|
17
|
+
if (a.length === 0) {
|
|
18
|
+
return b.length;
|
|
19
|
+
}
|
|
20
|
+
if (b.length === 0) {
|
|
21
|
+
return a.length;
|
|
22
|
+
}
|
|
23
|
+
const previous = new Array(b.length + 1);
|
|
24
|
+
const current = new Array(b.length + 1);
|
|
25
|
+
for (let j = 0; j <= b.length; j += 1) {
|
|
26
|
+
previous[j] = j;
|
|
27
|
+
}
|
|
28
|
+
for (let i = 1; i <= a.length; i += 1) {
|
|
29
|
+
current[0] = i;
|
|
30
|
+
for (let j = 1; j <= b.length; j += 1) {
|
|
31
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
32
|
+
current[j] = Math.min(current[j - 1] + 1, previous[j] + 1, previous[j - 1] + cost);
|
|
33
|
+
}
|
|
34
|
+
for (let j = 0; j <= b.length; j += 1) {
|
|
35
|
+
previous[j] = current[j];
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return previous[b.length];
|
|
39
|
+
}
|
|
40
|
+
function diceCoefficient(a, b) {
|
|
41
|
+
if (a === b) {
|
|
42
|
+
return 1;
|
|
43
|
+
}
|
|
44
|
+
if (a.length < 2 || b.length < 2) {
|
|
45
|
+
return 0;
|
|
46
|
+
}
|
|
47
|
+
const bigrams = new Map();
|
|
48
|
+
for (let i = 0; i < a.length - 1; i += 1) {
|
|
49
|
+
const bigram = a.slice(i, i + 2);
|
|
50
|
+
bigrams.set(bigram, (bigrams.get(bigram) ?? 0) + 1);
|
|
51
|
+
}
|
|
52
|
+
let matches = 0;
|
|
53
|
+
for (let i = 0; i < b.length - 1; i += 1) {
|
|
54
|
+
const bigram = b.slice(i, i + 2);
|
|
55
|
+
const count = bigrams.get(bigram) ?? 0;
|
|
56
|
+
if (count > 0) {
|
|
57
|
+
bigrams.set(bigram, count - 1);
|
|
58
|
+
matches += 1;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return (2 * matches) / ((a.length - 1) + (b.length - 1));
|
|
62
|
+
}
|
|
63
|
+
function computeSimilarityScore(pattern, candidate) {
|
|
64
|
+
const normalizedPattern = normalizeSearchText(pattern);
|
|
65
|
+
const normalizedCandidate = normalizeSearchText(candidate);
|
|
66
|
+
if (!normalizedPattern || !normalizedCandidate) {
|
|
67
|
+
return 0;
|
|
68
|
+
}
|
|
69
|
+
if (normalizedPattern === normalizedCandidate) {
|
|
70
|
+
return 1;
|
|
71
|
+
}
|
|
72
|
+
const maxLength = Math.max(normalizedPattern.length, normalizedCandidate.length, 1);
|
|
73
|
+
const lengthPenalty = Math.abs(normalizedCandidate.length - normalizedPattern.length) / maxLength;
|
|
74
|
+
let bestScore = 0;
|
|
75
|
+
if (normalizedCandidate.startsWith(normalizedPattern) ||
|
|
76
|
+
normalizedPattern.startsWith(normalizedCandidate)) {
|
|
77
|
+
bestScore = Math.max(bestScore, 0.93 - (lengthPenalty * 0.2));
|
|
78
|
+
}
|
|
79
|
+
const tokens = tokenizeSearchText(candidate);
|
|
80
|
+
if (tokens.some((token) => token.startsWith(normalizedPattern) || normalizedPattern.startsWith(token))) {
|
|
81
|
+
bestScore = Math.max(bestScore, 0.88 - (lengthPenalty * 0.15));
|
|
82
|
+
}
|
|
83
|
+
const editDistance = levenshteinDistance(normalizedPattern, normalizedCandidate);
|
|
84
|
+
const maxDistance = Math.max(1, Math.ceil(normalizedPattern.length * 0.34));
|
|
85
|
+
if (editDistance <= maxDistance) {
|
|
86
|
+
const editSimilarity = 1 - (editDistance / maxLength);
|
|
87
|
+
bestScore = Math.max(bestScore, 0.62 + (editSimilarity * 0.3));
|
|
88
|
+
}
|
|
89
|
+
const dice = diceCoefficient(normalizedPattern, normalizedCandidate);
|
|
90
|
+
if (dice >= 0.5) {
|
|
91
|
+
bestScore = Math.max(bestScore, 0.45 + (dice * 0.35));
|
|
92
|
+
}
|
|
93
|
+
return bestScore;
|
|
94
|
+
}
|
|
95
|
+
function compareSubstringCandidates(pattern, a, b) {
|
|
96
|
+
const lowerPattern = pattern.toLowerCase();
|
|
97
|
+
const aExact = Number(a.lookupNames.some((value) => value.toLowerCase() === lowerPattern));
|
|
98
|
+
const bExact = Number(b.lookupNames.some((value) => value.toLowerCase() === lowerPattern));
|
|
99
|
+
if (aExact !== bExact) {
|
|
100
|
+
return bExact - aExact;
|
|
101
|
+
}
|
|
102
|
+
return Number(b.record.exported ?? false) - Number(a.record.exported ?? false) ||
|
|
103
|
+
a.record.name.localeCompare(b.record.name) ||
|
|
104
|
+
a.record.filePath.localeCompare(b.record.filePath) ||
|
|
105
|
+
a.record.startLine - b.record.startLine ||
|
|
106
|
+
a.record.qualifiedName.localeCompare(b.record.qualifiedName);
|
|
107
|
+
}
|
|
108
|
+
function compareSimilarCandidates(a, b) {
|
|
109
|
+
return b.score - a.score ||
|
|
110
|
+
Number(b.candidate.record.exported ?? false) - Number(a.candidate.record.exported ?? false) ||
|
|
111
|
+
a.candidate.record.name.localeCompare(b.candidate.record.name) ||
|
|
112
|
+
a.candidate.record.filePath.localeCompare(b.candidate.record.filePath) ||
|
|
113
|
+
a.candidate.record.startLine - b.candidate.record.startLine ||
|
|
114
|
+
a.candidate.record.qualifiedName.localeCompare(b.candidate.record.qualifiedName);
|
|
115
|
+
}
|
|
116
|
+
export function searchSymbols(candidates, pattern, options = {}) {
|
|
117
|
+
const lowerPattern = pattern.toLowerCase();
|
|
118
|
+
const normalizedKind = options.kind?.trim().toLowerCase();
|
|
119
|
+
const skip = Math.max(0, options.skip ?? 0);
|
|
120
|
+
const limit = Math.max(1, options.limit ?? 30);
|
|
121
|
+
const filtered = candidates.filter((candidate) => {
|
|
122
|
+
if (normalizedKind && candidate.record.kind.toLowerCase() !== normalizedKind) {
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
return true;
|
|
126
|
+
});
|
|
127
|
+
const substringMatches = filtered
|
|
128
|
+
.filter((candidate) => candidate.lookupNames.some((value) => value.toLowerCase().includes(lowerPattern)))
|
|
129
|
+
.sort((a, b) => compareSubstringCandidates(pattern, a, b));
|
|
130
|
+
if (substringMatches.length > 0) {
|
|
131
|
+
return {
|
|
132
|
+
matches: substringMatches.slice(skip, skip + limit).map((candidate) => candidate.symbol),
|
|
133
|
+
mode: "substring",
|
|
134
|
+
total: substringMatches.length,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
const normalizedPattern = normalizeSearchText(pattern);
|
|
138
|
+
if (normalizedPattern.length < MIN_SIMILARITY_QUERY_LENGTH) {
|
|
139
|
+
return { matches: [], mode: "none", total: 0 };
|
|
140
|
+
}
|
|
141
|
+
const similarMatches = filtered
|
|
142
|
+
.map((candidate) => ({
|
|
143
|
+
candidate,
|
|
144
|
+
score: Math.max(...candidate.lookupNames.map((value) => computeSimilarityScore(pattern, value))),
|
|
145
|
+
}))
|
|
146
|
+
.filter((entry) => entry.score >= MIN_SIMILARITY_SCORE)
|
|
147
|
+
.sort(compareSimilarCandidates);
|
|
148
|
+
if (similarMatches.length === 0) {
|
|
149
|
+
return { matches: [], mode: "none", total: 0 };
|
|
150
|
+
}
|
|
151
|
+
return {
|
|
152
|
+
matches: similarMatches.slice(skip, skip + limit).map((entry) => entry.candidate.symbol),
|
|
153
|
+
mode: "similar",
|
|
154
|
+
total: similarMatches.length,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
@@ -1,26 +1,7 @@
|
|
|
1
1
|
import { expectNonEmptyString, expectOptionalNumber } from "@minicode/agent-sdk";
|
|
2
2
|
import { getSymbolDisplayName, getSymbolLookupNames } from "../indexer/symbol-names.js";
|
|
3
|
+
import { searchSymbols } from "../shared/symbol-search.js";
|
|
3
4
|
const DEFAULT_LIMIT = 30;
|
|
4
|
-
function compareMatchedSymbols(pattern, a, b) {
|
|
5
|
-
const lowerPattern = pattern.toLowerCase();
|
|
6
|
-
const aDisplay = getSymbolDisplayName(a).toLowerCase();
|
|
7
|
-
const bDisplay = getSymbolDisplayName(b).toLowerCase();
|
|
8
|
-
const aExact = Number(aDisplay === lowerPattern || a.qualifiedName.toLowerCase() === lowerPattern || a.name.toLowerCase() === lowerPattern);
|
|
9
|
-
const bExact = Number(bDisplay === lowerPattern || b.qualifiedName.toLowerCase() === lowerPattern || b.name.toLowerCase() === lowerPattern);
|
|
10
|
-
if (aExact !== bExact) {
|
|
11
|
-
return bExact - aExact;
|
|
12
|
-
}
|
|
13
|
-
return Number(b.exported) - Number(a.exported) ||
|
|
14
|
-
aDisplay.localeCompare(bDisplay) ||
|
|
15
|
-
a.filePath.localeCompare(b.filePath) ||
|
|
16
|
-
a.startLine - b.startLine ||
|
|
17
|
-
a.qualifiedName.localeCompare(b.qualifiedName);
|
|
18
|
-
}
|
|
19
|
-
function matchesPattern(text, pattern) {
|
|
20
|
-
const lowerText = text.toLowerCase();
|
|
21
|
-
const lowerPattern = pattern.toLowerCase();
|
|
22
|
-
return lowerText.includes(lowerPattern);
|
|
23
|
-
}
|
|
24
5
|
export function createSearchCodeMapTool(projectIndex) {
|
|
25
6
|
return {
|
|
26
7
|
name: "search_code_map",
|
|
@@ -58,28 +39,39 @@ export function createSearchCodeMapTool(projectIndex) {
|
|
|
58
39
|
: undefined;
|
|
59
40
|
const limit = Math.max(1, Math.min(100, expectOptionalNumber(input, "limit") ?? DEFAULT_LIMIT));
|
|
60
41
|
const skip = Math.max(0, expectOptionalNumber(input, "skip") ?? 0);
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
42
|
+
const result = searchSymbols([...projectIndex.symbols.values()].map((sym) => ({
|
|
43
|
+
symbol: sym,
|
|
44
|
+
record: {
|
|
45
|
+
name: getSymbolDisplayName(sym),
|
|
46
|
+
qualifiedName: sym.qualifiedName,
|
|
47
|
+
kind: sym.kind,
|
|
48
|
+
filePath: sym.filePath,
|
|
49
|
+
startLine: sym.startLine,
|
|
50
|
+
exported: sym.exported,
|
|
51
|
+
},
|
|
52
|
+
lookupNames: getSymbolLookupNames(sym),
|
|
53
|
+
})), pattern, { kind, limit, skip });
|
|
54
|
+
const shown = result.matches;
|
|
73
55
|
const lines = shown.map((s) => `- ${getSymbolDisplayName(s)} (${s.kind}) — ${s.filePath}:${s.startLine} — qualified: ${s.qualifiedName}`);
|
|
74
|
-
const remaining =
|
|
56
|
+
const remaining = result.total - skip - shown.length;
|
|
75
57
|
const footer = remaining > 0
|
|
76
58
|
? `\n... and ${remaining} more (use skip: ${skip + limit}, limit: ${limit} for next page)`
|
|
77
59
|
: "";
|
|
78
|
-
if (
|
|
60
|
+
if (result.total === 0) {
|
|
79
61
|
return `No symbols matching "${pattern}"${kind ? ` (kind: ${kind})` : ""}. Try a shorter or different pattern.`;
|
|
80
62
|
}
|
|
63
|
+
if (result.mode === "similar") {
|
|
64
|
+
return [
|
|
65
|
+
`# No exact substring matches for "${pattern}"${kind ? ` (kind: ${kind})` : ""}`,
|
|
66
|
+
"",
|
|
67
|
+
`Showing similar symbols instead (${result.total} total):`,
|
|
68
|
+
"",
|
|
69
|
+
...lines,
|
|
70
|
+
footer,
|
|
71
|
+
].join("\n");
|
|
72
|
+
}
|
|
81
73
|
return [
|
|
82
|
-
`# Symbols matching "${pattern}" (${
|
|
74
|
+
`# Symbols matching "${pattern}" (${result.total} total)`,
|
|
83
75
|
"",
|
|
84
76
|
...lines,
|
|
85
77
|
footer,
|