@sean.holung/minicode 0.3.5 → 0.3.6

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.
@@ -1,5 +1,5 @@
1
1
  import { formatConfigForDisplay, MINICODE_HOME, resolveConfigEnv } from "../agent/config.js";
2
- import { formatPersistedConfigValue, getEditableConfigDefinition, getEffectiveEditableConfigValue, isEditableConfigKey, listEditableConfigDefinitions, loadPersistedConfig, setPersistedConfigValue, unsetPersistedConfigValue, } from "../agent/editable-config.js";
2
+ import { buildStructuredConfigPayload, formatPersistedConfigValue, getEditableConfigDefinition, getEffectiveEditableConfigValue, isEditableConfigKey, listEditableConfigDefinitions, setPersistedConfigValue, unsetPersistedConfigValue, } from "../agent/editable-config.js";
3
3
  function renderUsage() {
4
4
  return [
5
5
  'Usage:',
@@ -12,7 +12,7 @@ function renderUsage() {
12
12
  }
13
13
  function renderEditableKeys() {
14
14
  const lines = [
15
- "Editable config keys (persisted in ~/.minicode/agent.config.json; environment variables take precedence):",
15
+ "Editable config keys (persisted in ~/.minicode/.env; exported shell environment variables take precedence):",
16
16
  ];
17
17
  for (const definition of listEditableConfigDefinitions()) {
18
18
  const valueHint = definition.type === "enum"
@@ -21,7 +21,7 @@ function renderEditableKeys() {
21
21
  lines.push(` ${definition.key} ${valueHint} — ${definition.description} (env: ${definition.envVar})`);
22
22
  }
23
23
  lines.push("");
24
- lines.push('Use "/config set <key> <value>" to update your global config.');
24
+ lines.push('Use "/config set <key> <value>" to update ~/.minicode/.env.');
25
25
  lines.push("Secrets like API keys stay env-only for now.");
26
26
  return lines.join("\n");
27
27
  }
@@ -31,14 +31,16 @@ async function renderConfigValue(key, context) {
31
31
  }
32
32
  const minicodeHome = context.minicodeHome ?? MINICODE_HOME;
33
33
  const definition = getEditableConfigDefinition(key);
34
- const persisted = await loadPersistedConfig(minicodeHome);
35
- const env = await resolveConfigEnv({ minicodeHome });
36
- const envValue = env.values[definition.envVar];
34
+ const payload = await buildStructuredConfigPayload(context.config, minicodeHome);
35
+ const entry = payload.entries.find((item) => item.key === key);
36
+ if (!entry) {
37
+ return `Unknown editable config key "${key}".\n\n${renderEditableKeys()}`;
38
+ }
37
39
  return [
38
40
  `${definition.key}`,
39
41
  ` effective: ${getEffectiveEditableConfigValue(context.config, key)}`,
40
- ` config file: ${formatPersistedConfigValue(persisted[definition.fileKey])}`,
41
- ` env override (${definition.envVar}): ${formatPersistedConfigValue(envValue)}`,
42
+ ` saved in ~/.minicode/.env: ${formatPersistedConfigValue(entry.persistedValue)}`,
43
+ ` exported env override (${definition.envVar}): ${formatPersistedConfigValue(entry.envValue)}`,
42
44
  ].join("\n");
43
45
  }
44
46
  async function persistConfigValue(key, rawValue, context) {
@@ -59,8 +61,8 @@ async function persistConfigValue(key, rawValue, context) {
59
61
  `File: ${result.path}`,
60
62
  "Restart minicode to pick up persisted config changes in a new session.",
61
63
  ];
62
- if (env.values[definition.envVar] !== undefined) {
63
- lines.push(`Note: ${definition.envVar} is currently set and will override this persisted value until it is unset.`);
64
+ if (env.sources[definition.envVar] === "process") {
65
+ lines.push(`Note: ${definition.envVar} is currently exported in your shell and will override this saved value until it is unset.`);
64
66
  }
65
67
  return lines.join("\n");
66
68
  }
@@ -82,11 +84,11 @@ async function removeConfigValue(key, context) {
82
84
  });
83
85
  const lines = [
84
86
  `Removed persisted value for "${key}".`,
85
- `File: ${minicodeHome}/agent.config.json`,
87
+ `File: ${minicodeHome}/.env`,
86
88
  "Restart minicode to ensure the updated config is applied in a new session.",
87
89
  ];
88
- if (env.values[definition.envVar] !== undefined) {
89
- lines.push(`Note: ${definition.envVar} is still set in the environment, so the effective value may remain unchanged.`);
90
+ if (env.sources[definition.envVar] === "process") {
91
+ lines.push(`Note: ${definition.envVar} is still exported in your shell, so the effective value may remain unchanged.`);
90
92
  }
91
93
  return lines.join("\n");
92
94
  }
@@ -13,9 +13,11 @@ import { getSymbolDisplayName } from "../indexer/symbol-names.js";
13
13
  export class AgentBridge {
14
14
  agent;
15
15
  config;
16
+ baseConfig;
16
17
  modelClient;
17
18
  projectIndex;
18
- buildAgent;
19
+ toolRegistry;
20
+ sessionOpenRouterConnected = false;
19
21
  busy = false;
20
22
  abortController = null;
21
23
  broadcast;
@@ -43,9 +45,8 @@ export class AgentBridge {
43
45
  }
44
46
  async init() {
45
47
  const config = await loadAgentConfig();
46
- let modelClient;
47
48
  try {
48
- modelClient = createModelClient(config);
49
+ this.modelClient = createModelClient(config);
49
50
  }
50
51
  catch {
51
52
  // Model client may fail to initialize if API keys are missing.
@@ -75,28 +76,47 @@ export class AgentBridge {
75
76
  return this.appendAnnotationsToResult(name, input, result);
76
77
  };
77
78
  this.config = config;
79
+ this.baseConfig = AgentBridge.cloneConfig(config);
78
80
  this.projectIndex = projectIndex;
79
- if (modelClient) {
80
- this.modelClient = modelClient;
81
- this.buildAgent = (session, onUiUpdate) => {
82
- return new CodingAgent({
83
- config,
84
- modelClient,
85
- toolRegistry,
86
- verbose: this.verbose,
87
- ...(session ? { session } : {}),
88
- ...(projectIndex !== undefined
89
- ? { getCodeMap: (focusSymbols) => projectIndex.getCodeMap(undefined, focusSymbols) }
90
- : {}),
91
- onUiUpdate: onUiUpdate ?? ((event) => {
92
- this.emit(event);
93
- }),
94
- getSystemPromptSuffix: () => this.buildAnnotationSuffix(),
95
- });
96
- };
97
- this.agent = this.buildAgent();
81
+ this.toolRegistry = toolRegistry;
82
+ if (this.modelClient) {
83
+ this.agent = this.createAgent();
98
84
  }
99
85
  }
86
+ static cloneConfig(config) {
87
+ return {
88
+ ...config,
89
+ commandDenylist: [...config.commandDenylist],
90
+ };
91
+ }
92
+ static applyConfig(target, source) {
93
+ const targetRecord = target;
94
+ for (const key of Object.keys(targetRecord)) {
95
+ if (!(key in source)) {
96
+ delete targetRecord[key];
97
+ }
98
+ }
99
+ Object.assign(targetRecord, AgentBridge.cloneConfig(source));
100
+ }
101
+ createAgent(session, onUiUpdate) {
102
+ if (!this.modelClient || !this.toolRegistry) {
103
+ throw new Error("Agent runtime is not initialized.");
104
+ }
105
+ return new CodingAgent({
106
+ config: this.config,
107
+ modelClient: this.modelClient,
108
+ toolRegistry: this.toolRegistry,
109
+ verbose: this.verbose,
110
+ ...(session ? { session } : {}),
111
+ ...(this.projectIndex !== undefined
112
+ ? { getCodeMap: (focusSymbols) => this.projectIndex.getCodeMap(undefined, focusSymbols) }
113
+ : {}),
114
+ onUiUpdate: onUiUpdate ?? ((event) => {
115
+ this.emit(event);
116
+ }),
117
+ getSystemPromptSuffix: () => this.buildAnnotationSuffix(),
118
+ });
119
+ }
100
120
  // ── File watcher for automatic reindexing ──
101
121
  static WATCH_EXTENSIONS = new Set([".ts", ".tsx", ".js", ".jsx"]);
102
122
  static SKIP_DIRS = new Set(["node_modules", ".git", "dist", "build", "coverage"]);
@@ -171,6 +191,45 @@ export class AgentBridge {
171
191
  getConfig() {
172
192
  return this.config;
173
193
  }
194
+ isOpenRouterSessionConnected() {
195
+ return this.sessionOpenRouterConnected;
196
+ }
197
+ connectOpenRouter(apiKey) {
198
+ const trimmedKey = apiKey.trim();
199
+ if (!trimmedKey) {
200
+ throw new Error("OpenRouter OAuth exchange did not return an API key.");
201
+ }
202
+ if (this.busy) {
203
+ throw new Error("busy");
204
+ }
205
+ const currentSession = this.agent?.getSession();
206
+ this.config.modelProvider = "openai-compatible";
207
+ this.config.openAiBaseUrl = "https://openrouter.ai/api/v1";
208
+ this.config.openAiApiKey = trimmedKey;
209
+ this.sessionOpenRouterConnected = true;
210
+ this.modelClient = createModelClient(this.config);
211
+ this.agent = this.createAgent(currentSession);
212
+ }
213
+ disconnectOpenRouter() {
214
+ if (this.busy) {
215
+ throw new Error("busy");
216
+ }
217
+ if (!this.sessionOpenRouterConnected) {
218
+ return false;
219
+ }
220
+ const currentSession = this.agent?.getSession();
221
+ AgentBridge.applyConfig(this.config, this.baseConfig);
222
+ this.sessionOpenRouterConnected = false;
223
+ try {
224
+ this.modelClient = createModelClient(this.config);
225
+ this.agent = this.createAgent(currentSession);
226
+ }
227
+ catch {
228
+ this.modelClient = undefined;
229
+ this.agent = undefined;
230
+ }
231
+ return true;
232
+ }
174
233
  requireAgent() {
175
234
  if (!this.agent) {
176
235
  throw new Error("Agent is not configured. Set MODEL and provider settings in ~/.minicode/.env");
@@ -230,13 +289,13 @@ export class AgentBridge {
230
289
  return saveSession(agent.getSession(), label, annotationsObj);
231
290
  }
232
291
  async loadSess(label) {
233
- if (!this.buildAgent) {
292
+ if (!this.modelClient) {
234
293
  throw new Error("Agent is not configured.");
235
294
  }
236
295
  const result = (await loadSessionByLabel(label)) ?? (await loadSession(label));
237
296
  if (!result)
238
297
  return null;
239
- this.agent = this.buildAgent(result.session);
298
+ this.agent = this.createAgent(result.session);
240
299
  // Restore annotations from saved session
241
300
  this.annotations.clear();
242
301
  if (result.annotations) {
@@ -473,12 +532,12 @@ export class AgentBridge {
473
532
  async explainSymbol(name, onEvent, signal) {
474
533
  if (!this.projectIndex)
475
534
  throw new Error("No project index");
476
- if (!this.buildAgent)
535
+ if (!this.modelClient)
477
536
  throw new Error("Agent is not configured.");
478
537
  const sym = this.projectIndex.getSymbol(name);
479
538
  if (!sym)
480
539
  throw new Error(`Symbol "${name}" not found`);
481
- const explainAgent = this.buildAgent(undefined, onEvent);
540
+ const explainAgent = this.createAgent(undefined, onEvent);
482
541
  const prompt = `Explain "${sym.name}" (${sym.kind} in ${sym.filePath}).
483
542
  Use read_symbol, get_dependencies, find_references to gather context.
484
543
  Explain what it does, how it works, what depends on it, and key design decisions.
@@ -491,12 +550,12 @@ Be concise but thorough.`;
491
550
  const report = this.getStructuralAnalysis();
492
551
  if (!report)
493
552
  throw new Error("No project index");
494
- if (!this.buildAgent)
553
+ if (!this.modelClient)
495
554
  throw new Error("Agent is not configured.");
496
555
  const finding = report.findings.find((item) => item.id === findingId);
497
556
  if (!finding)
498
557
  throw new Error(`Structural finding "${findingId}" not found`);
499
- const explainAgent = this.buildAgent(undefined, onEvent);
558
+ const explainAgent = this.createAgent(undefined, onEvent);
500
559
  const affectedSymbols = finding.symbols.length > 0
501
560
  ? finding.symbols.slice(0, 8).join(", ")
502
561
  : "(none)";
@@ -8,9 +8,11 @@ import { createWebSocketServer } from "./websocket.js";
8
8
  import { handleChatCompletions, handleModels } from "./openai-compat.js";
9
9
  import { formatConfigForDisplay, getConfigMissing } from "../agent/config.js";
10
10
  import { applyPersistedConfigUpdates, buildStructuredConfigPayload } from "../agent/editable-config.js";
11
+ import { getHomeEnvPath, upsertHomeEnvValues } from "../agent/home-env.js";
11
12
  import { sortModelsAlphabetically } from "../model-utils.js";
12
13
  import { serializeSymbolMatch } from "../shared/symbol-resolution.js";
13
14
  import { handleMcpRequest } from "./mcp-server.js";
15
+ import { buildSessionPreview } from "../session/session-preview.js";
14
16
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
15
17
  // Resolve web dir: always serve from dist/src/web (built by scripts/build-web.mjs)
16
18
  // In dev (tsx): __dirname = src/serve → go up to project root, then dist/src/web
@@ -49,7 +51,12 @@ async function serveStatic(res, urlPath) {
49
51
  const content = await readFile(filePath);
50
52
  const ext = path.extname(filePath);
51
53
  const contentType = MIME_TYPES[ext] ?? "application/octet-stream";
52
- res.writeHead(200, { "Content-Type": contentType });
54
+ res.writeHead(200, {
55
+ "Content-Type": contentType,
56
+ // This local UI changes often during development. Avoid stale browser bundles
57
+ // causing the app to run an older client against a newer server.
58
+ "Cache-Control": "no-store",
59
+ });
53
60
  res.end(content);
54
61
  }
55
62
  catch {
@@ -62,7 +69,6 @@ async function buildWebSettingsPayload(config, minicodeHome) {
62
69
  }
63
70
  /** Create the HTTP request handler. Exported for testing. */
64
71
  export function createRequestHandler(bridge, emit, options = {}) {
65
- const config = bridge.getConfig();
66
72
  const emitFn = emit ?? (() => { });
67
73
  const minicodeHome = options.minicodeHome;
68
74
  return (req, res) => {
@@ -70,6 +76,7 @@ export function createRequestHandler(bridge, emit, options = {}) {
70
76
  const method = req.method ?? "GET";
71
77
  const pathname = url.pathname;
72
78
  const handle = async () => {
79
+ const config = bridge.getConfig();
73
80
  // MCP (Model Context Protocol) endpoint
74
81
  if (pathname === "/mcp") {
75
82
  await handleMcpRequest(req, res, bridge, emitFn);
@@ -92,6 +99,8 @@ export function createRequestHandler(bridge, emit, options = {}) {
92
99
  workspace: config.workspaceRoot,
93
100
  model: config.model,
94
101
  provider: config.modelProvider,
102
+ baseUrl: config.openAiBaseUrl,
103
+ sessionOpenRouterConnected: bridge.isOpenRouterSessionConnected(),
95
104
  needsSetup: missing.length > 0,
96
105
  missing,
97
106
  });
@@ -102,6 +111,132 @@ export function createRequestHandler(bridge, emit, options = {}) {
102
111
  sendJson(res, 200, { models, activeModel: config.model });
103
112
  return;
104
113
  }
114
+ if (pathname === "/api/openrouter/connect" && method === "POST") {
115
+ const body = JSON.parse(await readBody(req));
116
+ if (!body.code || typeof body.code !== "string") {
117
+ sendJson(res, 400, { error: "code is required" });
118
+ return;
119
+ }
120
+ if (!body.codeVerifier || typeof body.codeVerifier !== "string") {
121
+ sendJson(res, 400, { error: "codeVerifier is required" });
122
+ return;
123
+ }
124
+ let exchangeResponse;
125
+ try {
126
+ exchangeResponse = await fetch("https://openrouter.ai/api/v1/auth/keys", {
127
+ method: "POST",
128
+ headers: {
129
+ "Content-Type": "application/json",
130
+ },
131
+ body: JSON.stringify({
132
+ code: body.code,
133
+ code_verifier: body.codeVerifier,
134
+ code_challenge_method: "S256",
135
+ }),
136
+ });
137
+ }
138
+ catch (error) {
139
+ const message = error instanceof Error ? error.message : "OpenRouter OAuth exchange failed";
140
+ sendJson(res, 502, { error: message });
141
+ return;
142
+ }
143
+ if (!exchangeResponse.ok) {
144
+ const message = await exchangeResponse.text();
145
+ sendJson(res, exchangeResponse.status, {
146
+ error: message.trim().length > 0
147
+ ? `OpenRouter OAuth exchange failed: ${message}`
148
+ : `OpenRouter OAuth exchange failed (${exchangeResponse.status})`,
149
+ });
150
+ return;
151
+ }
152
+ const payload = await exchangeResponse.json();
153
+ if (!payload.key || typeof payload.key !== "string") {
154
+ sendJson(res, 502, { error: "OpenRouter OAuth exchange did not return an API key." });
155
+ return;
156
+ }
157
+ try {
158
+ bridge.connectOpenRouter(payload.key);
159
+ }
160
+ catch (error) {
161
+ const message = error instanceof Error ? error.message : "Failed to configure OpenRouter";
162
+ sendJson(res, message === "busy" ? 409 : 400, { error: message });
163
+ return;
164
+ }
165
+ let persistedToEnv = false;
166
+ let persistedEnvPath = null;
167
+ let persistWarning = null;
168
+ if (body.persistToEnv === true) {
169
+ try {
170
+ const result = await upsertHomeEnvValues({
171
+ values: {
172
+ MODEL_PROVIDER: "openai-compatible",
173
+ OPENAI_BASE_URL: "https://openrouter.ai/api/v1",
174
+ OPENROUTER_API_KEY: payload.key,
175
+ },
176
+ ...(minicodeHome ? { minicodeHome } : {}),
177
+ });
178
+ persistedToEnv = true;
179
+ persistedEnvPath = result.path;
180
+ }
181
+ catch (error) {
182
+ const message = error instanceof Error ? error.message : "Failed to update ~/.minicode/.env";
183
+ persistedEnvPath = getHomeEnvPath(minicodeHome);
184
+ persistWarning = `OpenRouter connected for this serve session, but minicode could not update ${persistedEnvPath}: ${message}`;
185
+ }
186
+ }
187
+ const missing = getConfigMissing(config);
188
+ const onlyModelMissing = missing.length === 1 && missing[0] === "MODEL is not set";
189
+ const message = persistWarning
190
+ ? `${persistWarning}${onlyModelMissing ? " Select a model to continue." : ""}`
191
+ : persistedToEnv
192
+ ? (onlyModelMissing
193
+ ? "OpenRouter connected for this serve session and saved to ~/.minicode/.env. Select a model to continue, and minicode will remember it for future runs."
194
+ : "OpenRouter connected for this serve session and saved to ~/.minicode/.env for future runs.")
195
+ : (onlyModelMissing
196
+ ? "OpenRouter connected for this serve session. Select a model to continue."
197
+ : "OpenRouter connected for this serve session.");
198
+ sendJson(res, 200, {
199
+ ok: true,
200
+ sessionOnly: true,
201
+ persistedToEnv,
202
+ persistedEnvPath,
203
+ persistWarning,
204
+ provider: config.modelProvider,
205
+ model: config.model,
206
+ baseUrl: config.openAiBaseUrl,
207
+ needsSetup: missing.length > 0,
208
+ missing,
209
+ message,
210
+ });
211
+ return;
212
+ }
213
+ if (pathname === "/api/openrouter/disconnect" && method === "POST") {
214
+ let disconnected = false;
215
+ try {
216
+ disconnected = bridge.disconnectOpenRouter();
217
+ }
218
+ catch (error) {
219
+ const message = error instanceof Error ? error.message : "Failed to remove OpenRouter session";
220
+ sendJson(res, message === "busy" ? 409 : 400, { error: message });
221
+ return;
222
+ }
223
+ const missing = getConfigMissing(config);
224
+ const body = {
225
+ ok: true,
226
+ disconnected,
227
+ sessionOnly: true,
228
+ provider: config.modelProvider,
229
+ model: config.model,
230
+ baseUrl: config.openAiBaseUrl,
231
+ needsSetup: missing.length > 0,
232
+ missing,
233
+ message: disconnected
234
+ ? "Removed the session-only OpenRouter connection and restored your original provider settings."
235
+ : "No session-only OpenRouter connection was active.",
236
+ };
237
+ sendJson(res, 200, body);
238
+ return;
239
+ }
105
240
  if (pathname === "/api/model" && method === "POST") {
106
241
  const body = JSON.parse(await readBody(req));
107
242
  if (!body.model || typeof body.model !== "string") {
@@ -109,7 +244,26 @@ export function createRequestHandler(bridge, emit, options = {}) {
109
244
  return;
110
245
  }
111
246
  bridge.switchModel(body.model);
112
- sendJson(res, 200, { model: body.model });
247
+ let persistedToEnv = false;
248
+ let persistedEnvPath = null;
249
+ let message;
250
+ try {
251
+ const result = await upsertHomeEnvValues({
252
+ values: {
253
+ MODEL: body.model,
254
+ },
255
+ ...(minicodeHome ? { minicodeHome } : {}),
256
+ });
257
+ persistedToEnv = true;
258
+ persistedEnvPath = result.path;
259
+ message = `Saved MODEL=${body.model} to ~/.minicode/.env.`;
260
+ }
261
+ catch (error) {
262
+ const persistMessage = error instanceof Error ? error.message : "Failed to update ~/.minicode/.env";
263
+ sendJson(res, 500, { error: persistMessage });
264
+ return;
265
+ }
266
+ sendJson(res, 200, { model: body.model, persistedToEnv, persistedEnvPath, message });
113
267
  return;
114
268
  }
115
269
  if (pathname === "/api/context" && method === "GET") {
@@ -180,7 +334,10 @@ export function createRequestHandler(bridge, emit, options = {}) {
180
334
  sendJson(res, 404, { error: "Session not found" });
181
335
  return;
182
336
  }
183
- sendJson(res, 200, { label: result.label });
337
+ sendJson(res, 200, {
338
+ label: result.label,
339
+ messages: buildSessionPreview(result.session.getMessages()),
340
+ });
184
341
  return;
185
342
  }
186
343
  // ── Graph / Index API ──
@@ -0,0 +1,14 @@
1
+ export const DEFAULT_SESSION_PREVIEW_LIMIT = 10;
2
+ const COMPACTION_SUMMARY_PREFIX = "[Conversation Summary";
3
+ export function isCompactionSummaryMessage(message) {
4
+ return (message.role === "user" &&
5
+ message.content.startsWith(COMPACTION_SUMMARY_PREFIX));
6
+ }
7
+ export function buildSessionPreview(messages, limit = DEFAULT_SESSION_PREVIEW_LIMIT) {
8
+ if (limit <= 0) {
9
+ return [];
10
+ }
11
+ return messages
12
+ .filter((message) => !isCompactionSummaryMessage(message))
13
+ .slice(-limit);
14
+ }
@@ -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
+ }