@mrclrchtr/supi-code-intelligence 1.3.1 → 1.5.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 (133) 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 +15 -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 +15 -13
  9. package/node_modules/@mrclrchtr/supi-core/src/path-utils.ts +40 -0
  10. package/node_modules/@mrclrchtr/supi-core/src/registry-utils.ts +42 -10
  11. package/node_modules/@mrclrchtr/{supi-lsp/node_modules/@mrclrchtr/supi-core/src → supi-core/src/settings}/settings-registry.ts +1 -1
  12. package/node_modules/@mrclrchtr/supi-lsp/README.md +58 -39
  13. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/README.md +52 -41
  14. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/package.json +1 -1
  15. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/api.ts +15 -13
  16. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/{config-settings.ts → config/config-settings.ts} +2 -2
  17. package/node_modules/@mrclrchtr/{supi-core/src → supi-lsp/node_modules/@mrclrchtr/supi-core/src/context}/context-provider-registry.ts +1 -1
  18. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/extension.ts +1 -1
  19. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/index.ts +15 -13
  20. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/path-utils.ts +40 -0
  21. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/registry-utils.ts +42 -10
  22. package/node_modules/@mrclrchtr/{supi-core/src → supi-lsp/node_modules/@mrclrchtr/supi-core/src/settings}/settings-registry.ts +1 -1
  23. package/node_modules/@mrclrchtr/supi-lsp/package.json +3 -2
  24. package/node_modules/@mrclrchtr/supi-lsp/src/api.ts +16 -3
  25. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-refresh.ts +1 -1
  26. package/node_modules/@mrclrchtr/supi-lsp/src/client/client.ts +27 -3
  27. package/node_modules/@mrclrchtr/supi-lsp/src/client/transport.ts +61 -5
  28. package/node_modules/@mrclrchtr/supi-lsp/src/config/tsconfig-scope.ts +244 -0
  29. package/node_modules/@mrclrchtr/supi-lsp/src/{types.ts → config/types.ts} +4 -2
  30. package/node_modules/@mrclrchtr/supi-lsp/src/coordinates.ts +11 -0
  31. package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/diagnostic-augmentation.ts +5 -5
  32. package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/diagnostic-context.ts +115 -0
  33. package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/diagnostic-display.ts +1 -1
  34. package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/diagnostic-summary.ts +3 -2
  35. package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/diagnostics.ts +1 -1
  36. package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/stale-diagnostics.ts +1 -1
  37. package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/suppression-diagnostics.ts +1 -1
  38. package/node_modules/@mrclrchtr/supi-lsp/src/{workspace-sentinels.ts → diagnostics/workspace-sentinels.ts} +2 -2
  39. package/node_modules/@mrclrchtr/supi-lsp/src/format.ts +2 -23
  40. package/node_modules/@mrclrchtr/supi-lsp/src/index.ts +18 -5
  41. package/node_modules/@mrclrchtr/supi-lsp/src/lsp.ts +72 -120
  42. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-diagnostics.ts +1 -1
  43. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-helpers.ts +4 -2
  44. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-project-info.ts +10 -21
  45. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-workspace-recovery.ts +1 -1
  46. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-workspace-symbol.ts +158 -6
  47. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager.ts +202 -43
  48. package/node_modules/@mrclrchtr/supi-lsp/src/{lsp-state.ts → session/lsp-state.ts} +22 -11
  49. package/node_modules/@mrclrchtr/supi-lsp/src/{scanner.ts → session/scanner.ts} +3 -3
  50. package/node_modules/@mrclrchtr/supi-lsp/src/{service-registry.ts → session/service-registry.ts} +109 -33
  51. package/node_modules/@mrclrchtr/supi-lsp/src/{settings-registration.ts → session/settings-registration.ts} +1 -1
  52. package/node_modules/@mrclrchtr/supi-lsp/src/session/tree-persist.ts +75 -0
  53. package/node_modules/@mrclrchtr/supi-lsp/src/summary.ts +1 -1
  54. package/node_modules/@mrclrchtr/supi-lsp/src/tool/guidance.ts +78 -0
  55. package/node_modules/@mrclrchtr/supi-lsp/src/tool/names.ts +19 -0
  56. package/node_modules/@mrclrchtr/supi-lsp/src/{overrides.ts → tool/overrides.ts} +55 -24
  57. package/node_modules/@mrclrchtr/supi-lsp/src/tool/register-tools.ts +71 -0
  58. package/node_modules/@mrclrchtr/supi-lsp/src/tool/service-actions.ts +258 -0
  59. package/node_modules/@mrclrchtr/supi-lsp/src/tool/tool-specs.ts +248 -0
  60. package/node_modules/@mrclrchtr/supi-lsp/src/{ui.ts → ui/ui.ts} +4 -4
  61. package/node_modules/@mrclrchtr/supi-lsp/src/utils.ts +5 -23
  62. package/node_modules/@mrclrchtr/supi-tree-sitter/README.md +58 -39
  63. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/README.md +107 -0
  64. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/package.json +44 -0
  65. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/api.ts +85 -0
  66. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/config/config-settings.ts +76 -0
  67. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/config/config.ts +186 -0
  68. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/context/context-messages.ts +119 -0
  69. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/context/context-provider-registry.ts +36 -0
  70. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/context/context-tag.ts +31 -0
  71. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/debug-registry.ts +255 -0
  72. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/extension.ts +1 -0
  73. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/index.ts +85 -0
  74. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/path-utils.ts +40 -0
  75. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/project-roots.ts +170 -0
  76. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/registry-utils.ts +86 -0
  77. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/session-utils.ts +29 -0
  78. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/settings/settings-command.ts +15 -0
  79. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/settings/settings-registry.ts +41 -0
  80. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/settings/settings-ui.ts +226 -0
  81. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/terminal.ts +60 -0
  82. package/node_modules/@mrclrchtr/supi-tree-sitter/package.json +8 -3
  83. package/node_modules/@mrclrchtr/supi-tree-sitter/src/api.ts +6 -2
  84. package/node_modules/@mrclrchtr/supi-tree-sitter/src/index.ts +6 -2
  85. package/node_modules/@mrclrchtr/supi-tree-sitter/src/{runtime.ts → session/runtime.ts} +6 -5
  86. package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/service-registry.ts +30 -0
  87. package/node_modules/@mrclrchtr/supi-tree-sitter/src/{session.ts → session/session.ts} +20 -12
  88. package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/action-specs.ts +92 -0
  89. package/node_modules/@mrclrchtr/supi-tree-sitter/src/{callees.ts → tool/callees.ts} +3 -3
  90. package/node_modules/@mrclrchtr/supi-tree-sitter/src/{exports.ts → tool/exports.ts} +4 -4
  91. package/node_modules/@mrclrchtr/supi-tree-sitter/src/{formatting.ts → tool/formatting.ts} +1 -1
  92. package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/guidance.ts +31 -0
  93. package/node_modules/@mrclrchtr/supi-tree-sitter/src/{imports.ts → tool/imports.ts} +4 -4
  94. package/node_modules/@mrclrchtr/supi-tree-sitter/src/{node-at.ts → tool/node-at.ts} +3 -3
  95. package/node_modules/@mrclrchtr/supi-tree-sitter/src/{outline.ts → tool/outline.ts} +3 -3
  96. package/node_modules/@mrclrchtr/supi-tree-sitter/src/tree-sitter.ts +118 -91
  97. package/node_modules/@mrclrchtr/supi-tree-sitter/src/types.ts +13 -2
  98. package/package.json +4 -4
  99. package/src/actions/affected-action.ts +4 -4
  100. package/src/actions/brief-action.ts +12 -13
  101. package/src/actions/callees-action.ts +14 -10
  102. package/src/actions/callers-action.ts +4 -4
  103. package/src/actions/implementations-action.ts +4 -4
  104. package/src/code-intelligence.ts +4 -11
  105. package/src/pattern-structured.ts +20 -22
  106. package/src/providers/semantic-provider.ts +34 -0
  107. package/src/providers/structural-provider.ts +26 -0
  108. package/src/search-helpers.ts +4 -15
  109. package/src/target-resolution.ts +26 -35
  110. package/src/tool/action-specs.ts +66 -0
  111. package/src/tool/guidance.ts +18 -0
  112. package/src/tool-actions.ts +23 -40
  113. package/node_modules/@mrclrchtr/supi-lsp/src/guidance.ts +0 -163
  114. package/node_modules/@mrclrchtr/supi-lsp/src/search-fallback.ts +0 -98
  115. package/node_modules/@mrclrchtr/supi-lsp/src/tool-actions.ts +0 -430
  116. package/node_modules/@mrclrchtr/supi-lsp/src/tree-persist.ts +0 -48
  117. package/node_modules/@mrclrchtr/supi-lsp/src/tsconfig-scope.ts +0 -156
  118. package/src/guidance.ts +0 -42
  119. /package/node_modules/@mrclrchtr/supi-core/src/{config.ts → config/config.ts} +0 -0
  120. /package/node_modules/@mrclrchtr/supi-core/src/{context-messages.ts → context/context-messages.ts} +0 -0
  121. /package/node_modules/@mrclrchtr/supi-core/src/{context-tag.ts → context/context-tag.ts} +0 -0
  122. /package/node_modules/@mrclrchtr/supi-core/src/{settings-command.ts → settings/settings-command.ts} +0 -0
  123. /package/node_modules/@mrclrchtr/supi-core/src/{settings-ui.ts → settings/settings-ui.ts} +0 -0
  124. /package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/{config.ts → config/config.ts} +0 -0
  125. /package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/{context-messages.ts → context/context-messages.ts} +0 -0
  126. /package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/{context-tag.ts → context/context-tag.ts} +0 -0
  127. /package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/{settings-command.ts → settings/settings-command.ts} +0 -0
  128. /package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/{settings-ui.ts → settings/settings-ui.ts} +0 -0
  129. /package/node_modules/@mrclrchtr/supi-lsp/src/{capabilities.ts → config/capabilities.ts} +0 -0
  130. /package/node_modules/@mrclrchtr/supi-lsp/src/{config.ts → config/config.ts} +0 -0
  131. /package/node_modules/@mrclrchtr/supi-lsp/src/{defaults.json → config/defaults.json} +0 -0
  132. /package/node_modules/@mrclrchtr/supi-lsp/src/{renderer.ts → ui/renderer.ts} +0 -0
  133. /package/node_modules/@mrclrchtr/supi-tree-sitter/src/{structure.ts → tool/structure.ts} +0 -0
@@ -2,9 +2,9 @@
2
2
  // Peer extensions can import `getSessionLspService` from the package root
3
3
  // to reuse the active LSP runtime without starting duplicate servers.
4
4
 
5
- import * as path from "node:path";
6
- import type { LspManager } from "./manager/manager.ts";
5
+ import { createSessionStateRegistry } from "@mrclrchtr/supi-core/api";
7
6
  import type {
7
+ CodeAction,
8
8
  Diagnostic,
9
9
  DocumentSymbol,
10
10
  Hover,
@@ -13,11 +13,43 @@ import type {
13
13
  Position,
14
14
  ProjectServerInfo,
15
15
  SymbolInformation,
16
+ WorkspaceEdit,
16
17
  WorkspaceSymbol,
17
- } from "./types.ts";
18
+ } from "../config/types.ts";
19
+ import type { LspManager } from "../manager/manager.ts";
20
+ import { resolveSessionPath } from "../utils.ts";
21
+
22
+ /** Workspace diagnostic summary grouped by file. */
23
+ export interface WorkspaceDiagnosticSummaryEntry {
24
+ file: string;
25
+ errors: number;
26
+ warnings: number;
27
+ }
28
+
29
+ /** Outstanding diagnostics grouped by file, including info and hint counts. */
30
+ export interface OutstandingDiagnosticSummaryEntry {
31
+ file: string;
32
+ total: number;
33
+ errors: number;
34
+ warnings: number;
35
+ information: number;
36
+ hints: number;
37
+ }
38
+
39
+ /** Result from a workspace diagnostic recovery pass. */
40
+ export interface RecoverDiagnosticsResult {
41
+ refreshedClients: number;
42
+ restartedClients: number;
43
+ staleAssessment: {
44
+ suspected: boolean;
45
+ matchedFiles: Array<{ file: string; diagnostics: Diagnostic[] }>;
46
+ warning: string | null;
47
+ };
48
+ }
18
49
 
19
50
  export type SessionLspServiceState =
20
51
  | { kind: "ready"; service: SessionLspService }
52
+ | { kind: "inactive"; service: SessionLspService }
21
53
  | { kind: "pending" }
22
54
  | { kind: "disabled" }
23
55
  | { kind: "unavailable"; reason: string };
@@ -25,7 +57,9 @@ export type SessionLspServiceState =
25
57
  /**
26
58
  * Public wrapper around {@link LspManager} that exposes stable semantic operations.
27
59
  * File path inputs may be absolute or session-cwd-relative; a leading `@` is stripped
28
- * to match pi's built-in path-tool convention.
60
+ * to match pi's built-in path-tool convention. Position arguments use raw 0-based LSP
61
+ * coordinates; use `toLspPosition()` from `@mrclrchtr/supi-lsp/api` when starting from
62
+ * user-facing 1-based line and character values.
29
63
  */
30
64
  export class SessionLspService {
31
65
  constructor(private readonly manager: LspManager) {}
@@ -77,77 +111,119 @@ export class SessionLspService {
77
111
  return this.manager.workspaceSymbol(query);
78
112
  }
79
113
 
114
+ async rename(
115
+ filePath: string,
116
+ position: Position,
117
+ newName: string,
118
+ ): Promise<WorkspaceEdit | null> {
119
+ const resolvedPath = this.resolveFilePath(filePath);
120
+ const client = await this.manager.ensureFileOpen(resolvedPath);
121
+ if (!client) return null;
122
+ return client.rename(resolvedPath, position, newName);
123
+ }
124
+
125
+ async codeActions(filePath: string, position: Position): Promise<CodeAction[] | null> {
126
+ const resolvedPath = this.resolveFilePath(filePath);
127
+ const client = await this.manager.ensureFileOpen(resolvedPath);
128
+ if (!client) return null;
129
+
130
+ const range = { start: position, end: position };
131
+ const diagnostics = client
132
+ .getDiagnostics(resolvedPath)
133
+ .filter((diagnostic) => diagnostic.range.start.line <= position.line)
134
+ .filter((diagnostic) => diagnostic.range.end.line >= position.line);
135
+
136
+ return client.codeActions(resolvedPath, range, { diagnostics });
137
+ }
138
+
80
139
  // ── Project / runtime awareness ─────────────────────────────────────
81
140
 
82
141
  getProjectServers(): ProjectServerInfo[] {
83
142
  return this.manager.getKnownProjectServers([]);
84
143
  }
85
144
 
145
+ /** Check whether the file can be served semantically for explicit LSP operations. */
86
146
  isSupportedSourceFile(filePath: string): boolean {
87
- return this.manager.isSupportedSourceFile(this.resolveFilePath(filePath));
147
+ return this.manager.canServeFile(this.resolveFilePath(filePath));
88
148
  }
89
149
 
90
150
  // ── Diagnostics ─────────────────────────────────────────────────────
91
151
 
152
+ /** Sync a file through LSP and return diagnostics up to the supplied severity threshold. */
153
+ async fileDiagnostics(filePath: string, maxSeverity: number = 4): Promise<Diagnostic[] | null> {
154
+ const resolvedPath = this.resolveFilePath(filePath);
155
+ if (!this.manager.canServeFile(resolvedPath)) return null;
156
+ return this.manager.syncFileAndGetDiagnostics(resolvedPath, maxSeverity);
157
+ }
158
+
159
+ /** Get a lightweight workspace diagnostic summary for all tracked files. */
160
+ getWorkspaceDiagnosticSummary(): WorkspaceDiagnosticSummaryEntry[] {
161
+ return this.manager.getDiagnosticSummary();
162
+ }
163
+
164
+ /** Get outstanding diagnostics grouped by file at or above the supplied severity threshold. */
92
165
  getOutstandingDiagnostics(
93
166
  maxSeverity: number = 1,
94
167
  ): Array<{ file: string; diagnostics: Diagnostic[] }> {
95
168
  return this.manager.getOutstandingDiagnostics(maxSeverity);
96
169
  }
97
170
 
98
- getOutstandingDiagnosticSummary(
99
- maxSeverity: number = 1,
100
- ): import("./manager/manager-types.ts").OutstandingDiagnosticSummaryEntry[] {
171
+ /** Get outstanding diagnostic counts grouped by file. */
172
+ getOutstandingDiagnosticSummary(maxSeverity: number = 1): OutstandingDiagnosticSummaryEntry[] {
101
173
  return this.manager.getOutstandingDiagnosticSummary(maxSeverity);
102
174
  }
103
175
 
104
- /** Access the underlying manager for advanced use cases (discouraged). */
105
- getManager(): LspManager {
106
- return this.manager;
176
+ /** Trigger a workspace-wide diagnostics refresh and stale-state recovery pass. */
177
+ async recoverDiagnostics(options?: {
178
+ restartIfStillStale?: boolean;
179
+ maxWaitMs?: number;
180
+ quietMs?: number;
181
+ }): Promise<RecoverDiagnosticsResult> {
182
+ return this.manager.recoverWorkspaceDiagnostics(options);
107
183
  }
108
184
 
109
185
  private resolveFilePath(filePath: string): string {
110
- const normalizedPath = filePath.startsWith("@") ? filePath.slice(1) : filePath;
111
- return path.resolve(this.manager.getCwd(), normalizedPath);
186
+ return resolveSessionPath(this.manager.getCwd(), filePath);
112
187
  }
113
188
  }
114
189
 
115
190
  // ── Registry ──────────────────────────────────────────────────────────
116
191
 
117
- const REGISTRY_KEY = Symbol.for("@mrclrchtr/supi-lsp/session-registry");
118
-
119
- function getRegistry(): Map<string, SessionLspServiceState> {
120
- const globalScope = globalThis as typeof globalThis & Record<symbol, unknown>;
121
- const existing = globalScope[REGISTRY_KEY];
122
- if (existing instanceof Map) return existing as Map<string, SessionLspServiceState>;
123
-
124
- const registry = new Map<string, SessionLspServiceState>();
125
- globalScope[REGISTRY_KEY] = registry;
126
- return registry;
127
- }
128
-
129
- function normalizeCwd(cwd: string): string {
130
- return path.resolve(cwd);
131
- }
132
-
133
- const registry = getRegistry();
192
+ const WAIT_INTERVAL_MS = 25;
193
+ const registry = createSessionStateRegistry<SessionLspServiceState>("supi-lsp/session-registry");
134
194
 
135
195
  /** Publish the LSP service state for a session cwd. */
136
196
  export function setSessionLspServiceState(cwd: string, state: SessionLspServiceState): void {
137
- registry.set(normalizeCwd(cwd), state);
197
+ registry.set(cwd, state);
138
198
  }
139
199
 
140
200
  /** Acquire the LSP service state for a session cwd. */
141
201
  export function getSessionLspService(cwd: string): SessionLspServiceState {
142
202
  return (
143
- registry.get(normalizeCwd(cwd)) ?? {
203
+ registry.get(cwd) ?? {
144
204
  kind: "unavailable",
145
205
  reason: "No LSP session initialized for this workspace",
146
206
  }
147
207
  );
148
208
  }
149
209
 
210
+ /** Wait briefly for a pending session-scoped LSP service to become ready. */
211
+ export async function waitForSessionLspService(
212
+ cwd: string,
213
+ timeoutMs: number = 250,
214
+ ): Promise<SessionLspServiceState> {
215
+ const deadline = Date.now() + Math.max(0, timeoutMs);
216
+ let state = getSessionLspService(cwd);
217
+
218
+ while (state.kind === "pending" && Date.now() < deadline) {
219
+ await new Promise((resolve) => setTimeout(resolve, WAIT_INTERVAL_MS));
220
+ state = getSessionLspService(cwd);
221
+ }
222
+
223
+ return state;
224
+ }
225
+
150
226
  /** Remove the LSP service state for a session cwd. */
151
227
  export function clearSessionLspService(cwd: string): void {
152
- registry.delete(normalizeCwd(cwd));
228
+ registry.clear(cwd);
153
229
  }
@@ -8,7 +8,7 @@ import {
8
8
  loadSupiConfigForScope,
9
9
  registerConfigSettings,
10
10
  } from "@mrclrchtr/supi-core/api";
11
- import { loadConfig } from "./config.ts";
11
+ import { loadConfig } from "../config/config.ts";
12
12
 
13
13
  // ── Types ────────────────────────────────────────────────────
14
14
 
@@ -0,0 +1,75 @@
1
+ // LSP tree navigation persistence — restores tool activation state across /tree navigation.
2
+
3
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
4
+ import type { LspManager } from "../manager/manager.ts";
5
+ import { LSP_TOOL_NAMES } from "../tool/names.ts";
6
+ import { SessionLspService, setSessionLspServiceState } from "./service-registry.ts";
7
+
8
+ /** Shape of the entry persisted via `pi.appendEntry()`. */
9
+ export interface LspStateEntry {
10
+ active: boolean;
11
+ }
12
+
13
+ /** Restore LSP activation state from the current branch after /tree navigation. */
14
+ export function registerTreePersistHandlers(
15
+ pi: ExtensionAPI,
16
+ state: { lspActive: boolean; manager?: LspManager | null },
17
+ ): void {
18
+ pi.on("session_tree", async (_event, ctx) => {
19
+ const branch = ctx.sessionManager.getBranch();
20
+ const isActive = findLastLspState(branch)?.active === true;
21
+
22
+ syncBranchToolActivation(pi, isActive);
23
+ syncBranchServiceState(state.manager, isActive);
24
+ state.lspActive = isActive;
25
+ });
26
+ }
27
+
28
+ function findLastLspState(branch: Array<{ type: string; customType?: string; data?: unknown }>) {
29
+ let lastEntry: LspStateEntry | undefined;
30
+ for (const entry of branch) {
31
+ if (entry.type === "custom" && entry.customType === "lsp-active") {
32
+ lastEntry = entry.data as LspStateEntry | undefined;
33
+ }
34
+ }
35
+ return lastEntry;
36
+ }
37
+
38
+ function syncBranchToolActivation(pi: ExtensionAPI, isActive: boolean): void {
39
+ const activeTools = pi.getActiveTools();
40
+ if (isActive) {
41
+ const missing = LSP_TOOL_NAMES.filter((toolName) => !activeTools.includes(toolName));
42
+ if (missing.length > 0) {
43
+ pi.setActiveTools([...activeTools, ...missing]);
44
+ }
45
+ return;
46
+ }
47
+
48
+ const nextTools = activeTools.filter(
49
+ (toolName) => !LSP_TOOL_NAMES.includes(toolName as (typeof LSP_TOOL_NAMES)[number]),
50
+ );
51
+ if (nextTools.length !== activeTools.length) {
52
+ pi.setActiveTools(nextTools);
53
+ }
54
+ }
55
+
56
+ function syncBranchServiceState(manager: LspManager | null | undefined, isActive: boolean): void {
57
+ if (!manager) return;
58
+
59
+ setSessionLspServiceState(manager.getCwd(), {
60
+ kind: isActive ? "ready" : "inactive",
61
+ service: new SessionLspService(manager),
62
+ });
63
+ }
64
+
65
+ /** Persist that LSP is active in the session tree. */
66
+ export function persistLspActiveState(pi: ExtensionAPI, state: { lspActive: boolean }): void {
67
+ state.lspActive = true;
68
+ pi.appendEntry<LspStateEntry>("lsp-active", { active: true });
69
+ }
70
+
71
+ /** Persist that LSP is inactive in the session tree. */
72
+ export function persistLspInactiveState(pi: ExtensionAPI, state: { lspActive: boolean }): void {
73
+ state.lspActive = false;
74
+ pi.appendEntry<LspStateEntry>("lsp-active", { active: false });
75
+ }
@@ -84,7 +84,7 @@ export function isPathRelevant(filePath: string, relevantPaths: string[], cwd: s
84
84
  });
85
85
  }
86
86
 
87
- import { isFileExcludedByTsconfig } from "./tsconfig-scope.ts";
87
+ import { isFileExcludedByTsconfig } from "./config/tsconfig-scope.ts";
88
88
 
89
89
  /** Check whether a file path is inside the project tree (within cwd, not node_modules/.pnpm/out-of-tree).
90
90
  * Does NOT check tsconfig exclusion — use `shouldIgnoreLspPath` for diagnostics/guidance filtering. */
@@ -0,0 +1,78 @@
1
+ // Prompt guidance and tool descriptions for the expert LSP toolset.
2
+
3
+ import * as path from "node:path";
4
+ import type { ProjectServerInfo } from "../config/types.ts";
5
+ import { LSP_LOOKUP_TOOL, type LspToolName } from "./names.ts";
6
+ import { LSP_TOOL_DEFINITION_SPECS } from "./tool-specs.ts";
7
+
8
+ export interface LspToolPromptSurface {
9
+ description: string;
10
+ promptSnippet: string;
11
+ promptGuidelines: string[];
12
+ }
13
+
14
+ export type LspToolPromptSurfaceMap = Record<LspToolName, LspToolPromptSurface>;
15
+
16
+ export const defaultLspToolPromptSurfaces = buildLspToolPromptSurfaces([], ".");
17
+
18
+ export function buildLspToolPromptSurfaces(
19
+ servers: ProjectServerInfo[],
20
+ cwd: string,
21
+ ): LspToolPromptSurfaceMap {
22
+ const coverageGuidelines = buildCoverageGuidelines(servers, cwd);
23
+
24
+ return Object.fromEntries(
25
+ LSP_TOOL_DEFINITION_SPECS.map((spec) => [
26
+ spec.name,
27
+ {
28
+ description: spec.description,
29
+ promptSnippet: spec.promptSnippet,
30
+ promptGuidelines:
31
+ "includeCoverageGuidelines" in spec && spec.includeCoverageGuidelines
32
+ ? [...spec.basePromptGuidelines, ...coverageGuidelines]
33
+ : [...spec.basePromptGuidelines],
34
+ } satisfies LspToolPromptSurface,
35
+ ]),
36
+ ) as LspToolPromptSurfaceMap;
37
+ }
38
+
39
+ function buildCoverageGuidelines(servers: ProjectServerInfo[], cwd: string): string[] {
40
+ const active = servers
41
+ .filter((server) => server.status === "running")
42
+ .map((server) => {
43
+ const root = displayRoot(server.root, cwd);
44
+ const fileTypes = server.fileTypes.map((entry) => `.${entry}`).join(",");
45
+ const actions = server.supportedActions.join(",");
46
+ const actionText = actions.length > 0 ? ` | actions: ${actions}` : "";
47
+ return `lsp server coverage: ${server.name} | root: ${root} | files: ${fileTypes}${actionText}`;
48
+ });
49
+
50
+ const unavailable = servers
51
+ .filter((server) => server.status !== "running")
52
+ .map((server) => server.name);
53
+
54
+ const dynamic = [...active];
55
+ if (unavailable.length > 0) {
56
+ dynamic.push(
57
+ `lsp server unavailable: ${unavailable.join(",")} — install or enable to extend semantic coverage`,
58
+ );
59
+ }
60
+
61
+ return dynamic;
62
+ }
63
+
64
+ function displayRoot(root: string, cwd: string): string {
65
+ const relative = path.relative(cwd, root);
66
+ if (relative === "") return ".";
67
+ if (relative.startsWith(`..${path.sep}`) || relative === "..") return root;
68
+ return relative.replaceAll(path.sep, "/");
69
+ }
70
+
71
+ // Compatibility exports for older internal tests and helper imports.
72
+ export const toolDescription = defaultLspToolPromptSurfaces[LSP_LOOKUP_TOOL].description;
73
+ export const promptSnippet = defaultLspToolPromptSurfaces[LSP_LOOKUP_TOOL].promptSnippet;
74
+ export const promptGuidelines = defaultLspToolPromptSurfaces[LSP_LOOKUP_TOOL].promptGuidelines;
75
+
76
+ export function buildProjectGuidelines(servers: ProjectServerInfo[], cwd: string): string[] {
77
+ return buildLspToolPromptSurfaces(servers, cwd)[LSP_LOOKUP_TOOL].promptGuidelines;
78
+ }
@@ -0,0 +1,19 @@
1
+ // Stable LSP tool names shared across registration, guidance, and runtime state.
2
+
3
+ export const LSP_LOOKUP_TOOL = "lsp_lookup";
4
+ export const LSP_DOCUMENT_SYMBOLS_TOOL = "lsp_document_symbols";
5
+ export const LSP_WORKSPACE_SYMBOLS_TOOL = "lsp_workspace_symbols";
6
+ export const LSP_DIAGNOSTICS_TOOL = "lsp_diagnostics";
7
+ export const LSP_REFACTOR_TOOL = "lsp_refactor";
8
+ export const LSP_RECOVER_TOOL = "lsp_recover";
9
+
10
+ export const LSP_TOOL_NAMES = [
11
+ LSP_LOOKUP_TOOL,
12
+ LSP_DOCUMENT_SYMBOLS_TOOL,
13
+ LSP_WORKSPACE_SYMBOLS_TOOL,
14
+ LSP_DIAGNOSTICS_TOOL,
15
+ LSP_REFACTOR_TOOL,
16
+ LSP_RECOVER_TOOL,
17
+ ] as const;
18
+
19
+ export type LspToolName = (typeof LSP_TOOL_NAMES)[number];
@@ -1,15 +1,23 @@
1
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
1
+ import type {
2
+ AgentToolUpdateCallback,
3
+ EditToolInput,
4
+ ExtensionAPI,
5
+ ExtensionContext,
6
+ ReadToolInput,
7
+ WriteToolInput,
8
+ } from "@earendil-works/pi-coding-agent";
2
9
  import { createEditTool, createReadTool, createWriteTool } from "@earendil-works/pi-coding-agent";
3
- import { augmentDiagnostics } from "./diagnostics/diagnostic-augmentation.ts";
4
- import { formatGroupedDiagnostics } from "./diagnostics/diagnostics.ts";
5
- import { splitSuppressionDiagnostics } from "./diagnostics/suppression-diagnostics.ts";
6
- import type { LspManager } from "./manager/manager.ts";
7
- import type { Diagnostic } from "./types.ts";
10
+ import type { Diagnostic } from "../config/types.ts";
11
+ import { augmentDiagnostics } from "../diagnostics/diagnostic-augmentation.ts";
12
+ import { formatGroupedDiagnostics } from "../diagnostics/diagnostics.ts";
13
+ import { splitSuppressionDiagnostics } from "../diagnostics/suppression-diagnostics.ts";
14
+ import type { LspManager } from "../manager/manager.ts";
15
+ import { resolveSessionPath } from "../utils.ts";
8
16
 
9
17
  interface LspOverrideState {
10
18
  getInlineSeverity(): number;
11
19
  getManager(): LspManager | null;
12
- getCwd(): string;
20
+ isActive(): boolean;
13
21
  }
14
22
 
15
23
  export function registerLspAwareToolOverrides(pi: ExtensionAPI, state: LspOverrideState): void {
@@ -20,11 +28,17 @@ export function registerLspAwareToolOverrides(pi: ExtensionAPI, state: LspOverri
20
28
  pi.registerTool({
21
29
  ...readMeta,
22
30
  // biome-ignore lint/complexity/useMaxParams: pi ToolDefinition.execute signature
23
- async execute(toolCallId, params, signal, onUpdate, _ctx) {
24
- const cwd = state.getCwd();
25
- const originalRead = createReadTool(cwd);
31
+ async execute(
32
+ toolCallId: string,
33
+ params: ReadToolInput,
34
+ signal: AbortSignal | undefined,
35
+ onUpdate: AgentToolUpdateCallback | undefined,
36
+ ctx: ExtensionContext,
37
+ ) {
38
+ const originalRead = createReadTool(ctx.cwd);
26
39
  const result = await originalRead.execute(toolCallId, params, signal, onUpdate);
27
- await ensureFileOpen(state.getManager(), params.path);
40
+ if (!state.isActive()) return result;
41
+ await ensureFileOpen(state.getManager(), ctx.cwd, params.path);
28
42
  return result;
29
43
  },
30
44
  });
@@ -32,15 +46,21 @@ export function registerLspAwareToolOverrides(pi: ExtensionAPI, state: LspOverri
32
46
  pi.registerTool({
33
47
  ...writeMeta,
34
48
  // biome-ignore lint/complexity/useMaxParams: pi ToolDefinition.execute signature
35
- async execute(toolCallId, params, signal, onUpdate, _ctx) {
36
- const cwd = state.getCwd();
37
- const originalWrite = createWriteTool(cwd);
49
+ async execute(
50
+ toolCallId: string,
51
+ params: WriteToolInput,
52
+ signal: AbortSignal | undefined,
53
+ onUpdate: AgentToolUpdateCallback | undefined,
54
+ ctx: ExtensionContext,
55
+ ) {
56
+ const originalWrite = createWriteTool(ctx.cwd);
38
57
  const result = await originalWrite.execute(toolCallId, params, signal, onUpdate);
58
+ if (!state.isActive()) return result;
39
59
  return appendInlineDiagnostics({
40
60
  manager: state.getManager(),
41
61
  filePath: params.path,
42
62
  inlineSeverity: state.getInlineSeverity(),
43
- cwd,
63
+ cwd: ctx.cwd,
44
64
  result,
45
65
  });
46
66
  },
@@ -49,15 +69,21 @@ export function registerLspAwareToolOverrides(pi: ExtensionAPI, state: LspOverri
49
69
  pi.registerTool({
50
70
  ...editMeta,
51
71
  // biome-ignore lint/complexity/useMaxParams: pi ToolDefinition.execute signature
52
- async execute(toolCallId, params, signal, onUpdate, _ctx) {
53
- const cwd = state.getCwd();
54
- const originalEdit = createEditTool(cwd);
72
+ async execute(
73
+ toolCallId: string,
74
+ params: EditToolInput,
75
+ signal: AbortSignal | undefined,
76
+ onUpdate: AgentToolUpdateCallback | undefined,
77
+ ctx: ExtensionContext,
78
+ ) {
79
+ const originalEdit = createEditTool(ctx.cwd);
55
80
  const result = await originalEdit.execute(toolCallId, params, signal, onUpdate);
81
+ if (!state.isActive()) return result;
56
82
  return appendInlineDiagnostics({
57
83
  manager: state.getManager(),
58
84
  filePath: params.path,
59
85
  inlineSeverity: state.getInlineSeverity(),
60
- cwd,
86
+ cwd: ctx.cwd,
61
87
  result,
62
88
  });
63
89
  },
@@ -78,17 +104,18 @@ async function appendInlineDiagnostics<T extends { content: unknown[]; details:
78
104
  if (!options.manager) return options.result;
79
105
 
80
106
  try {
107
+ const resolvedFilePath = resolveSessionPath(options.cwd, options.filePath);
81
108
  const effectiveSeverity = Math.max(options.inlineSeverity, 2);
82
109
  const entries = await options.manager.syncFileAndGetCascadingDiagnostics(
83
- options.filePath,
110
+ resolvedFilePath,
84
111
  effectiveSeverity,
85
112
  );
86
113
  if (entries.length === 0) return options.result;
87
114
 
88
115
  const primaryDiagnostics =
89
- entries.find((entry) => entry.file === options.filePath)?.diagnostics ?? [];
116
+ entries.find((entry) => entry.file === resolvedFilePath)?.diagnostics ?? [];
90
117
  const augmentation = await augmentDiagnostics(
91
- options.filePath,
118
+ resolvedFilePath,
92
119
  splitSuppressionDiagnostics(primaryDiagnostics, options.inlineSeverity).regular,
93
120
  options.manager,
94
121
  options.cwd,
@@ -162,11 +189,15 @@ export function buildInlineDiagnosticsMessage(
162
189
  return sections.join("\n\n");
163
190
  }
164
191
 
165
- async function ensureFileOpen(manager: LspManager | null, filePath: string): Promise<void> {
192
+ async function ensureFileOpen(
193
+ manager: LspManager | null,
194
+ cwd: string,
195
+ filePath: string,
196
+ ): Promise<void> {
166
197
  if (!manager) return;
167
198
 
168
199
  try {
169
- await manager.ensureFileOpen(filePath);
200
+ await manager.ensureFileOpen(resolveSessionPath(cwd, filePath));
170
201
  } catch {
171
202
  // Never block the agent on LSP errors
172
203
  }
@@ -0,0 +1,71 @@
1
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import { getSessionLspService } from "../session/service-registry.ts";
3
+ import type { LspToolPromptSurfaceMap } from "./guidance.ts";
4
+ import { LSP_TOOL_DEFINITION_SPECS } from "./tool-specs.ts";
5
+
6
+ /** Register the expert LSP toolset. Tools are re-registered on session_start to refresh guidance. */
7
+ export function registerLspTools(pi: ExtensionAPI, promptSurfaces: LspToolPromptSurfaceMap): void {
8
+ for (const spec of LSP_TOOL_DEFINITION_SPECS) {
9
+ const surface = promptSurfaces[spec.name];
10
+ pi.registerTool({
11
+ name: spec.name,
12
+ label: spec.label,
13
+ description: surface.description,
14
+ promptSnippet: surface.promptSnippet,
15
+ promptGuidelines: surface.promptGuidelines,
16
+ parameters: spec.parameters,
17
+ execute: createToolExecutor(spec.run),
18
+ });
19
+ }
20
+ }
21
+
22
+ function getReadyService(cwd: string) {
23
+ const state = getSessionLspService(cwd);
24
+ return state.kind === "ready" ? state.service : null;
25
+ }
26
+
27
+ function describeUnavailableService(cwd: string): string {
28
+ const state = getSessionLspService(cwd);
29
+ switch (state.kind) {
30
+ case "pending":
31
+ return "LSP is still starting for this workspace. Retry in a moment.";
32
+ case "inactive":
33
+ return `LSP is inactive on the current session branch for ${cwd}.`;
34
+ case "disabled":
35
+ return `LSP is disabled for ${cwd}.`;
36
+ case "unavailable":
37
+ return state.reason;
38
+ default:
39
+ return "LSP not initialized. Start a new session first.";
40
+ }
41
+ }
42
+
43
+ function createToolExecutor(
44
+ run: (
45
+ service: NonNullable<ReturnType<typeof getReadyService>>,
46
+ cwd: string,
47
+ params: unknown,
48
+ ) => Promise<string>,
49
+ ) {
50
+ // biome-ignore lint/complexity/useMaxParams: pi ToolDefinition.execute signature
51
+ return async (
52
+ _toolCallId: string,
53
+ params: unknown,
54
+ _signal: AbortSignal | undefined,
55
+ _onUpdate: unknown,
56
+ ctx: ExtensionContext,
57
+ ) => {
58
+ const service = getReadyService(ctx.cwd);
59
+ const text = service
60
+ ? await run(service, ctx.cwd, params)
61
+ : describeUnavailableService(ctx.cwd);
62
+ return makeTextResult(text);
63
+ };
64
+ }
65
+
66
+ function makeTextResult(text: string) {
67
+ return {
68
+ content: [{ type: "text" as const, text }],
69
+ details: {},
70
+ };
71
+ }