@sean.holung/minicode 0.3.2 → 0.3.3

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.
Files changed (107) hide show
  1. package/README.md +48 -43
  2. package/dist/scripts/run-benchmarks.js +147 -0
  3. package/dist/src/agent/config.js +149 -40
  4. package/dist/src/agent/editable-config.js +314 -0
  5. package/dist/src/analysis/structural-analysis.js +379 -0
  6. package/dist/src/benchmark/evaluator.js +79 -0
  7. package/dist/src/benchmark/index.js +4 -0
  8. package/dist/src/benchmark/reporter.js +177 -0
  9. package/dist/src/benchmark/runner.js +100 -0
  10. package/dist/src/benchmark/task-loader.js +78 -0
  11. package/dist/src/benchmark/types.js +5 -0
  12. package/dist/src/cli/args.js +10 -0
  13. package/dist/src/cli/config-slash-command.js +135 -0
  14. package/dist/src/cli/plugin-install.js +69 -0
  15. package/dist/src/index.js +76 -6
  16. package/dist/src/indexer/cache.js +6 -4
  17. package/dist/src/indexer/code-map.js +41 -13
  18. package/dist/src/indexer/plugins/typescript.js +70 -23
  19. package/dist/src/indexer/project-index.js +175 -36
  20. package/dist/src/indexer/symbol-names.js +92 -0
  21. package/dist/src/model-utils.js +18 -0
  22. package/dist/src/serve/agent-bridge.js +203 -24
  23. package/dist/src/serve/mcp-server.js +405 -0
  24. package/dist/src/serve/server.js +165 -10
  25. package/dist/src/serve/websocket.js +8 -0
  26. package/dist/src/shared/graph-styles.js +119 -0
  27. package/dist/src/tools/find-path.js +75 -0
  28. package/dist/src/tools/find-references.js +7 -2
  29. package/dist/src/tools/get-dependencies.js +3 -2
  30. package/dist/src/tools/read-symbol.js +12 -5
  31. package/dist/src/tools/registry.js +3 -1
  32. package/dist/src/tools/search-code-map.js +4 -2
  33. package/dist/src/ui/app.js +1 -1
  34. package/dist/src/ui/cli-ink.js +79 -4
  35. package/dist/src/ui/components/header-bar.js +6 -2
  36. package/dist/src/ui/state/ui-store.js +5 -0
  37. package/dist/src/web/app.js +1124 -176
  38. package/dist/src/web/index.html +113 -3
  39. package/dist/src/web/style.css +973 -55
  40. package/dist/tests/agent.test.js +31 -0
  41. package/dist/tests/analysis-helpers.test.js +89 -0
  42. package/dist/tests/analysis-ui.test.js +29 -0
  43. package/dist/tests/benchmark-harness.test.js +527 -0
  44. package/dist/tests/config-api.test.js +143 -0
  45. package/dist/tests/config-integration.test.js +751 -0
  46. package/dist/tests/config-slash-command.test.js +106 -0
  47. package/dist/tests/config.test.js +42 -1
  48. package/dist/tests/context-indicator.test.js +220 -0
  49. package/dist/tests/editable-config.test.js +109 -0
  50. package/dist/tests/find-path.test.js +183 -0
  51. package/dist/tests/focus-tracker.test.js +62 -0
  52. package/dist/tests/graph-onboarding.test.js +55 -0
  53. package/dist/tests/graph-styles.test.js +65 -0
  54. package/dist/tests/indexer.test.js +137 -0
  55. package/dist/tests/mcp-and-plugin.test.js +186 -0
  56. package/dist/tests/model-client-openai.test.js +29 -0
  57. package/dist/tests/model-selection.test.js +136 -0
  58. package/dist/tests/model-utils.test.js +22 -0
  59. package/dist/tests/reasoning-effort.test.js +264 -0
  60. package/dist/tests/run-benchmarks.test.js +161 -0
  61. package/dist/tests/search-code-map.test.js +18 -0
  62. package/dist/tests/serve.integration.test.js +218 -2
  63. package/dist/tests/session-ui.test.js +21 -0
  64. package/dist/tests/session.test.js +50 -0
  65. package/dist/tests/settings-ui.test.js +30 -0
  66. package/dist/tests/structural-analysis.test.js +218 -0
  67. package/node_modules/@minicode/agent-sdk/README.md +80 -51
  68. package/node_modules/@minicode/agent-sdk/dist/src/agent/agent.d.ts +16 -5
  69. package/node_modules/@minicode/agent-sdk/dist/src/agent/agent.d.ts.map +1 -1
  70. package/node_modules/@minicode/agent-sdk/dist/src/agent/agent.js +51 -33
  71. package/node_modules/@minicode/agent-sdk/dist/src/agent/agent.js.map +1 -1
  72. package/node_modules/@minicode/agent-sdk/dist/src/agent/types.d.ts +14 -0
  73. package/node_modules/@minicode/agent-sdk/dist/src/agent/types.d.ts.map +1 -1
  74. package/node_modules/@minicode/agent-sdk/dist/src/index.d.ts +3 -2
  75. package/node_modules/@minicode/agent-sdk/dist/src/index.d.ts.map +1 -1
  76. package/node_modules/@minicode/agent-sdk/dist/src/index.js +2 -0
  77. package/node_modules/@minicode/agent-sdk/dist/src/index.js.map +1 -1
  78. package/node_modules/@minicode/agent-sdk/dist/src/indexer/focus-tracker.d.ts +35 -0
  79. package/node_modules/@minicode/agent-sdk/dist/src/indexer/focus-tracker.d.ts.map +1 -0
  80. package/node_modules/@minicode/agent-sdk/dist/src/indexer/focus-tracker.js +64 -0
  81. package/node_modules/@minicode/agent-sdk/dist/src/indexer/focus-tracker.js.map +1 -0
  82. package/node_modules/@minicode/agent-sdk/dist/src/indexer/types.d.ts +7 -0
  83. package/node_modules/@minicode/agent-sdk/dist/src/indexer/types.d.ts.map +1 -1
  84. package/node_modules/@minicode/agent-sdk/dist/src/model/client.d.ts +5 -1
  85. package/node_modules/@minicode/agent-sdk/dist/src/model/client.d.ts.map +1 -1
  86. package/node_modules/@minicode/agent-sdk/dist/src/model/client.js +83 -11
  87. package/node_modules/@minicode/agent-sdk/dist/src/model/client.js.map +1 -1
  88. package/node_modules/@minicode/agent-sdk/dist/src/safety/guardrails.d.ts +1 -0
  89. package/node_modules/@minicode/agent-sdk/dist/src/safety/guardrails.d.ts.map +1 -1
  90. package/node_modules/@minicode/agent-sdk/dist/src/safety/guardrails.js +8 -1
  91. package/node_modules/@minicode/agent-sdk/dist/src/safety/guardrails.js.map +1 -1
  92. package/node_modules/@minicode/agent-sdk/dist/src/session/session.d.ts.map +1 -1
  93. package/node_modules/@minicode/agent-sdk/dist/src/session/session.js +4 -1
  94. package/node_modules/@minicode/agent-sdk/dist/src/session/session.js.map +1 -1
  95. package/node_modules/@minicode/agent-sdk/dist/tests/agent.test.js +3 -1
  96. package/node_modules/@minicode/agent-sdk/dist/tests/agent.test.js.map +1 -1
  97. package/node_modules/@minicode/agent-sdk/dist/tests/guardrails.test.js +8 -2
  98. package/node_modules/@minicode/agent-sdk/dist/tests/guardrails.test.js.map +1 -1
  99. package/node_modules/@minicode/agent-sdk/dist/tsconfig.tsbuildinfo +1 -1
  100. package/package.json +9 -5
  101. package/plugin/.claude-plugin/plugin.json +12 -0
  102. package/plugin/.mcp.json +8 -0
  103. package/plugin/CLAUDE.md +26 -0
  104. package/plugin/skills/analyze/SKILL.md +12 -0
  105. package/plugin/skills/focus/SKILL.md +20 -0
  106. package/plugin/skills/graph/SKILL.md +13 -0
  107. package/plugin/skills/symbols/SKILL.md +13 -0
@@ -2,26 +2,75 @@ import { readdir, readFile } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { generateCodeMap } from "./code-map.js";
4
4
  import { getPluginForFile, loadPlugins } from "./plugin-loader.js";
5
+ import { getSymbolLookupNames, normalizeIndexedSymbols } from "./symbol-names.js";
5
6
  const SKIP_DIRS = new Set(["node_modules", ".git", "dist", "build", "coverage"]);
6
- async function collectSourceFiles(dir, root, files) {
7
+ async function collectSourceFiles(dir, root, files, validExtensions) {
7
8
  const entries = await readdir(dir, { withFileTypes: true });
8
9
  for (const entry of entries) {
9
10
  const fullPath = path.join(dir, entry.name);
10
11
  if (entry.isDirectory()) {
11
12
  if (!SKIP_DIRS.has(entry.name) && !entry.name.startsWith(".")) {
12
- await collectSourceFiles(fullPath, root, files);
13
+ await collectSourceFiles(fullPath, root, files, validExtensions);
13
14
  }
14
15
  continue;
15
16
  }
16
17
  if (entry.isFile()) {
17
18
  const ext = path.extname(entry.name).toLowerCase();
18
- if ([".ts", ".tsx", ".js", ".jsx"].includes(ext)) {
19
+ if (validExtensions.has(ext)) {
19
20
  files.push(path.relative(root, fullPath));
20
21
  }
21
22
  }
22
23
  }
23
24
  }
25
+ function buildAdjacencyFrom(edges) {
26
+ const map = new Map();
27
+ for (const edge of edges) {
28
+ const list = map.get(edge.from);
29
+ if (list) {
30
+ list.push(edge);
31
+ }
32
+ else {
33
+ map.set(edge.from, [edge]);
34
+ }
35
+ }
36
+ return map;
37
+ }
38
+ function buildAdjacencyTo(edges) {
39
+ const map = new Map();
40
+ for (const edge of edges) {
41
+ const list = map.get(edge.to);
42
+ if (list) {
43
+ list.push(edge);
44
+ }
45
+ else {
46
+ map.set(edge.to, [edge]);
47
+ }
48
+ }
49
+ return map;
50
+ }
51
+ function resolveSymbol(name, symbols) {
52
+ const direct = symbols.get(name);
53
+ if (direct)
54
+ return direct;
55
+ const matches = [...symbols.values()].filter((sym) => getSymbolLookupNames(sym).includes(name));
56
+ if (matches.length === 0) {
57
+ return undefined;
58
+ }
59
+ matches.sort((a, b) => Number(b.exported) - Number(a.exported) ||
60
+ a.filePath.localeCompare(b.filePath) ||
61
+ a.startLine - b.startLine ||
62
+ a.qualifiedName.localeCompare(b.qualifiedName));
63
+ return matches[0];
64
+ }
24
65
  export function createProjectIndex(symbols, files, dependencyEdges, plugins, projectFiles, workspaceRoot) {
66
+ let adjacencyFrom = buildAdjacencyFrom(dependencyEdges);
67
+ function rebuildSymbolsMap() {
68
+ const normalizedSymbols = normalizeIndexedSymbols(files);
69
+ symbols.clear();
70
+ for (const [qualifiedName, symbol] of normalizedSymbols) {
71
+ symbols.set(qualifiedName, symbol);
72
+ }
73
+ }
25
74
  return {
26
75
  symbols,
27
76
  files,
@@ -30,20 +79,13 @@ export function createProjectIndex(symbols, files, dependencyEdges, plugins, pro
30
79
  projectFiles,
31
80
  workspaceRoot,
32
81
  getSymbol(name) {
33
- const direct = symbols.get(name);
34
- if (direct)
35
- return direct;
36
- for (const sym of symbols.values()) {
37
- if (sym.name === name || sym.qualifiedName === name)
38
- return sym;
39
- }
40
- return undefined;
82
+ return resolveSymbol(name, symbols);
41
83
  },
42
84
  getSymbolsInFile(filePath) {
43
85
  return files.get(filePath) ?? [];
44
86
  },
45
87
  getDependencyCone(symbolName, depth = 2) {
46
- const target = symbols.get(symbolName) ?? [...symbols.values()].find((s) => s.name === symbolName || s.qualifiedName === symbolName);
88
+ const target = resolveSymbol(symbolName, symbols);
47
89
  if (!target)
48
90
  return [];
49
91
  const result = new Map();
@@ -52,13 +94,14 @@ export function createProjectIndex(symbols, files, dependencyEdges, plugins, pro
52
94
  for (let d = 0; d < depth; d += 1) {
53
95
  const next = new Set();
54
96
  for (const from of frontier) {
55
- for (const edge of dependencyEdges) {
56
- if (edge.from === from) {
57
- const dep = symbols.get(edge.to) ?? [...symbols.values()].find((s) => s.qualifiedName === edge.to || s.name === edge.to);
58
- if (dep && !result.has(dep.qualifiedName)) {
59
- result.set(dep.qualifiedName, dep);
60
- next.add(dep.qualifiedName);
61
- }
97
+ const outEdges = adjacencyFrom.get(from);
98
+ if (!outEdges)
99
+ continue;
100
+ for (const edge of outEdges) {
101
+ const dep = resolveSymbol(edge.to, symbols);
102
+ if (dep && !result.has(dep.qualifiedName)) {
103
+ result.set(dep.qualifiedName, dep);
104
+ next.add(dep.qualifiedName);
62
105
  }
63
106
  }
64
107
  }
@@ -66,6 +109,108 @@ export function createProjectIndex(symbols, files, dependencyEdges, plugins, pro
66
109
  }
67
110
  return [...result.values()];
68
111
  },
112
+ findPath(fromSymbol, toSymbol, maxDepth = 10) {
113
+ const from = resolveSymbol(fromSymbol, symbols);
114
+ const to = resolveSymbol(toSymbol, symbols);
115
+ if (!from || !to)
116
+ return [];
117
+ // BFS over both outgoing and incoming edges (undirected search)
118
+ const adjacencyTo = buildAdjacencyTo(dependencyEdges);
119
+ const visited = new Set();
120
+ const parent = new Map();
121
+ const queue = [
122
+ { name: from.qualifiedName, depth: 0 },
123
+ ];
124
+ visited.add(from.qualifiedName);
125
+ while (queue.length > 0) {
126
+ const current = queue.shift();
127
+ if (current.name === to.qualifiedName) {
128
+ // Reconstruct path
129
+ const path = [];
130
+ let node = to.qualifiedName;
131
+ while (node !== undefined) {
132
+ const sym = resolveSymbol(node, symbols);
133
+ if (sym)
134
+ path.unshift(sym);
135
+ node = parent.get(node);
136
+ }
137
+ return path;
138
+ }
139
+ if (current.depth >= maxDepth)
140
+ continue;
141
+ // Follow outgoing edges
142
+ const outEdges = adjacencyFrom.get(current.name) ?? [];
143
+ for (const edge of outEdges) {
144
+ const target = resolveSymbol(edge.to, symbols);
145
+ if (target && !visited.has(target.qualifiedName)) {
146
+ visited.add(target.qualifiedName);
147
+ parent.set(target.qualifiedName, current.name);
148
+ queue.push({ name: target.qualifiedName, depth: current.depth + 1 });
149
+ }
150
+ }
151
+ // Follow incoming edges (reverse direction)
152
+ const inEdges = adjacencyTo.get(current.name) ?? [];
153
+ for (const edge of inEdges) {
154
+ const source = resolveSymbol(edge.from, symbols);
155
+ if (source && !visited.has(source.qualifiedName)) {
156
+ visited.add(source.qualifiedName);
157
+ parent.set(source.qualifiedName, current.name);
158
+ queue.push({ name: source.qualifiedName, depth: current.depth + 1 });
159
+ }
160
+ }
161
+ }
162
+ return [];
163
+ },
164
+ findPathToEntryPoint(symbolName, maxDepth = 20) {
165
+ const target = resolveSymbol(symbolName, symbols);
166
+ if (!target)
167
+ return [];
168
+ // Build reverse adjacency: who calls/references this symbol?
169
+ const adjacencyTo = buildAdjacencyTo(dependencyEdges);
170
+ // Find entry points: symbols that have no inbound edges
171
+ const hasInbound = new Set();
172
+ for (const edge of dependencyEdges) {
173
+ hasInbound.add(edge.to);
174
+ }
175
+ // If the target itself is already an entry point (no inbound edges), nothing to trace
176
+ if (!hasInbound.has(target.qualifiedName))
177
+ return [];
178
+ // DFS from target following inbound edges back to entry points
179
+ const paths = [];
180
+ const currentPath = [target];
181
+ const visiting = new Set([target.qualifiedName]);
182
+ function dfs(node, depth) {
183
+ if (depth >= maxDepth)
184
+ return;
185
+ const inEdges = adjacencyTo.get(node) ?? [];
186
+ const callers = inEdges
187
+ .map((e) => resolveSymbol(e.from, symbols))
188
+ .filter((s) => s != null && !visiting.has(s.qualifiedName));
189
+ if (callers.length === 0) {
190
+ // Dead end going backwards — this is as far as we can trace
191
+ if (currentPath.length > 1) {
192
+ paths.push([...currentPath].reverse());
193
+ }
194
+ return;
195
+ }
196
+ for (const caller of callers) {
197
+ // If this caller is an entry point (no inbound edges), record the path
198
+ if (!hasInbound.has(caller.qualifiedName)) {
199
+ currentPath.push(caller);
200
+ paths.push([...currentPath].reverse());
201
+ currentPath.pop();
202
+ continue;
203
+ }
204
+ visiting.add(caller.qualifiedName);
205
+ currentPath.push(caller);
206
+ dfs(caller.qualifiedName, depth + 1);
207
+ currentPath.pop();
208
+ visiting.delete(caller.qualifiedName);
209
+ }
210
+ }
211
+ dfs(target.qualifiedName, 0);
212
+ return paths.slice(0, 10);
213
+ },
69
214
  getCodeMap(tokenBudget, focusSymbols) {
70
215
  return generateCodeMap(files, tokenBudget, dependencyEdges, focusSymbols);
71
216
  },
@@ -76,24 +221,17 @@ export function createProjectIndex(symbols, files, dependencyEdges, plugins, pro
76
221
  const plugin = getPluginForFile(relPath, plugins);
77
222
  if (!plugin)
78
223
  return;
79
- const oldSymbols = files.get(relPath) ?? [];
80
- for (const sym of oldSymbols) {
81
- symbols.delete(sym.qualifiedName);
82
- }
83
224
  files.delete(relPath);
84
225
  projectFiles.set(relPath, content);
85
226
  const extracted = plugin.indexFile(relPath, content);
86
- for (const sym of extracted) {
87
- symbols.set(sym.qualifiedName, sym);
88
- const existing = files.get(relPath) ?? [];
89
- existing.push(sym);
90
- files.set(relPath, existing);
91
- }
227
+ files.set(relPath, extracted);
228
+ rebuildSymbolsMap();
92
229
  for (const p of plugins) {
93
230
  if (p.resolveDependencies) {
94
231
  const allSymbols = [...symbols.values()];
95
232
  const edges = p.resolveDependencies(allSymbols, projectFiles);
96
233
  dependencyEdges.splice(0, dependencyEdges.length, ...edges);
234
+ adjacencyFrom = buildAdjacencyFrom(dependencyEdges);
97
235
  break;
98
236
  }
99
237
  }
@@ -106,8 +244,9 @@ export function createProjectIndex(symbols, files, dependencyEdges, plugins, pro
106
244
  export async function buildProjectIndex(workspaceRoot) {
107
245
  const plugins = await loadPlugins(workspaceRoot);
108
246
  const root = path.resolve(workspaceRoot);
247
+ const validExtensions = new Set(plugins.flatMap((p) => p.extensions));
109
248
  const sourceFiles = [];
110
- await collectSourceFiles(root, root, sourceFiles);
249
+ await collectSourceFiles(root, root, sourceFiles, validExtensions);
111
250
  const symbols = new Map();
112
251
  const files = new Map();
113
252
  const projectFiles = new Map();
@@ -125,12 +264,12 @@ export async function buildProjectIndex(workspaceRoot) {
125
264
  }
126
265
  projectFiles.set(relPath, content);
127
266
  const extracted = plugin.indexFile(relPath, content);
128
- for (const sym of extracted) {
129
- symbols.set(sym.qualifiedName, sym);
130
- const existing = files.get(relPath) ?? [];
131
- existing.push(sym);
132
- files.set(relPath, existing);
133
- }
267
+ files.set(relPath, extracted);
268
+ }
269
+ const normalizedSymbols = normalizeIndexedSymbols(files);
270
+ symbols.clear();
271
+ for (const [qualifiedName, symbol] of normalizedSymbols) {
272
+ symbols.set(qualifiedName, symbol);
134
273
  }
135
274
  let dependencyEdges = [];
136
275
  for (const plugin of plugins) {
@@ -0,0 +1,92 @@
1
+ function dedupe(values) {
2
+ return [...new Set(values.filter((value) => value.length > 0))];
3
+ }
4
+ function buildDisplayName(baseName, symbol, kindCount, distinctFileCount) {
5
+ if (distinctFileCount > 1) {
6
+ return `${baseName} (${symbol.kind} in ${symbol.filePath}:${symbol.startLine})`;
7
+ }
8
+ if (kindCount === 1) {
9
+ return `${baseName} (${symbol.kind})`;
10
+ }
11
+ return `${baseName} (${symbol.kind}:${symbol.startLine})`;
12
+ }
13
+ function buildQualifiedName(baseName, symbol, kindCount, distinctFileCount) {
14
+ if (distinctFileCount > 1) {
15
+ return `${baseName}#${symbol.kind}@${symbol.filePath}:${symbol.startLine}`;
16
+ }
17
+ if (kindCount === 1) {
18
+ return `${baseName}#${symbol.kind}`;
19
+ }
20
+ return `${baseName}#${symbol.kind}:${symbol.startLine}`;
21
+ }
22
+ function countByKind(symbols) {
23
+ const counts = new Map();
24
+ for (const symbol of symbols) {
25
+ counts.set(symbol.kind, (counts.get(symbol.kind) ?? 0) + 1);
26
+ }
27
+ return counts;
28
+ }
29
+ function countByFile(symbols) {
30
+ const counts = new Map();
31
+ for (const symbol of symbols) {
32
+ counts.set(symbol.filePath, (counts.get(symbol.filePath) ?? 0) + 1);
33
+ }
34
+ return counts;
35
+ }
36
+ export function getSymbolDisplayName(symbol) {
37
+ return symbol.displayName ?? symbol.qualifiedName;
38
+ }
39
+ export function getSymbolLookupNames(symbol) {
40
+ return dedupe([
41
+ symbol.qualifiedName,
42
+ symbol.originalQualifiedName ?? "",
43
+ symbol.displayName ?? "",
44
+ symbol.name,
45
+ ...(symbol.aliases ?? []),
46
+ ]);
47
+ }
48
+ export function normalizeIndexedSymbols(symbolsByFile) {
49
+ const allSymbols = [...symbolsByFile.values()].flat();
50
+ const groupedByBaseName = new Map();
51
+ for (const symbol of allSymbols) {
52
+ const baseName = symbol.originalQualifiedName ?? symbol.qualifiedName;
53
+ symbol.originalQualifiedName = baseName;
54
+ symbol.displayName = symbol.displayName ?? baseName;
55
+ symbol.aliases = dedupe([
56
+ symbol.name,
57
+ baseName,
58
+ symbol.displayName,
59
+ ...(symbol.aliases ?? []),
60
+ ]);
61
+ const existing = groupedByBaseName.get(baseName);
62
+ if (existing) {
63
+ existing.push(symbol);
64
+ }
65
+ else {
66
+ groupedByBaseName.set(baseName, [symbol]);
67
+ }
68
+ }
69
+ for (const [baseName, group] of groupedByBaseName) {
70
+ if (group.length < 2)
71
+ continue;
72
+ const sorted = [...group].sort((a, b) => a.filePath.localeCompare(b.filePath) ||
73
+ a.startLine - b.startLine ||
74
+ a.kind.localeCompare(b.kind));
75
+ const kindCounts = countByKind(sorted);
76
+ const distinctFileCount = countByFile(sorted).size;
77
+ for (const symbol of sorted) {
78
+ const kindCount = kindCounts.get(symbol.kind) ?? 1;
79
+ const displayName = buildDisplayName(baseName, symbol, kindCount, distinctFileCount);
80
+ const qualifiedName = buildQualifiedName(baseName, symbol, kindCount, distinctFileCount);
81
+ symbol.displayName = displayName;
82
+ symbol.aliases = dedupe([
83
+ symbol.name,
84
+ baseName,
85
+ displayName,
86
+ ...(symbol.aliases ?? []),
87
+ ]);
88
+ symbol.qualifiedName = qualifiedName;
89
+ }
90
+ }
91
+ return new Map(allSymbols.map((symbol) => [symbol.qualifiedName, symbol]));
92
+ }
@@ -0,0 +1,18 @@
1
+ function getModelDisplayName(model) {
2
+ return (model.name ?? model.id).trim();
3
+ }
4
+ export function sortModelsAlphabetically(models) {
5
+ return [...models].sort((left, right) => {
6
+ const byDisplayName = getModelDisplayName(left).localeCompare(getModelDisplayName(right), undefined, {
7
+ sensitivity: "base",
8
+ numeric: true,
9
+ });
10
+ if (byDisplayName !== 0) {
11
+ return byDisplayName;
12
+ }
13
+ return left.id.localeCompare(right.id, undefined, {
14
+ sensitivity: "base",
15
+ numeric: true,
16
+ });
17
+ });
18
+ }