@sean.holung/minicode 0.3.6 → 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 +2 -1
- package/dist/scripts/run-benchmarks.js +1 -0
- package/dist/src/agent/config.js +2 -0
- package/dist/src/agent/editable-config.js +6 -0
- package/dist/src/serve/mcp-server.js +19 -13
- package/dist/src/serve/server.js +29 -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 +103 -23
- package/dist/src/web/index.html +16 -0
- package/dist/src/web/style.css +48 -0
- package/dist/tests/config-api.test.js +5 -0
- package/dist/tests/config.test.js +9 -0
- package/dist/tests/graph-onboarding.test.js +12 -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 +26 -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
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# minicode
|
|
2
2
|
|
|
3
|
-
> Now supports connecting to [OpenRouter](https://openrouter.ai/) account via minicode UI. Sign in with OpenRouter account. Use for free with compatible free tier OpenRouter hosted models from MiniMax, Nvidia, Qwen, Google, etc.
|
|
3
|
+
> Now supports connecting to [OpenRouter](https://openrouter.ai/) account via minicode UI. Sign in with OpenRouter account. Use for free with compatible [free tier OpenRouter hosted models](https://openrouter.ai/models?q=free) from MiniMax, Nvidia, Qwen, Google, etc.
|
|
4
4
|
|
|
5
5
|
A graph-native coding agent and code exploration environment built around structural context optimization that leverages symbol-aware retrieval, dependency graphs, and targeted context. It started as a way to make local models viable under tighter context budgets, and it now also works well with hosted frontier models through the same runtime, web UI, and OpenAI-compatible serve mode.
|
|
6
6
|
|
|
@@ -199,6 +199,7 @@ Nothing is written inside your workspace; config and cache live under `~/.minico
|
|
|
199
199
|
| `COMMAND_DENYLIST` | No | none | Optional JSON array or comma-separated regex patterns appended to the built-in destructive-command denylist |
|
|
200
200
|
| `MAX_STEPS` | No | `50` | Max agent loop iterations per user turn |
|
|
201
201
|
| `MAX_TOKENS` | No | `4096` | Max model output tokens per model call |
|
|
202
|
+
| `MODEL_TIMEOUT_SECONDS` | No | `60` | Timeout waiting for a model API call to start responding before aborting and surfacing an error |
|
|
202
203
|
| `MAX_CONTEXT_TOKENS` | No | `32000` | Approximate session history trimming target. For small models (e.g. 8k context), set lower (e.g. `6000`) to leave room for responses. |
|
|
203
204
|
| `MAX_TOOL_OUTPUT_CHARS` | No | `8000` | Max chars per tool result before truncation. Set to `0` to disable. |
|
|
204
205
|
| `WORKSPACE_ROOT` | No | current working directory | Root directory tools are allowed to access (set at runtime, not typically configured) |
|
|
@@ -99,6 +99,7 @@ export function buildConfig(options = {}) {
|
|
|
99
99
|
model,
|
|
100
100
|
maxSteps: getNumberSetting(getShellOverride("MAX_STEPS"), fileConfig.maxSteps, 50),
|
|
101
101
|
maxTokens: getNumberSetting(getShellOverride("MAX_TOKENS"), fileConfig.maxTokens, 4096),
|
|
102
|
+
modelTimeoutSeconds: getNumberSetting(getShellOverride("MODEL_TIMEOUT_SECONDS"), fileConfig.modelTimeoutSeconds, 60),
|
|
102
103
|
maxContextTokens: getNumberSetting(getShellOverride("MAX_CONTEXT_TOKENS"), fileConfig.maxContextTokens, 32000),
|
|
103
104
|
workspaceRoot: repoRoot,
|
|
104
105
|
commandTimeoutMs: getNumberSetting(getShellOverride("COMMAND_TIMEOUT_MS"), fileConfig.commandTimeoutMs, 30000),
|
package/dist/src/agent/config.js
CHANGED
|
@@ -16,6 +16,7 @@ export function formatConfigForDisplay(config) {
|
|
|
16
16
|
"model: " + config.model,
|
|
17
17
|
"maxSteps: " + config.maxSteps,
|
|
18
18
|
"maxTokens: " + config.maxTokens,
|
|
19
|
+
"modelTimeoutSeconds: " + config.modelTimeoutSeconds,
|
|
19
20
|
"maxContextTokens: " + config.maxContextTokens,
|
|
20
21
|
"commandTimeoutMs: " + config.commandTimeoutMs,
|
|
21
22
|
"maxFileSizeBytes: " + config.maxFileSizeBytes,
|
|
@@ -230,6 +231,7 @@ export async function loadAgentConfig(cwd = process.cwd(), options = {}) {
|
|
|
230
231
|
model: env.MODEL ?? "",
|
|
231
232
|
maxSteps: parseNumber(env.MAX_STEPS, 50),
|
|
232
233
|
maxTokens: parseNumber(env.MAX_TOKENS, 4096),
|
|
234
|
+
modelTimeoutSeconds: parseNumber(env.MODEL_TIMEOUT_SECONDS, 60),
|
|
233
235
|
maxContextTokens: parseNumber(env.MAX_CONTEXT_TOKENS, 32_000),
|
|
234
236
|
workspaceRoot,
|
|
235
237
|
commandTimeoutMs: parseNumber(env.COMMAND_TIMEOUT_MS, 30_000),
|
|
@@ -34,6 +34,12 @@ export const EDITABLE_CONFIG_DEFINITIONS = [
|
|
|
34
34
|
type: "number",
|
|
35
35
|
description: "Maximum completion tokens per model response",
|
|
36
36
|
},
|
|
37
|
+
{
|
|
38
|
+
key: "modelTimeoutSeconds",
|
|
39
|
+
envVar: "MODEL_TIMEOUT_SECONDS",
|
|
40
|
+
type: "number",
|
|
41
|
+
description: "Maximum time to wait for a model API call to start responding",
|
|
42
|
+
},
|
|
37
43
|
{
|
|
38
44
|
key: "maxContextTokens",
|
|
39
45
|
envVar: "MAX_CONTEXT_TOKENS",
|
|
@@ -11,6 +11,7 @@ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/
|
|
|
11
11
|
import { z } from "zod";
|
|
12
12
|
import { getSymbolDisplayName } from "../indexer/symbol-names.js";
|
|
13
13
|
import { formatAmbiguousSymbolMatches, resolveSymbolInput, } from "../shared/symbol-resolution.js";
|
|
14
|
+
import { searchSymbols } from "../shared/symbol-search.js";
|
|
14
15
|
/** Active transports keyed by session ID. */
|
|
15
16
|
const transports = new Map();
|
|
16
17
|
/**
|
|
@@ -157,22 +158,27 @@ function createMcpServer(bridge, emit) {
|
|
|
157
158
|
kind: z.string().optional().describe("Filter by kind: function, class, interface, type, variable, method"),
|
|
158
159
|
}, async ({ query, kind }) => {
|
|
159
160
|
return wrapToolCall("search_code_map", { query, kind }, async () => {
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
161
|
+
const result = searchSymbols(bridge.getSymbols().map((symbol) => ({
|
|
162
|
+
symbol,
|
|
163
|
+
record: {
|
|
164
|
+
name: symbol.name,
|
|
165
|
+
qualifiedName: symbol.qualifiedName,
|
|
166
|
+
kind: symbol.kind,
|
|
167
|
+
filePath: symbol.filePath,
|
|
168
|
+
startLine: symbol.startLine,
|
|
169
|
+
exported: symbol.exported,
|
|
170
|
+
},
|
|
171
|
+
lookupNames: [symbol.name, symbol.qualifiedName],
|
|
172
|
+
})), query, { kind, limit: 30 });
|
|
173
|
+
if (result.total === 0) {
|
|
172
174
|
return { content: [{ type: "text", text: `No symbols matching "${query}"${kind ? ` (kind: ${kind})` : ""}.` }] };
|
|
173
175
|
}
|
|
176
|
+
const matches = result.matches;
|
|
177
|
+
const heading = result.mode === "similar"
|
|
178
|
+
? `No exact substring matches for "${query}"${kind ? ` (kind: ${kind})` : ""}. Showing ${matches.length} similar symbol(s):`
|
|
179
|
+
: `Found ${matches.length} symbol(s) matching "${query}":`;
|
|
174
180
|
const lines = [
|
|
175
|
-
|
|
181
|
+
heading,
|
|
176
182
|
...matches.map((s) => ` - ${s.name} (${s.kind}) — ${s.filePath}:${s.startLine} — qualified: ${s.qualifiedName}${s.signature ? `\n ${s.signature}` : ""}`),
|
|
177
183
|
];
|
|
178
184
|
return { content: [{ type: "text", text: lines.join("\n") }] };
|
package/dist/src/serve/server.js
CHANGED
|
@@ -30,6 +30,19 @@ function sendJson(res, status, body) {
|
|
|
30
30
|
res.writeHead(status, { "Content-Type": "application/json" });
|
|
31
31
|
res.end(JSON.stringify(body));
|
|
32
32
|
}
|
|
33
|
+
function resolveWorkspaceFilePath(workspaceRoot, requestedPath) {
|
|
34
|
+
const trimmedPath = requestedPath.trim();
|
|
35
|
+
if (trimmedPath.length === 0) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
const root = path.resolve(workspaceRoot);
|
|
39
|
+
const absolutePath = path.resolve(root, trimmedPath);
|
|
40
|
+
const relativePath = path.relative(root, absolutePath);
|
|
41
|
+
if (relativePath.startsWith("..") || path.isAbsolute(relativePath)) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
return { absolutePath, relativePath };
|
|
45
|
+
}
|
|
33
46
|
function readBody(req) {
|
|
34
47
|
return new Promise((resolve, reject) => {
|
|
35
48
|
const chunks = [];
|
|
@@ -423,6 +436,22 @@ export function createRequestHandler(bridge, emit, options = {}) {
|
|
|
423
436
|
}
|
|
424
437
|
return;
|
|
425
438
|
}
|
|
439
|
+
if (pathname === "/api/file-source" && method === "GET") {
|
|
440
|
+
const requestedPath = url.searchParams.get("path") ?? "";
|
|
441
|
+
const resolved = resolveWorkspaceFilePath(config.workspaceRoot, requestedPath);
|
|
442
|
+
if (!resolved) {
|
|
443
|
+
sendJson(res, 403, { error: "Invalid workspace file path" });
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
try {
|
|
447
|
+
const source = await readFile(resolved.absolutePath, "utf8");
|
|
448
|
+
sendJson(res, 200, { filePath: resolved.relativePath, source });
|
|
449
|
+
}
|
|
450
|
+
catch {
|
|
451
|
+
sendJson(res, 404, { error: `Could not read file: ${resolved.relativePath}` });
|
|
452
|
+
}
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
426
455
|
if (pathname === "/api/code-map" && method === "GET") {
|
|
427
456
|
const budgetParam = url.searchParams.get("budget");
|
|
428
457
|
const budget = budgetParam ? Number(budgetParam) : undefined;
|
|
@@ -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,
|
package/dist/src/web/app.js
CHANGED
|
@@ -2,6 +2,22 @@ var __defProp = Object.defineProperty;
|
|
|
2
2
|
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
3
3
|
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
4
4
|
|
|
5
|
+
// src/web/modal-state.ts
|
|
6
|
+
function syncBodyModalOpenState() {
|
|
7
|
+
const anyModalOpen = [...document.querySelectorAll(".modal")].some((modal) => !modal.classList.contains("hidden"));
|
|
8
|
+
document.body.classList.toggle("modal-open", anyModalOpen);
|
|
9
|
+
}
|
|
10
|
+
function openModal(modal) {
|
|
11
|
+
modal.classList.remove("hidden");
|
|
12
|
+
modal.setAttribute("aria-hidden", "false");
|
|
13
|
+
syncBodyModalOpenState();
|
|
14
|
+
}
|
|
15
|
+
function closeModal(modal) {
|
|
16
|
+
modal.classList.add("hidden");
|
|
17
|
+
modal.setAttribute("aria-hidden", "true");
|
|
18
|
+
syncBodyModalOpenState();
|
|
19
|
+
}
|
|
20
|
+
|
|
5
21
|
// node_modules/marked/lib/marked.esm.js
|
|
6
22
|
function M() {
|
|
7
23
|
return { async: false, breaks: false, extensions: null, gfm: true, hooks: null, pedantic: false, renderer: null, silent: false, tokenizer: null, walkTokens: null };
|
|
@@ -1660,6 +1676,8 @@ var analysisReport = null;
|
|
|
1660
1676
|
var activeAnalysisFindingId = null;
|
|
1661
1677
|
var activeAnalysisFilter = "all";
|
|
1662
1678
|
var analysisExplanationCache = /* @__PURE__ */ new Map();
|
|
1679
|
+
var filePreviewModalInitialized = false;
|
|
1680
|
+
var latestFilePreviewRequestId = 0;
|
|
1663
1681
|
var LAYOUT_OPTIONS = {
|
|
1664
1682
|
name: "cose",
|
|
1665
1683
|
nodeRepulsion: function() {
|
|
@@ -1685,6 +1703,7 @@ async function initGraph() {
|
|
|
1685
1703
|
initialized = true;
|
|
1686
1704
|
const cyEl = document.getElementById("cy");
|
|
1687
1705
|
const detailEl = document.getElementById("symbol-detail");
|
|
1706
|
+
setupFilePreviewModal();
|
|
1688
1707
|
try {
|
|
1689
1708
|
const [graphRes, symbolsRes, focusRes] = await Promise.all([
|
|
1690
1709
|
fetch("/api/graph"),
|
|
@@ -1823,6 +1842,78 @@ function focusFileInGraph(filePath) {
|
|
|
1823
1842
|
refreshAnalysisGraphState();
|
|
1824
1843
|
runLayout();
|
|
1825
1844
|
}
|
|
1845
|
+
function getFilePreviewLanguage(filePath) {
|
|
1846
|
+
const ext = filePath.split(".").pop()?.toLowerCase() || "";
|
|
1847
|
+
const langMap = {
|
|
1848
|
+
ts: "typescript",
|
|
1849
|
+
tsx: "typescript",
|
|
1850
|
+
js: "javascript",
|
|
1851
|
+
jsx: "javascript",
|
|
1852
|
+
json: "json",
|
|
1853
|
+
md: "markdown",
|
|
1854
|
+
css: "css",
|
|
1855
|
+
html: "xml"
|
|
1856
|
+
};
|
|
1857
|
+
return langMap[ext] || "plaintext";
|
|
1858
|
+
}
|
|
1859
|
+
function closeFilePreview() {
|
|
1860
|
+
const modal = document.getElementById("file-preview-modal");
|
|
1861
|
+
if (!modal) return;
|
|
1862
|
+
closeModal(modal);
|
|
1863
|
+
}
|
|
1864
|
+
function setupFilePreviewModal() {
|
|
1865
|
+
if (filePreviewModalInitialized) return;
|
|
1866
|
+
filePreviewModalInitialized = true;
|
|
1867
|
+
const modal = document.getElementById("file-preview-modal");
|
|
1868
|
+
const backdrop = document.getElementById("file-preview-backdrop");
|
|
1869
|
+
const closeBtn = document.getElementById("file-preview-close");
|
|
1870
|
+
if (!modal || !backdrop || !closeBtn) {
|
|
1871
|
+
return;
|
|
1872
|
+
}
|
|
1873
|
+
backdrop.addEventListener("click", () => closeFilePreview());
|
|
1874
|
+
closeBtn.addEventListener("click", () => closeFilePreview());
|
|
1875
|
+
document.addEventListener("keydown", (event) => {
|
|
1876
|
+
if (event.key === "Escape" && !modal.classList.contains("hidden")) {
|
|
1877
|
+
closeFilePreview();
|
|
1878
|
+
}
|
|
1879
|
+
});
|
|
1880
|
+
}
|
|
1881
|
+
async function openFilePreview(filePath) {
|
|
1882
|
+
const modal = document.getElementById("file-preview-modal");
|
|
1883
|
+
const pathEl = document.getElementById("file-preview-path");
|
|
1884
|
+
const codeEl = document.getElementById("file-preview-code");
|
|
1885
|
+
if (!modal || !pathEl || !codeEl) {
|
|
1886
|
+
return;
|
|
1887
|
+
}
|
|
1888
|
+
latestFilePreviewRequestId += 1;
|
|
1889
|
+
const requestId = latestFilePreviewRequestId;
|
|
1890
|
+
pathEl.textContent = filePath;
|
|
1891
|
+
codeEl.className = "file-preview-code";
|
|
1892
|
+
codeEl.textContent = "Loading...";
|
|
1893
|
+
openModal(modal);
|
|
1894
|
+
try {
|
|
1895
|
+
const res = await fetch(`/api/file-source?path=${encodeURIComponent(filePath)}`);
|
|
1896
|
+
if (!res.ok) {
|
|
1897
|
+
codeEl.textContent = "(file unavailable)";
|
|
1898
|
+
return;
|
|
1899
|
+
}
|
|
1900
|
+
const data = await res.json();
|
|
1901
|
+
if (requestId !== latestFilePreviewRequestId) {
|
|
1902
|
+
return;
|
|
1903
|
+
}
|
|
1904
|
+
pathEl.textContent = data.filePath;
|
|
1905
|
+
codeEl.className = `file-preview-code language-${getFilePreviewLanguage(data.filePath)}`;
|
|
1906
|
+
codeEl.textContent = data.source;
|
|
1907
|
+
if (typeof hljs !== "undefined") {
|
|
1908
|
+
hljs.highlightElement(codeEl);
|
|
1909
|
+
}
|
|
1910
|
+
} catch {
|
|
1911
|
+
if (requestId !== latestFilePreviewRequestId) {
|
|
1912
|
+
return;
|
|
1913
|
+
}
|
|
1914
|
+
codeEl.textContent = "(file unavailable)";
|
|
1915
|
+
}
|
|
1916
|
+
}
|
|
1826
1917
|
async function focusSymbolInGraph(symbolId, options = {}) {
|
|
1827
1918
|
await focusSymbolsInGraph([symbolId], options);
|
|
1828
1919
|
}
|
|
@@ -2349,7 +2440,7 @@ async function showDetail(node, detailEl) {
|
|
|
2349
2440
|
<span class="detail-name">${escapeHtml(data.label)}</span>
|
|
2350
2441
|
<span class="detail-kind-badge" style="background:${kindColor}20;color:${kindColor}">${kind}</span>
|
|
2351
2442
|
</div>
|
|
2352
|
-
<div class="detail-file">${
|
|
2443
|
+
<div class="detail-file">${data.file ? `<button type="button" class="detail-file-link" data-file="${escapeHtml(data.file)}">${escapeHtml(data.file)}${data.startLine ? ":" + data.startLine : ""}</button>` : "unknown"}</div>
|
|
2353
2444
|
`;
|
|
2354
2445
|
html += `<div class="detail-actions">`;
|
|
2355
2446
|
html += `<button class="detail-pin header-btn" data-name="${escapeHtml(data.qualifiedName)}">${isPinned ? "Unpin" : "Pin to focus"}</button>`;
|
|
@@ -2391,6 +2482,12 @@ async function showDetail(node, detailEl) {
|
|
|
2391
2482
|
const name = pinBtn.dataset.name || "";
|
|
2392
2483
|
await togglePin(name, node, pinBtn);
|
|
2393
2484
|
});
|
|
2485
|
+
const fileLink = detailEl.querySelector(".detail-file-link");
|
|
2486
|
+
fileLink?.addEventListener("click", () => {
|
|
2487
|
+
const filePath = fileLink.dataset.file || "";
|
|
2488
|
+
if (!filePath) return;
|
|
2489
|
+
void openFilePreview(filePath);
|
|
2490
|
+
});
|
|
2394
2491
|
const explainBtn = detailEl.querySelector(".detail-explain-btn");
|
|
2395
2492
|
explainBtn.addEventListener("click", () => {
|
|
2396
2493
|
const name = explainBtn.dataset.name || "";
|
|
@@ -2599,6 +2696,7 @@ function setupToolbar() {
|
|
|
2599
2696
|
dropdown.classList.add("hidden");
|
|
2600
2697
|
if (type === "file") {
|
|
2601
2698
|
focusFileInGraph(id);
|
|
2699
|
+
void openFilePreview(id);
|
|
2602
2700
|
return;
|
|
2603
2701
|
}
|
|
2604
2702
|
void focusSymbolInGraph(id, {
|
|
@@ -3170,16 +3268,6 @@ function closeHeaderMenus() {
|
|
|
3170
3268
|
modelDropdown.classList.add("hidden");
|
|
3171
3269
|
sessionDropdown.classList.add("hidden");
|
|
3172
3270
|
}
|
|
3173
|
-
function isSettingsModalOpen() {
|
|
3174
|
-
return !settingsModal.classList.contains("hidden");
|
|
3175
|
-
}
|
|
3176
|
-
function isOpenRouterConnectModalOpen() {
|
|
3177
|
-
return !openRouterConnectModal.classList.contains("hidden");
|
|
3178
|
-
}
|
|
3179
|
-
function syncModalOpenState() {
|
|
3180
|
-
const anyModalOpen = isSettingsModalOpen() || isOpenRouterConnectModalOpen();
|
|
3181
|
-
document.body.classList.toggle("modal-open", anyModalOpen);
|
|
3182
|
-
}
|
|
3183
3271
|
function formatSettingsValue(value) {
|
|
3184
3272
|
return value === null ? "(unset)" : String(value);
|
|
3185
3273
|
}
|
|
@@ -3394,30 +3482,22 @@ function updateSettingsActions() {
|
|
|
3394
3482
|
}
|
|
3395
3483
|
function openSettings() {
|
|
3396
3484
|
closeHeaderMenus();
|
|
3397
|
-
settingsModal
|
|
3398
|
-
settingsModal.setAttribute("aria-hidden", "false");
|
|
3399
|
-
syncModalOpenState();
|
|
3485
|
+
openModal(settingsModal);
|
|
3400
3486
|
void loadSettings();
|
|
3401
3487
|
}
|
|
3402
3488
|
function closeSettings() {
|
|
3403
|
-
settingsModal
|
|
3404
|
-
settingsModal.setAttribute("aria-hidden", "true");
|
|
3405
|
-
syncModalOpenState();
|
|
3489
|
+
closeModal(settingsModal);
|
|
3406
3490
|
clearSettingsBanner();
|
|
3407
3491
|
}
|
|
3408
3492
|
function openOpenRouterConnectModal() {
|
|
3409
3493
|
closeHeaderMenus();
|
|
3410
|
-
openRouterConnectModal
|
|
3411
|
-
openRouterConnectModal.setAttribute("aria-hidden", "false");
|
|
3494
|
+
openModal(openRouterConnectModal);
|
|
3412
3495
|
openRouterPersistCheckbox.checked = false;
|
|
3413
3496
|
openRouterConnectContinueBtn.disabled = false;
|
|
3414
|
-
syncModalOpenState();
|
|
3415
3497
|
}
|
|
3416
3498
|
function closeOpenRouterConnectModal() {
|
|
3417
|
-
openRouterConnectModal
|
|
3418
|
-
openRouterConnectModal.setAttribute("aria-hidden", "true");
|
|
3499
|
+
closeModal(openRouterConnectModal);
|
|
3419
3500
|
openRouterConnectContinueBtn.disabled = false;
|
|
3420
|
-
syncModalOpenState();
|
|
3421
3501
|
}
|
|
3422
3502
|
chatForm.addEventListener("submit", (e) => {
|
|
3423
3503
|
e.preventDefault();
|
package/dist/src/web/index.html
CHANGED
|
@@ -235,6 +235,22 @@ MODEL=your-model-name</pre>
|
|
|
235
235
|
</section>
|
|
236
236
|
</div>
|
|
237
237
|
|
|
238
|
+
<div id="file-preview-modal" class="modal hidden" aria-hidden="true">
|
|
239
|
+
<div id="file-preview-backdrop" class="modal-backdrop"></div>
|
|
240
|
+
<section class="modal-panel modal-panel-file-preview" role="dialog" aria-modal="true" aria-labelledby="file-preview-title">
|
|
241
|
+
<div class="modal-header">
|
|
242
|
+
<div>
|
|
243
|
+
<h2 id="file-preview-title">File preview</h2>
|
|
244
|
+
<p id="file-preview-path" class="modal-subtitle">Loading…</p>
|
|
245
|
+
</div>
|
|
246
|
+
<button id="file-preview-close" class="header-btn" type="button">Close</button>
|
|
247
|
+
</div>
|
|
248
|
+
<div class="file-preview-body">
|
|
249
|
+
<pre id="file-preview-code" class="file-preview-code">Loading...</pre>
|
|
250
|
+
</div>
|
|
251
|
+
</section>
|
|
252
|
+
</div>
|
|
253
|
+
|
|
238
254
|
<script type="module" src="app.js"></script>
|
|
239
255
|
</body>
|
|
240
256
|
</html>
|