@mrclrchtr/supi-code-intelligence 1.3.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/README.md +70 -32
  2. package/node_modules/@mrclrchtr/supi-core/README.md +52 -41
  3. package/node_modules/@mrclrchtr/supi-core/package.json +1 -1
  4. package/node_modules/@mrclrchtr/supi-core/src/api.ts +13 -13
  5. package/node_modules/@mrclrchtr/supi-core/src/{config-settings.ts → config/config-settings.ts} +2 -2
  6. package/node_modules/@mrclrchtr/{supi-lsp/node_modules/@mrclrchtr/supi-core/src → supi-core/src/context}/context-provider-registry.ts +1 -1
  7. package/node_modules/@mrclrchtr/supi-core/src/extension.ts +1 -1
  8. package/node_modules/@mrclrchtr/supi-core/src/index.ts +13 -13
  9. package/node_modules/@mrclrchtr/{supi-lsp/node_modules/@mrclrchtr/supi-core/src → supi-core/src/settings}/settings-registry.ts +1 -1
  10. package/node_modules/@mrclrchtr/supi-lsp/README.md +58 -39
  11. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/README.md +52 -41
  12. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/package.json +1 -1
  13. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/api.ts +13 -13
  14. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/{config-settings.ts → config/config-settings.ts} +2 -2
  15. package/node_modules/@mrclrchtr/{supi-core/src → supi-lsp/node_modules/@mrclrchtr/supi-core/src/context}/context-provider-registry.ts +1 -1
  16. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/extension.ts +1 -1
  17. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/index.ts +13 -13
  18. package/node_modules/@mrclrchtr/{supi-core/src → supi-lsp/node_modules/@mrclrchtr/supi-core/src/settings}/settings-registry.ts +1 -1
  19. package/node_modules/@mrclrchtr/supi-lsp/package.json +5 -3
  20. package/node_modules/@mrclrchtr/supi-lsp/src/api.ts +16 -3
  21. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-refresh.ts +1 -1
  22. package/node_modules/@mrclrchtr/supi-lsp/src/client/client.ts +27 -3
  23. package/node_modules/@mrclrchtr/supi-lsp/src/client/transport.ts +61 -5
  24. package/node_modules/@mrclrchtr/supi-lsp/src/config/tsconfig-scope.ts +244 -0
  25. package/node_modules/@mrclrchtr/supi-lsp/src/{types.ts → config/types.ts} +4 -2
  26. package/node_modules/@mrclrchtr/supi-lsp/src/coordinates.ts +11 -0
  27. package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/diagnostic-augmentation.ts +5 -5
  28. package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/diagnostic-context.ts +115 -0
  29. package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/diagnostic-display.ts +1 -1
  30. package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/diagnostic-summary.ts +3 -2
  31. package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/diagnostics.ts +1 -1
  32. package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/stale-diagnostics.ts +1 -1
  33. package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/suppression-diagnostics.ts +1 -1
  34. package/node_modules/@mrclrchtr/supi-lsp/src/{workspace-sentinels.ts → diagnostics/workspace-sentinels.ts} +2 -2
  35. package/node_modules/@mrclrchtr/supi-lsp/src/format.ts +2 -23
  36. package/node_modules/@mrclrchtr/supi-lsp/src/index.ts +18 -5
  37. package/node_modules/@mrclrchtr/supi-lsp/src/lsp.ts +72 -120
  38. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-diagnostics.ts +1 -1
  39. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-helpers.ts +4 -2
  40. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-project-info.ts +10 -7
  41. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-workspace-recovery.ts +1 -1
  42. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-workspace-symbol.ts +158 -6
  43. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager.ts +202 -43
  44. package/node_modules/@mrclrchtr/supi-lsp/src/{lsp-state.ts → session/lsp-state.ts} +22 -11
  45. package/node_modules/@mrclrchtr/supi-lsp/src/{scanner.ts → session/scanner.ts} +3 -3
  46. package/node_modules/@mrclrchtr/supi-lsp/src/{service-registry.ts → session/service-registry.ts} +104 -12
  47. package/node_modules/@mrclrchtr/supi-lsp/src/{settings-registration.ts → session/settings-registration.ts} +1 -1
  48. package/node_modules/@mrclrchtr/supi-lsp/src/session/tree-persist.ts +75 -0
  49. package/node_modules/@mrclrchtr/supi-lsp/src/summary.ts +1 -1
  50. package/node_modules/@mrclrchtr/supi-lsp/src/tool/guidance.ts +138 -0
  51. package/node_modules/@mrclrchtr/supi-lsp/src/tool/names.ts +19 -0
  52. package/node_modules/@mrclrchtr/supi-lsp/src/{overrides.ts → tool/overrides.ts} +55 -24
  53. package/node_modules/@mrclrchtr/supi-lsp/src/tool/register-tools.ts +224 -0
  54. package/node_modules/@mrclrchtr/supi-lsp/src/tool/service-actions.ts +258 -0
  55. package/node_modules/@mrclrchtr/supi-lsp/src/{ui.ts → ui/ui.ts} +4 -4
  56. package/node_modules/@mrclrchtr/supi-lsp/src/utils.ts +11 -0
  57. package/node_modules/@mrclrchtr/supi-tree-sitter/README.md +46 -39
  58. package/node_modules/@mrclrchtr/supi-tree-sitter/package.json +1 -1
  59. package/node_modules/@mrclrchtr/supi-tree-sitter/src/api.ts +1 -1
  60. package/node_modules/@mrclrchtr/supi-tree-sitter/src/index.ts +1 -1
  61. package/node_modules/@mrclrchtr/supi-tree-sitter/src/{runtime.ts → session/runtime.ts} +3 -3
  62. package/node_modules/@mrclrchtr/supi-tree-sitter/src/{session.ts → session/session.ts} +4 -4
  63. package/node_modules/@mrclrchtr/supi-tree-sitter/src/{callees.ts → tool/callees.ts} +3 -3
  64. package/node_modules/@mrclrchtr/supi-tree-sitter/src/{exports.ts → tool/exports.ts} +4 -4
  65. package/node_modules/@mrclrchtr/supi-tree-sitter/src/{formatting.ts → tool/formatting.ts} +1 -1
  66. package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/guidance.ts +22 -0
  67. package/node_modules/@mrclrchtr/supi-tree-sitter/src/{imports.ts → tool/imports.ts} +4 -4
  68. package/node_modules/@mrclrchtr/supi-tree-sitter/src/{node-at.ts → tool/node-at.ts} +3 -3
  69. package/node_modules/@mrclrchtr/supi-tree-sitter/src/{outline.ts → tool/outline.ts} +3 -3
  70. package/node_modules/@mrclrchtr/supi-tree-sitter/src/tree-sitter.ts +6 -29
  71. package/package.json +8 -5
  72. package/src/actions/affected-action.ts +4 -4
  73. package/src/actions/brief-action.ts +12 -13
  74. package/src/actions/callees-action.ts +14 -10
  75. package/src/actions/callers-action.ts +4 -4
  76. package/src/actions/implementations-action.ts +4 -4
  77. package/src/code-intelligence.ts +1 -1
  78. package/src/pattern-structured.ts +20 -22
  79. package/src/providers/semantic-provider.ts +34 -0
  80. package/src/providers/structural-provider.ts +14 -0
  81. package/src/target-resolution.ts +26 -35
  82. package/src/tool/guidance.ts +21 -0
  83. package/node_modules/@mrclrchtr/supi-lsp/src/guidance.ts +0 -163
  84. package/node_modules/@mrclrchtr/supi-lsp/src/search-fallback.ts +0 -98
  85. package/node_modules/@mrclrchtr/supi-lsp/src/tool-actions.ts +0 -430
  86. package/node_modules/@mrclrchtr/supi-lsp/src/tree-persist.ts +0 -48
  87. package/node_modules/@mrclrchtr/supi-lsp/src/tsconfig-scope.ts +0 -156
  88. package/src/guidance.ts +0 -42
  89. /package/node_modules/@mrclrchtr/supi-core/src/{config.ts → config/config.ts} +0 -0
  90. /package/node_modules/@mrclrchtr/supi-core/src/{context-messages.ts → context/context-messages.ts} +0 -0
  91. /package/node_modules/@mrclrchtr/supi-core/src/{context-tag.ts → context/context-tag.ts} +0 -0
  92. /package/node_modules/@mrclrchtr/supi-core/src/{settings-command.ts → settings/settings-command.ts} +0 -0
  93. /package/node_modules/@mrclrchtr/supi-core/src/{settings-ui.ts → settings/settings-ui.ts} +0 -0
  94. /package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/{config.ts → config/config.ts} +0 -0
  95. /package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/{context-messages.ts → context/context-messages.ts} +0 -0
  96. /package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/{context-tag.ts → context/context-tag.ts} +0 -0
  97. /package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/{settings-command.ts → settings/settings-command.ts} +0 -0
  98. /package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/{settings-ui.ts → settings/settings-ui.ts} +0 -0
  99. /package/node_modules/@mrclrchtr/supi-lsp/src/{capabilities.ts → config/capabilities.ts} +0 -0
  100. /package/node_modules/@mrclrchtr/supi-lsp/src/{config.ts → config/config.ts} +0 -0
  101. /package/node_modules/@mrclrchtr/supi-lsp/src/{defaults.json → config/defaults.json} +0 -0
  102. /package/node_modules/@mrclrchtr/supi-lsp/src/{renderer.ts → ui/renderer.ts} +0 -0
  103. /package/node_modules/@mrclrchtr/supi-tree-sitter/src/{structure.ts → tool/structure.ts} +0 -0
@@ -1,76 +1,71 @@
1
- // LSP Extension for pi — provides hover, definition, diagnostics, symbols, rename, code-actions
2
- // via a registered `lsp` tool. Keeps language servers warm, surfaces inline diagnostics,
3
- // and injects diagnostic context only when outstanding issues exist.
1
+ // LSP Extension for pi — registers an expert semantic toolset, keeps language servers warm,
2
+ // surfaces inline diagnostics, and injects diagnostic context only when outstanding issues exist.
4
3
  // biome-ignore-all lint/nursery/noExcessiveLinesPerFile: lsp.ts stays cohesive wiring; recovery and sentinel helpers live in focused modules.
5
4
 
6
5
  import * as path from "node:path";
7
- import { StringEnum } from "@earendil-works/pi-ai";
8
- import type { BeforeAgentStartEventResult, ExtensionAPI } from "@earendil-works/pi-coding-agent";
6
+ import type {
7
+ AgentEndEvent,
8
+ BeforeAgentStartEvent,
9
+ BeforeAgentStartEventResult,
10
+ ContextEvent,
11
+ ExtensionAPI,
12
+ ExtensionContext,
13
+ SessionStartEvent,
14
+ ToolResultEvent,
15
+ } from "@earendil-works/pi-coding-agent";
9
16
  import { pruneAndReorderContextMessages, restorePromptContent } from "@mrclrchtr/supi-core/api";
10
- import { Type } from "typebox";
11
- import { loadConfig, resolveLanguageAlias } from "./config.ts";
12
- import { formatDiagnosticsDisplayContent } from "./diagnostics/diagnostic-display.ts";
13
- import { assessStaleDiagnostics } from "./diagnostics/stale-diagnostics.ts";
17
+ import { loadConfig, resolveLanguageAlias } from "./config/config.ts";
18
+ import { clearTsconfigCache } from "./config/tsconfig-scope.ts";
19
+ import { FileChangeType } from "./config/types.ts";
14
20
  import {
15
- buildProjectGuidelines,
16
21
  diagnosticsContextFingerprint,
17
22
  formatDiagnosticsContext,
18
- lspPromptGuidelines,
19
- lspPromptSnippet,
20
23
  MAX_DETAILED_DIAGNOSTICS,
21
- } from "./guidance.ts";
24
+ } from "./diagnostics/diagnostic-context.ts";
25
+ import { formatDiagnosticsDisplayContent } from "./diagnostics/diagnostic-display.ts";
26
+ import { assessStaleDiagnostics } from "./diagnostics/stale-diagnostics.ts";
27
+ import {
28
+ isWorkspaceRecoveryTrigger,
29
+ scanWorkspaceSentinels,
30
+ syncWorkspaceSentinelSnapshot,
31
+ } from "./diagnostics/workspace-sentinels.ts";
32
+ import { LspManager } from "./manager/manager.ts";
33
+ import { forceResyncStaleModuleFiles } from "./manager/manager-stale-resync.ts";
22
34
  import {
23
35
  createRuntimeState,
24
36
  disableLspState,
25
- ensureLspToolActive,
37
+ ensureLspToolsActive,
26
38
  isLspAwareTool,
27
39
  type LspRuntimeState,
28
40
  refreshProjectServers,
29
- removeLspTool,
30
- } from "./lsp-state.ts";
31
- import { LspManager } from "./manager/manager.ts";
32
- import { forceResyncStaleModuleFiles } from "./manager/manager-stale-resync.ts";
33
- import { registerLspAwareToolOverrides } from "./overrides.ts";
34
- import { registerLspMessageRenderer } from "./renderer.ts";
35
- import { scanMissingServers, scanProjectCapabilities, startDetectedServers } from "./scanner.ts";
41
+ removeLspTools,
42
+ } from "./session/lsp-state.ts";
43
+ import {
44
+ scanMissingServers,
45
+ scanProjectCapabilities,
46
+ startDetectedServers,
47
+ } from "./session/scanner.ts";
36
48
  import {
37
49
  clearSessionLspService,
38
50
  SessionLspService,
39
51
  setSessionLspServiceState,
40
- } from "./service-registry.ts";
52
+ } from "./session/service-registry.ts";
41
53
  import {
42
54
  getLspDisabledMessage,
43
55
  loadLspSettings,
44
56
  registerLspSettings,
45
- } from "./settings-registration.ts";
46
- import { type LspAction, lspToolDescription, safeExecuteAction } from "./tool-actions.ts";
57
+ } from "./session/settings-registration.ts";
47
58
  import {
48
59
  persistLspActiveState,
49
60
  persistLspInactiveState,
50
61
  registerTreePersistHandlers,
51
- } from "./tree-persist.ts";
52
- import { FileChangeType } from "./types.ts";
53
- import { toggleLspStatusOverlay, updateLspUi } from "./ui.ts";
54
- import { fileToUri } from "./utils.ts";
55
- import {
56
- isWorkspaceRecoveryTrigger,
57
- scanWorkspaceSentinels,
58
- syncWorkspaceSentinelSnapshot,
59
- } from "./workspace-sentinels.ts";
60
-
61
- const LspActionEnum = StringEnum([
62
- "hover",
63
- "definition",
64
- "references",
65
- "diagnostics",
66
- "symbols",
67
- "rename",
68
- "code_actions",
69
- "workspace_symbol",
70
- "search",
71
- "symbol_hover",
72
- "recover",
73
- ] as const);
62
+ } from "./session/tree-persist.ts";
63
+ import { buildLspToolPromptSurfaces, defaultLspToolPromptSurfaces } from "./tool/guidance.ts";
64
+ import { registerLspAwareToolOverrides } from "./tool/overrides.ts";
65
+ import { registerLspTools } from "./tool/register-tools.ts";
66
+ import { registerLspMessageRenderer } from "./ui/renderer.ts";
67
+ import { toggleLspStatusOverlay, updateLspUi } from "./ui/ui.ts";
68
+ import { fileToUri, resolveSessionPath } from "./utils.ts";
74
69
 
75
70
  export default function lspExtension(pi: ExtensionAPI) {
76
71
  registerLspSettings();
@@ -79,10 +74,10 @@ export default function lspExtension(pi: ExtensionAPI) {
79
74
  registerLspAwareToolOverrides(pi, {
80
75
  getInlineSeverity: () => state.inlineSeverity,
81
76
  getManager: () => state.manager,
82
- getCwd: () => state.manager?.getCwd() ?? process.cwd(),
77
+ isActive: () => state.lspActive,
83
78
  });
84
79
 
85
- registerLspTool(pi, state, lspPromptGuidelines);
80
+ registerLspTools(pi, defaultLspToolPromptSurfaces);
86
81
  registerSessionLifecycleHandlers(pi, state);
87
82
  registerBehaviorHandlers(pi, state);
88
83
  registerTreePersistHandlers(pi, state);
@@ -92,13 +87,14 @@ export default function lspExtension(pi: ExtensionAPI) {
92
87
 
93
88
  function registerSessionLifecycleHandlers(pi: ExtensionAPI, state: LspRuntimeState): void {
94
89
  // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: session_start orchestrates setup, server detection, settings, and persistence.
95
- pi.on("session_start", async (_event, ctx) => {
90
+ pi.on("session_start", async (_event: SessionStartEvent, ctx: ExtensionContext) => {
96
91
  if (state.manager) {
97
92
  clearSessionLspService(state.manager.getCwd());
98
93
  await state.manager.shutdownAll();
99
94
  }
100
95
 
101
96
  const cwd = ctx.cwd;
97
+ clearTsconfigCache();
102
98
  const lspSettings = loadLspSettings(cwd);
103
99
 
104
100
  if (!lspSettings.enabled) {
@@ -151,13 +147,14 @@ function registerSessionLifecycleHandlers(pi: ExtensionAPI, state: LspRuntimeSta
151
147
  kind: "ready",
152
148
  service: new SessionLspService(state.manager),
153
149
  });
154
- registerLspTool(pi, state, buildProjectGuidelines(state.projectServers, cwd));
155
- ensureLspToolActive(pi);
150
+ registerLspTools(pi, buildLspToolPromptSurfaces(state.projectServers, cwd));
151
+ ensureLspToolsActive(pi);
156
152
  persistLspActiveState(pi, state);
157
153
  updateLspUi(ctx, state.manager, state.inlineSeverity, state.projectServers);
158
154
  });
159
155
 
160
156
  pi.on("session_shutdown", async () => {
157
+ clearTsconfigCache();
161
158
  if (state.manager) {
162
159
  clearSessionLspService(state.manager.getCwd());
163
160
  await state.manager.shutdownAll();
@@ -174,7 +171,7 @@ function registerSessionLifecycleHandlers(pi: ExtensionAPI, state: LspRuntimeSta
174
171
  state.sentinelSnapshot = new Map();
175
172
  });
176
173
 
177
- pi.on("agent_end", async (_event, ctx) => {
174
+ pi.on("agent_end", async (_event: AgentEndEvent, ctx: ExtensionContext) => {
178
175
  state.currentContextToken = null;
179
176
  refreshProjectServers(state);
180
177
 
@@ -193,10 +190,11 @@ function markWorkspaceChange(state: LspRuntimeState): void {
193
190
 
194
191
  function softRecoverWorkspaceChanges(
195
192
  state: LspRuntimeState,
196
- changes: import("./types.ts").FileEvent[],
193
+ changes: import("./config/types.ts").FileEvent[],
197
194
  ): boolean {
198
195
  if (!state.manager || changes.length === 0) return false;
199
196
 
197
+ clearTsconfigCache();
200
198
  state.manager.clearAllPullResultIds();
201
199
  state.manager.notifyWorkspaceFileChanges(changes);
202
200
  markWorkspaceChange(state);
@@ -209,6 +207,11 @@ function refreshWorkspaceSentinels(state: LspRuntimeState, cwd: string): boolean
209
207
  return softRecoverWorkspaceChanges(state, changes);
210
208
  }
211
209
 
210
+ function shouldInvalidateTsconfigScopeCache(filePath: string): boolean {
211
+ const ext = path.extname(filePath).toLowerCase();
212
+ return ext === ".json" || ext === ".jsonc";
213
+ }
214
+
212
215
  function recoverWorkspaceChangesFromToolResult(
213
216
  state: LspRuntimeState,
214
217
  cwd: string,
@@ -221,11 +224,14 @@ function recoverWorkspaceChangesFromToolResult(
221
224
  const pathValue = (event.input as { path?: unknown }).path;
222
225
  if (typeof pathValue !== "string") return false;
223
226
 
224
- const resolvedPath = path.resolve(cwd, pathValue);
227
+ const resolvedPath = resolveSessionPath(cwd, pathValue);
228
+ if (shouldInvalidateTsconfigScopeCache(resolvedPath)) {
229
+ clearTsconfigCache();
230
+ }
225
231
  const fileEvent = { uri: fileToUri(resolvedPath), type: FileChangeType.Changed };
226
232
 
227
233
  // Sentinel files (package.json, tsconfig.json, lockfiles, .d.ts)
228
- if (isWorkspaceRecoveryTrigger(pathValue, cwd)) {
234
+ if (isWorkspaceRecoveryTrigger(resolvedPath, cwd)) {
229
235
  if (resolvedPath.endsWith(".d.ts")) {
230
236
  return softRecoverWorkspaceChanges(state, [fileEvent]);
231
237
  }
@@ -236,7 +242,7 @@ function recoverWorkspaceChangesFromToolResult(
236
242
  }
237
243
 
238
244
  // Source files matching an active language server's file types
239
- if (state.manager.hasServerForExtension(pathValue)) {
245
+ if (state.manager.hasServerForExtension(resolvedPath)) {
240
246
  return softRecoverWorkspaceChanges(state, [fileEvent]);
241
247
  }
242
248
 
@@ -247,7 +253,7 @@ function recoverWorkspaceChangesFromToolResult(
247
253
  // biome-ignore lint/complexity/useMaxParams: wrapper groups the prompt payload fields in one place.
248
254
  function buildDiagnosticResult(
249
255
  diagnostics: import("./manager/manager-types.ts").OutstandingDiagnosticSummaryEntry[],
250
- detailed: { file: string; diagnostics: import("./types.ts").Diagnostic[] }[] | undefined,
256
+ detailed: { file: string; diagnostics: import("./config/types.ts").Diagnostic[] }[] | undefined,
251
257
  severity: number,
252
258
  token: string,
253
259
  staleWarning?: string | null,
@@ -276,16 +282,16 @@ function buildDiagnosticResult(
276
282
 
277
283
  function registerBehaviorHandlers(pi: ExtensionAPI, state: LspRuntimeState): void {
278
284
  // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: before_agent_start coordinates sentinel recovery, pruning, refresh, and diagnostic injection.
279
- pi.on("before_agent_start", async (_event, ctx) => {
285
+ pi.on("before_agent_start", async (_event: BeforeAgentStartEvent, ctx: ExtensionContext) => {
280
286
  if (!state.manager || !state.lspActive) {
281
- removeLspTool(pi);
287
+ removeLspTools(pi);
282
288
  if (!state.manager && state.lspActive) {
283
289
  persistLspInactiveState(pi, state);
284
290
  }
285
291
  return;
286
292
  }
287
293
 
288
- ensureLspToolActive(pi);
294
+ ensureLspToolsActive(pi);
289
295
 
290
296
  refreshWorkspaceSentinels(state, ctx.cwd);
291
297
 
@@ -348,18 +354,16 @@ function registerBehaviorHandlers(pi: ExtensionAPI, state: LspRuntimeState): voi
348
354
  state.lastDiagnosticsFingerprint = fingerprint;
349
355
  state.currentContextToken = `lsp-context-${++state.contextCounter}`;
350
356
 
351
- const result = buildDiagnosticResult(
357
+ return buildDiagnosticResult(
352
358
  diagnostics,
353
359
  detailed,
354
360
  state.inlineSeverity,
355
361
  state.currentContextToken,
356
362
  staleWarning,
357
363
  );
358
-
359
- return result;
360
364
  });
361
365
 
362
- pi.on("context", (event) => {
366
+ pi.on("context", (event: ContextEvent) => {
363
367
  const messages = pruneAndReorderContextMessages(
364
368
  event.messages as Array<{
365
369
  role?: string;
@@ -378,14 +382,14 @@ function registerBehaviorHandlers(pi: ExtensionAPI, state: LspRuntimeState): voi
378
382
 
379
383
  if (
380
384
  contextMessages.length === event.messages.length &&
381
- contextMessages.every((m, i) => m === event.messages[i])
385
+ contextMessages.every((message, index) => message === event.messages[index])
382
386
  ) {
383
387
  return;
384
388
  }
385
389
  return { messages: contextMessages };
386
390
  });
387
391
 
388
- pi.on("tool_result", async (event, ctx) => {
392
+ pi.on("tool_result", async (event: ToolResultEvent, ctx: ExtensionContext) => {
389
393
  if (!state.manager) return;
390
394
 
391
395
  const recoveryTriggered = recoverWorkspaceChangesFromToolResult(state, ctx.cwd, {
@@ -401,62 +405,10 @@ function registerBehaviorHandlers(pi: ExtensionAPI, state: LspRuntimeState): voi
401
405
  });
402
406
  }
403
407
 
404
- function registerLspTool(
405
- pi: ExtensionAPI,
406
- state: LspRuntimeState,
407
- promptGuidelines: string[],
408
- ): void {
409
- pi.registerTool({
410
- name: "lsp",
411
- label: "LSP",
412
- description: lspToolDescription,
413
- promptSnippet: lspPromptSnippet,
414
- promptGuidelines,
415
- parameters: Type.Object({
416
- action: LspActionEnum,
417
- file: Type.Optional(Type.String({ description: "File path (relative or absolute)" })),
418
- line: Type.Optional(Type.Number({ description: "1-based line number" })),
419
- character: Type.Optional(Type.Number({ description: "1-based column number" })),
420
- newName: Type.Optional(Type.String({ description: "New name (for rename action)" })),
421
- query: Type.Optional(
422
- Type.String({ description: "Search query (for workspace_symbol and search actions)" }),
423
- ),
424
- symbol: Type.Optional(Type.String({ description: "Symbol name (for symbol_hover action)" })),
425
- }),
426
- // biome-ignore lint/complexity/useMaxParams: pi ToolDefinition.execute signature
427
- async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
428
- if (!state.manager) {
429
- return {
430
- content: [{ type: "text", text: "LSP not initialized. Start a new session first." }],
431
- details: {},
432
- };
433
- }
434
-
435
- const text = await safeExecuteAction(
436
- state.manager,
437
- params as {
438
- action: LspAction;
439
- file?: string;
440
- line?: number;
441
- character?: number;
442
- newName?: string;
443
- query?: string;
444
- symbol?: string;
445
- },
446
- );
447
-
448
- return {
449
- content: [{ type: "text", text }],
450
- details: {},
451
- };
452
- },
453
- });
454
- }
455
-
456
408
  function registerLspStatusCommand(pi: ExtensionAPI, state: LspRuntimeState): void {
457
409
  pi.registerCommand("lsp-status", {
458
410
  description: "Show detected LSP servers, roots, open files, and diagnostics",
459
- handler: async (_args, ctx) => {
411
+ handler: async (_args: string, ctx: ExtensionContext) => {
460
412
  const lspSettings = loadLspSettings(ctx.cwd);
461
413
  if (!lspSettings.enabled) {
462
414
  ctx.ui.notify(getLspDisabledMessage(ctx.cwd), "warning");
@@ -1,8 +1,8 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  import type { LspClient } from "../client/client.ts";
3
+ import type { Diagnostic } from "../config/types.ts";
3
4
  import { relativeFilePathFromUri } from "../diagnostics/diagnostic-summary.ts";
4
5
  import { shouldIgnoreLspPath } from "../summary.ts";
5
- import type { Diagnostic } from "../types.ts";
6
6
  import { fileToUri, uriToFile } from "../utils.ts";
7
7
  import { isExcludedByPattern } from "./manager-helpers.ts";
8
8
 
@@ -1,6 +1,7 @@
1
1
  import * as path from "node:path";
2
2
  import * as projectRoots from "@mrclrchtr/supi-core/api";
3
3
  import { isGlobMatch } from "../pattern-matcher.ts";
4
+ import { resolveSessionPath } from "../utils.ts";
4
5
 
5
6
  /** Unique key for a client identified by server name and root. */
6
7
  export function clientKey(serverName: string, root: string): string {
@@ -14,10 +15,11 @@ export function resolveRootForFile(
14
15
  rootMarkers: string[],
15
16
  opts: { knownRoots: Map<string, string[]>; cwd: string },
16
17
  ): string {
18
+ const resolvedFilePath = resolveSessionPath(opts.cwd, filePath);
17
19
  const preferredRoots = opts.knownRoots.get(serverName) ?? [];
18
- const knownRoot = projectRoots.resolveKnownRoot(filePath, preferredRoots);
20
+ const knownRoot = projectRoots.resolveKnownRoot(resolvedFilePath, preferredRoots);
19
21
  if (knownRoot) return knownRoot;
20
- const fileDir = path.dirname(path.resolve(filePath));
22
+ const fileDir = path.dirname(resolvedFilePath);
21
23
  return projectRoots.findProjectRoot(fileDir, rootMarkers, opts.cwd);
22
24
  }
23
25
 
@@ -1,26 +1,27 @@
1
1
  import type { LspClient } from "../client/client.ts";
2
+ import type { ProjectServerInfo } from "../config/types.ts";
2
3
  import { displayRelativeFilePath } from "../summary.ts";
3
- import type { ProjectServerInfo } from "../types.ts";
4
4
 
5
5
  interface ProjectServerInfoInput {
6
6
  serverName: string;
7
7
  root: string;
8
8
  fileTypes: string[];
9
9
  client: LspClient | undefined;
10
- unavailable: boolean;
10
+ unavailableReason?: "missing-command" | "start-failed" | "runtime-error";
11
11
  }
12
12
 
13
13
  export function buildProjectServerInfo(
14
14
  input: ProjectServerInfoInput,
15
15
  cwd: string,
16
16
  ): ProjectServerInfo {
17
- const status = input.unavailable
18
- ? "unavailable"
19
- : input.client?.status === "running"
17
+ const status =
18
+ input.client?.status === "running"
20
19
  ? "running"
21
- : input.client?.status === "error"
20
+ : input.client?.status === "error" || input.unavailableReason === "start-failed"
22
21
  ? "error"
23
- : "unavailable";
22
+ : input.unavailableReason === "runtime-error"
23
+ ? "error"
24
+ : "unavailable";
24
25
 
25
26
  return {
26
27
  name: input.serverName,
@@ -39,7 +40,9 @@ function getSupportedActions(capabilities: LspClient["serverCapabilities"] | und
39
40
  if (capabilities.hoverProvider) actions.push("hover(file,line,char)");
40
41
  if (capabilities.definitionProvider) actions.push("definition(file,line,char)");
41
42
  if (capabilities.referencesProvider) actions.push("references(file,line,char)");
43
+ if (capabilities.implementationProvider) actions.push("implementation(file,line,char)");
42
44
  if (capabilities.documentSymbolProvider) actions.push("symbols(file)");
45
+ if (capabilities.workspaceSymbolProvider) actions.push("workspace_symbols(query)");
43
46
  if (capabilities.renameProvider) actions.push("rename(file,line,char,newName)");
44
47
  if (capabilities.codeActionProvider) actions.push("code_actions(file,line,char)");
45
48
  return actions;
@@ -1,8 +1,8 @@
1
+ import type { Diagnostic, FileEvent } from "../config/types.ts";
1
2
  import {
2
3
  assessStaleDiagnostics,
3
4
  type StaleDiagnosticAssessment,
4
5
  } from "../diagnostics/stale-diagnostics.ts";
5
- import type { Diagnostic, FileEvent } from "../types.ts";
6
6
 
7
7
  export interface WorkspaceRecoveryResult {
8
8
  refreshedClients: number;
@@ -1,18 +1,170 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { walkProject } from "@mrclrchtr/supi-core/api";
1
4
  import type { LspClient } from "../client/client.ts";
2
- import type { SymbolInformation, WorkspaceSymbol } from "../types.ts";
5
+ import type {
6
+ DocumentSymbol,
7
+ Position,
8
+ SymbolInformation,
9
+ WorkspaceSymbol,
10
+ } from "../config/types.ts";
3
11
 
4
- export async function managerWorkspaceSymbol(
12
+ type WorkspaceSymbolLike = SymbolInformation | WorkspaceSymbol;
13
+
14
+ const SKIP_DIRS = new Set(["node_modules", ".git", ".pnpm", "dist", "build", "coverage"]);
15
+ const DEFAULT_WARM_FILE_DEPTH = 4;
16
+ const DEFAULT_MARKER_SCAN_DEPTH = 6;
17
+
18
+ export interface WorkspaceSymbolWarmTarget {
19
+ projectRoot: string;
20
+ file: string;
21
+ }
22
+
23
+ export interface WorkspaceSymbolWarmOptions {
24
+ maxFileDepth?: number;
25
+ maxMarkerDepth?: number;
26
+ }
27
+
28
+ export function getWorkspaceSymbolWarmPosition(
29
+ symbols: DocumentSymbol[] | SymbolInformation[] | null,
30
+ ): Position | null {
31
+ if (!symbols || symbols.length === 0) return null;
32
+ const first = symbols[0];
33
+ if ("selectionRange" in first) {
34
+ return first.selectionRange.start;
35
+ }
36
+ if ("location" in first) {
37
+ return first.location.range.start;
38
+ }
39
+ return null;
40
+ }
41
+
42
+ export async function collectWorkspaceSymbols(
5
43
  clients: Iterable<LspClient>,
6
44
  query: string,
7
- ): Promise<(SymbolInformation | WorkspaceSymbol)[] | null> {
8
- const all: (SymbolInformation | WorkspaceSymbol)[] = [];
45
+ ): Promise<{ results: WorkspaceSymbolLike[]; hasSupport: boolean }> {
46
+ const results: WorkspaceSymbolLike[] = [];
9
47
  let hasSupport = false;
48
+
10
49
  for (const client of clients) {
11
50
  if (client.status !== "running") continue;
12
51
  if (!client.serverCapabilities?.workspaceSymbolProvider) continue;
13
52
  hasSupport = true;
14
53
  const result = await client.workspaceSymbol(query);
15
- if (result) all.push(...result);
54
+ if (result) results.push(...result);
16
55
  }
17
- return hasSupport ? all : null;
56
+
57
+ return { results, hasSupport };
58
+ }
59
+
60
+ export async function managerWorkspaceSymbol(
61
+ clients: Iterable<LspClient>,
62
+ query: string,
63
+ ): Promise<WorkspaceSymbolLike[] | null> {
64
+ const { results, hasSupport } = await collectWorkspaceSymbols(clients, query);
65
+ return hasSupport ? results : null;
66
+ }
67
+
68
+ export function findWorkspaceSymbolWarmTargets(
69
+ root: string,
70
+ rootMarkers: string[],
71
+ fileTypes: string[],
72
+ options: WorkspaceSymbolWarmOptions = {},
73
+ ): WorkspaceSymbolWarmTarget[] {
74
+ const resolvedRoot = path.resolve(root);
75
+ const allowed = new Set(fileTypes.map((fileType) => fileType.toLowerCase()));
76
+ const maxFileDepth = options.maxFileDepth ?? DEFAULT_WARM_FILE_DEPTH;
77
+ const maxMarkerDepth = options.maxMarkerDepth ?? DEFAULT_MARKER_SCAN_DEPTH;
78
+ if (allowed.size === 0) return [];
79
+
80
+ const markerRoots = collectMarkerRoots(resolvedRoot, rootMarkers, maxMarkerDepth);
81
+ const targets = markerRoots
82
+ .map((entry) => {
83
+ const file = findWarmFileRecursive(entry.root, allowed, maxFileDepth);
84
+ return file ? { projectRoot: entry.root, file, priority: entry.priority } : null;
85
+ })
86
+ .filter((entry): entry is { projectRoot: string; file: string; priority: number } =>
87
+ Boolean(entry),
88
+ )
89
+ .sort(
90
+ (a, b) =>
91
+ a.priority - b.priority ||
92
+ a.projectRoot.length - b.projectRoot.length ||
93
+ a.projectRoot.localeCompare(b.projectRoot),
94
+ )
95
+ .map(({ projectRoot, file }) => ({ projectRoot, file }));
96
+
97
+ if (targets.length > 0) return targets;
98
+
99
+ const fallback = findWarmFileRecursive(resolvedRoot, allowed, maxFileDepth);
100
+ return fallback ? [{ projectRoot: resolvedRoot, file: fallback }] : [];
101
+ }
102
+
103
+ interface MarkerRootEntry {
104
+ root: string;
105
+ priority: number;
106
+ }
107
+
108
+ function collectMarkerRoots(
109
+ root: string,
110
+ rootMarkers: string[],
111
+ maxDepth: number,
112
+ ): MarkerRootEntry[] {
113
+ if (rootMarkers.length === 0) return [];
114
+
115
+ const markerIndex = new Map(rootMarkers.map((marker, index) => [marker, index]));
116
+ const matches = new Map<string, number>();
117
+
118
+ walkProject(root, maxDepth, (directory, entryNames) => {
119
+ const matchedPriority = rootMarkers.reduce<number | null>((best, marker) => {
120
+ if (!entryNames.has(marker)) return best;
121
+ const next = markerIndex.get(marker) ?? Number.MAX_SAFE_INTEGER;
122
+ return best === null ? next : Math.min(best, next);
123
+ }, null);
124
+
125
+ if (matchedPriority === null) return;
126
+ const resolvedDirectory = path.resolve(directory);
127
+ const existing = matches.get(resolvedDirectory);
128
+ if (existing === undefined || matchedPriority < existing) {
129
+ matches.set(resolvedDirectory, matchedPriority);
130
+ }
131
+ });
132
+
133
+ return Array.from(matches.entries()).map(([matchedRoot, priority]) => ({
134
+ root: matchedRoot,
135
+ priority,
136
+ }));
137
+ }
138
+
139
+ function findWarmFileRecursive(
140
+ directory: string,
141
+ allowed: Set<string>,
142
+ depth: number,
143
+ ): string | null {
144
+ let entries: fs.Dirent[];
145
+ try {
146
+ entries = fs.readdirSync(directory, { withFileTypes: true });
147
+ } catch {
148
+ return null;
149
+ }
150
+
151
+ const sortedEntries = [...entries].sort((a, b) => a.name.localeCompare(b.name));
152
+
153
+ for (const entry of sortedEntries) {
154
+ if (!entry.isFile()) continue;
155
+ const ext = path.extname(entry.name).slice(1).toLowerCase();
156
+ if (!allowed.has(ext)) continue;
157
+ return path.join(directory, entry.name);
158
+ }
159
+
160
+ if (depth <= 0) return null;
161
+
162
+ for (const entry of sortedEntries) {
163
+ if (!entry.isDirectory()) continue;
164
+ if (SKIP_DIRS.has(entry.name)) continue;
165
+ const nested = findWarmFileRecursive(path.join(directory, entry.name), allowed, depth - 1);
166
+ if (nested) return nested;
167
+ }
168
+
169
+ return null;
18
170
  }