@mrclrchtr/supi-lsp 1.16.0 → 2.0.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 (77) hide show
  1. package/README.md +44 -56
  2. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/package.json +6 -1
  3. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/abort-utils.ts +31 -0
  4. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/api.ts +8 -0
  5. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/config/config-settings.ts +10 -4
  6. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/config.ts +1 -0
  7. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/footer-registry.ts +1 -1
  8. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/index.ts +6 -0
  9. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/model-selection.ts +134 -0
  10. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/settings/settings-registry.ts +9 -2
  11. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/settings/settings-ui.ts +109 -4
  12. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/settings-ui.ts +5 -1
  13. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/spinner-frames.ts +11 -0
  14. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/status-spinner.ts +68 -0
  15. package/node_modules/@mrclrchtr/supi-code-runtime/package.json +3 -2
  16. package/node_modules/@mrclrchtr/supi-code-runtime/src/api.ts +7 -0
  17. package/node_modules/@mrclrchtr/supi-code-runtime/src/capability/types.ts +56 -2
  18. package/node_modules/@mrclrchtr/supi-code-runtime/src/evidence-badge.ts +40 -0
  19. package/node_modules/@mrclrchtr/supi-code-runtime/src/index.ts +5 -0
  20. package/node_modules/@mrclrchtr/supi-code-runtime/src/types.ts +76 -3
  21. package/node_modules/@mrclrchtr/supi-code-runtime/src/workspace/runtime.ts +37 -2
  22. package/node_modules/@mrclrchtr/supi-core/package.json +6 -1
  23. package/node_modules/@mrclrchtr/supi-core/src/abort-utils.ts +31 -0
  24. package/node_modules/@mrclrchtr/supi-core/src/api.ts +8 -0
  25. package/node_modules/@mrclrchtr/supi-core/src/config/config-settings.ts +10 -4
  26. package/node_modules/@mrclrchtr/supi-core/src/config.ts +1 -0
  27. package/node_modules/@mrclrchtr/supi-core/src/footer-registry.ts +1 -1
  28. package/node_modules/@mrclrchtr/supi-core/src/index.ts +6 -0
  29. package/node_modules/@mrclrchtr/supi-core/src/model-selection.ts +134 -0
  30. package/node_modules/@mrclrchtr/supi-core/src/settings/settings-registry.ts +9 -2
  31. package/node_modules/@mrclrchtr/supi-core/src/settings/settings-ui.ts +109 -4
  32. package/node_modules/@mrclrchtr/supi-core/src/settings-ui.ts +5 -1
  33. package/node_modules/@mrclrchtr/supi-core/src/spinner-frames.ts +11 -0
  34. package/node_modules/@mrclrchtr/supi-core/src/status-spinner.ts +68 -0
  35. package/node_modules/vscode-languageserver-protocol/lib/common/protocol.diagnostic.d.ts +21 -2
  36. package/node_modules/vscode-languageserver-protocol/lib/common/protocol.diagnostic.js +1 -0
  37. package/node_modules/vscode-languageserver-protocol/lib/common/protocol.inlineValue.d.ts +2 -1
  38. package/node_modules/vscode-languageserver-protocol/package.json +1 -1
  39. package/package.json +4 -31
  40. package/src/api.ts +27 -0
  41. package/src/client/client-refresh.ts +5 -0
  42. package/src/client/client.ts +246 -1
  43. package/src/client/transport.ts +4 -0
  44. package/src/config/capabilities.ts +3 -0
  45. package/src/config/config.ts +4 -2
  46. package/src/config/lsp-settings.ts +102 -0
  47. package/src/config/server-actions.ts +59 -0
  48. package/src/config/server-config.ts +4 -0
  49. package/src/index.ts +11 -0
  50. package/src/manager/capability-index.ts +1 -1
  51. package/src/manager/manager-project-info.ts +2 -1
  52. package/src/manager/manager.ts +115 -26
  53. package/src/provider/lsp-semantic-provider.ts +177 -134
  54. package/src/provider/refactor-planning.ts +214 -0
  55. package/src/session/runtime-controller.ts +320 -0
  56. package/src/session/runtime-registration.ts +26 -5
  57. package/src/session/service-registry.ts +81 -4
  58. package/src/summary.ts +2 -0
  59. package/src/extension.ts +0 -1
  60. package/src/format.ts +0 -373
  61. package/src/handlers/diagnostic-injection.ts +0 -182
  62. package/src/handlers/session-lifecycle.ts +0 -149
  63. package/src/handlers/status-command.ts +0 -38
  64. package/src/handlers/workspace-recovery.ts +0 -92
  65. package/src/lsp.ts +0 -37
  66. package/src/session/lsp-state.ts +0 -93
  67. package/src/session/settings-registration.ts +0 -296
  68. package/src/session/tree-persist.ts +0 -75
  69. package/src/tool/guidance.ts +0 -69
  70. package/src/tool/names.ts +0 -27
  71. package/src/tool/overrides.ts +0 -204
  72. package/src/tool/register-tools.ts +0 -69
  73. package/src/tool/service-actions.ts +0 -399
  74. package/src/tool/tool-specs.ts +0 -248
  75. package/src/ui/renderer.ts +0 -120
  76. package/src/ui/ui.ts +0 -360
  77. package/src/workspace-change.ts +0 -40
@@ -1,296 +0,0 @@
1
- // LSP settings registration for the supi settings registry.
2
-
3
- import { CONFIG_DIR_NAME, getSettingsListTheme } from "@earendil-works/pi-coding-agent";
4
- import type { SettingItem } from "@earendil-works/pi-tui";
5
- import { Container, Key, matchesKey, SettingsList, Text } from "@earendil-works/pi-tui";
6
- import {
7
- loadSupiConfig,
8
- loadSupiConfigForScope,
9
- registerConfigSettings,
10
- } from "@mrclrchtr/supi-core/config";
11
- import { loadConfig } from "../config/config.ts";
12
-
13
- // ── Types ────────────────────────────────────────────────────
14
-
15
- export interface LspSettings {
16
- enabled: boolean;
17
- severity: number;
18
- active: string[];
19
- exclude: string[];
20
- }
21
-
22
- const LSP_DEFAULTS: LspSettings = {
23
- enabled: true,
24
- severity: 1,
25
- active: [],
26
- exclude: [],
27
- };
28
-
29
- // ── Config helpers ───────────────────────────────────────────
30
-
31
- export function loadLspSettings(cwd: string, homeDir?: string): LspSettings {
32
- return loadSupiConfig("lsp", cwd, LSP_DEFAULTS, { homeDir });
33
- }
34
-
35
- /**
36
- * Return a user-facing message that indicates which config scope disabled LSP.
37
- */
38
- export function getLspDisabledMessage(cwd: string, homeDir?: string): string {
39
- const global = loadSupiConfigForScope("lsp", cwd, LSP_DEFAULTS, { scope: "global", homeDir });
40
- const project = loadSupiConfigForScope("lsp", cwd, LSP_DEFAULTS, { scope: "project", homeDir });
41
-
42
- if (project.enabled === false) {
43
- return `LSP is disabled in project settings (${CONFIG_DIR_NAME}/supi/config.json)`;
44
- }
45
- if (global.enabled === false) {
46
- return `LSP is disabled in global settings (~/${CONFIG_DIR_NAME}/agent/supi/config.json)`;
47
- }
48
- return "LSP is disabled in settings";
49
- }
50
-
51
- function severityLabel(severity: number): string {
52
- switch (severity) {
53
- case 1:
54
- return "errors";
55
- case 2:
56
- return "warnings";
57
- case 3:
58
- return "info";
59
- case 4:
60
- return "hints";
61
- default:
62
- return "errors";
63
- }
64
- }
65
-
66
- // ── Settings registration ────────────────────────────────────
67
-
68
- export function registerLspSettings(): void {
69
- registerConfigSettings({
70
- id: "lsp",
71
- label: "LSP",
72
- section: "lsp",
73
- defaults: LSP_DEFAULTS,
74
- buildItems: (settings, scope, cwd) => buildLspSettingItems(settings, scope, cwd),
75
- // biome-ignore lint/complexity/useMaxParams: ConfigSettingsOptions interface callback
76
- persistChange: (_scope, _cwd, settingId, value, helpers) => {
77
- handlePersistChange(settingId, value, helpers);
78
- },
79
- });
80
- }
81
-
82
- function handlePersistChange(
83
- settingId: string,
84
- value: string,
85
- helpers: { set: (key: string, value: unknown) => void; unset: (key: string) => void },
86
- ): void {
87
- switch (settingId) {
88
- case "enabled":
89
- helpers.set("enabled", value === "on");
90
- break;
91
- case "severity": {
92
- const num = Number.parseInt(value.split(" ")[0] ?? "1", 10);
93
- helpers.set("severity", Number.isNaN(num) ? 1 : num);
94
- break;
95
- }
96
- case "active": {
97
- const active = value
98
- .split(",")
99
- .map((s) => s.trim())
100
- .filter((s) => s.length > 0);
101
- if (active.length > 0) {
102
- helpers.set("active", active);
103
- } else {
104
- helpers.unset("active");
105
- }
106
- break;
107
- }
108
- case "exclude": {
109
- const patterns = value
110
- .split(",")
111
- .map((s) => s.trim())
112
- .filter((s) => s.length > 0);
113
- if (patterns.length > 0) {
114
- helpers.set("exclude", patterns);
115
- } else {
116
- helpers.unset("exclude");
117
- }
118
- break;
119
- }
120
- }
121
- }
122
-
123
- function buildLspSettingItems(
124
- settings: LspSettings,
125
- scope: "project" | "global",
126
- cwd: string,
127
- ): SettingItem[] {
128
- return [
129
- {
130
- id: "enabled",
131
- label: "Enable LSP",
132
- description: "Enable or disable all LSP functionality",
133
- currentValue: settings.enabled ? "on" : "off",
134
- values: ["on", "off"],
135
- },
136
- {
137
- id: "severity",
138
- label: "Inline Severity",
139
- description: "Minimum diagnostic severity to show inline (1=errors, 4=hints)",
140
- currentValue: `${settings.severity} (${severityLabel(settings.severity)})`,
141
- values: ["1 (errors)", "2 (warnings)", "3 (info)", "4 (hints)"],
142
- },
143
- {
144
- id: "active",
145
- label: "Active Servers",
146
- description: "Press Enter to configure which language servers are active",
147
- currentValue: settings.active.length > 0 ? settings.active.join(", ") : "all",
148
- submenu: (_currentValue, done) => createServerSubmenu(scope, cwd, settings, done),
149
- },
150
- {
151
- id: "exclude",
152
- label: "Exclude Patterns",
153
- description:
154
- "Gitignore patterns to suppress LSP diagnostics. Edit .pi/supi/config.json → lsp.exclude (or ~/.pi/agent/supi/config.json for global). Patterns like __tests__/ exclude a directory, *.test.ts wildcards match at any depth, /dist anchors to root.",
155
- currentValue: settings.exclude.length > 0 ? settings.exclude.join(", ") : "none",
156
- submenu: (_currentValue, done) => createExcludeSubmenu(scope, cwd, settings, done),
157
- },
158
- ];
159
- }
160
-
161
- // ── Server submenu ───────────────────────────────────────────
162
-
163
- function createServerSubmenu(
164
- _scope: "project" | "global",
165
- cwd: string,
166
- settings: LspSettings,
167
- done: (selectedValue?: string) => void,
168
- ): {
169
- render: (width: number) => string[];
170
- invalidate: () => void;
171
- handleInput: (data: string) => boolean;
172
- } {
173
- const config = loadConfig(cwd);
174
- const allServers = Object.keys(config.servers);
175
- const allEnabled = settings.active.length === 0;
176
- const enabledServers = new Set(settings.active);
177
-
178
- const items: SettingItem[] = allServers.map((name) => ({
179
- id: name,
180
- label: name,
181
- currentValue: allEnabled || enabledServers.has(name) ? "enabled" : "disabled",
182
- values: ["enabled", "disabled"],
183
- }));
184
-
185
- let dirty = false;
186
-
187
- const container = new Container();
188
- const header = new Text("Active Servers — all enabled by default", 0, 0);
189
- container.addChild(header);
190
-
191
- const settingsList = new SettingsList(
192
- items,
193
- Math.min(items.length + 2, 15),
194
- getSettingsListTheme(),
195
- (id, newValue) => {
196
- const idx = items.findIndex((i) => i.id === id);
197
- if (idx >= 0 && items[idx].currentValue !== newValue) {
198
- dirty = true;
199
- items[idx].currentValue = newValue;
200
- }
201
- },
202
- () => {
203
- // Escape on inner SettingsList — no-op, handled by submenu wrapper
204
- },
205
- { enableSearch: true },
206
- );
207
-
208
- container.addChild(settingsList);
209
-
210
- return {
211
- render: (width: number) => container.render(width),
212
- invalidate: () => container.invalidate(),
213
- handleInput: (data: string) => {
214
- if (matchesKey(data, Key.escape)) {
215
- if (!dirty) {
216
- done();
217
- return true;
218
- }
219
- const enabled = items.filter((i) => i.currentValue === "enabled").map((i) => i.id);
220
- done(enabled.join(", ") || undefined);
221
- return true;
222
- }
223
- settingsList.handleInput?.(data);
224
- return true;
225
- },
226
- };
227
- }
228
-
229
- // ── Exclude patterns submenu ─────────────────────────────────
230
-
231
- function createExcludeSubmenu(
232
- _scope: "project" | "global",
233
- _cwd: string,
234
- settings: LspSettings,
235
- done: (selectedValue?: string) => void,
236
- ): {
237
- render: (width: number) => string[];
238
- invalidate: () => void;
239
- handleInput: (data: string) => boolean;
240
- } {
241
- const items: SettingItem[] = settings.exclude.map((pattern) => ({
242
- id: pattern,
243
- label: pattern,
244
- currentValue: "enabled",
245
- values: ["enabled", "disabled"],
246
- }));
247
-
248
- let dirty = false;
249
-
250
- const container = new Container();
251
- const header = new Text("Exclude Patterns — toggle off to remove", 0, 0);
252
- container.addChild(header);
253
-
254
- const footer = new Text(
255
- `Add new patterns in ${CONFIG_DIR_NAME}/supi/config.json under lsp.exclude`,
256
- 0,
257
- 0,
258
- );
259
- container.addChild(footer);
260
-
261
- const settingsList = new SettingsList(
262
- items,
263
- Math.min(items.length + 3, 15),
264
- getSettingsListTheme(),
265
- (id, newValue) => {
266
- const idx = items.findIndex((i) => i.id === id);
267
- if (idx >= 0 && items[idx].currentValue !== newValue) {
268
- dirty = true;
269
- items[idx].currentValue = newValue;
270
- }
271
- },
272
- () => {
273
- // Escape on inner SettingsList — no-op, handled by submenu wrapper
274
- },
275
- );
276
-
277
- container.addChild(settingsList);
278
-
279
- return {
280
- render: (width: number) => container.render(width),
281
- invalidate: () => container.invalidate(),
282
- handleInput: (data: string) => {
283
- if (matchesKey(data, Key.escape)) {
284
- if (!dirty) {
285
- done();
286
- return true;
287
- }
288
- const enabled = items.filter((i) => i.currentValue === "enabled").map((i) => i.id);
289
- done(enabled.join(", ") || undefined);
290
- return true;
291
- }
292
- settingsList.handleInput?.(data);
293
- return true;
294
- },
295
- };
296
- }
@@ -1,75 +0,0 @@
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
- }
@@ -1,69 +0,0 @@
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/server-config.ts";
5
- import { LSP_HOVER_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
- spec.name === LSP_HOVER_TOOL && coverageGuidelines.length > 0
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 for more 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
- }
package/src/tool/names.ts DELETED
@@ -1,27 +0,0 @@
1
- // Stable LSP tool names shared across registration, guidance, and runtime state.
2
-
3
- export const LSP_HOVER_TOOL = "lsp_hover";
4
- export const LSP_DEFINITION_TOOL = "lsp_definition";
5
- export const LSP_REFERENCES_TOOL = "lsp_references";
6
- export const LSP_IMPLEMENTATION_TOOL = "lsp_implementation";
7
- export const LSP_DOCUMENT_SYMBOLS_TOOL = "lsp_document_symbols";
8
- export const LSP_WORKSPACE_SYMBOLS_TOOL = "lsp_workspace_symbols";
9
- export const LSP_DIAGNOSTICS_TOOL = "lsp_diagnostics";
10
- export const LSP_RENAME_TOOL = "lsp_rename";
11
- export const LSP_CODE_ACTIONS_TOOL = "lsp_code_actions";
12
- export const LSP_RECOVER_TOOL = "lsp_recover";
13
-
14
- export const LSP_TOOL_NAMES = [
15
- LSP_HOVER_TOOL,
16
- LSP_DEFINITION_TOOL,
17
- LSP_REFERENCES_TOOL,
18
- LSP_IMPLEMENTATION_TOOL,
19
- LSP_DOCUMENT_SYMBOLS_TOOL,
20
- LSP_WORKSPACE_SYMBOLS_TOOL,
21
- LSP_DIAGNOSTICS_TOOL,
22
- LSP_RENAME_TOOL,
23
- LSP_CODE_ACTIONS_TOOL,
24
- LSP_RECOVER_TOOL,
25
- ] as const;
26
-
27
- export type LspToolName = (typeof LSP_TOOL_NAMES)[number];
@@ -1,204 +0,0 @@
1
- import type {
2
- AgentToolUpdateCallback,
3
- EditToolInput,
4
- ExtensionAPI,
5
- ExtensionContext,
6
- ReadToolInput,
7
- WriteToolInput,
8
- } from "@earendil-works/pi-coding-agent";
9
- import { createEditTool, createReadTool, createWriteTool } from "@earendil-works/pi-coding-agent";
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";
16
-
17
- interface LspOverrideState {
18
- getInlineSeverity(): number;
19
- getManager(): LspManager | null;
20
- isActive(): boolean;
21
- }
22
-
23
- export function registerLspAwareToolOverrides(pi: ExtensionAPI, state: LspOverrideState): void {
24
- const readMeta = createReadTool(process.cwd());
25
- const writeMeta = createWriteTool(process.cwd());
26
- const editMeta = createEditTool(process.cwd());
27
-
28
- pi.registerTool({
29
- ...readMeta,
30
- // biome-ignore lint/complexity/useMaxParams: pi ToolDefinition.execute signature
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);
39
- const result = await originalRead.execute(toolCallId, params, signal, onUpdate);
40
- if (!state.isActive()) return result;
41
- await ensureFileOpen(state.getManager(), ctx.cwd, params.path);
42
- return result;
43
- },
44
- });
45
-
46
- pi.registerTool({
47
- ...writeMeta,
48
- // biome-ignore lint/complexity/useMaxParams: pi ToolDefinition.execute signature
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);
57
- const result = await originalWrite.execute(toolCallId, params, signal, onUpdate);
58
- if (!state.isActive()) return result;
59
- return appendInlineDiagnostics({
60
- manager: state.getManager(),
61
- filePath: params.path,
62
- inlineSeverity: state.getInlineSeverity(),
63
- cwd: ctx.cwd,
64
- result,
65
- });
66
- },
67
- });
68
-
69
- pi.registerTool({
70
- ...editMeta,
71
- // biome-ignore lint/complexity/useMaxParams: pi ToolDefinition.execute signature
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);
80
- const result = await originalEdit.execute(toolCallId, params, signal, onUpdate);
81
- if (!state.isActive()) return result;
82
- return appendInlineDiagnostics({
83
- manager: state.getManager(),
84
- filePath: params.path,
85
- inlineSeverity: state.getInlineSeverity(),
86
- cwd: ctx.cwd,
87
- result,
88
- });
89
- },
90
- });
91
- }
92
-
93
- interface AppendInlineDiagnosticsOptions<T extends { content: unknown[]; details: unknown }> {
94
- manager: LspManager | null;
95
- filePath: string;
96
- inlineSeverity: number;
97
- cwd: string;
98
- result: T;
99
- }
100
-
101
- async function appendInlineDiagnostics<T extends { content: unknown[]; details: unknown }>(
102
- options: AppendInlineDiagnosticsOptions<T>,
103
- ): Promise<T> {
104
- if (!options.manager) return options.result;
105
-
106
- try {
107
- const resolvedFilePath = resolveSessionPath(options.cwd, options.filePath);
108
- const effectiveSeverity = Math.max(options.inlineSeverity, 2);
109
- const entries = await options.manager.syncFileAndGetCascadingDiagnostics(
110
- resolvedFilePath,
111
- effectiveSeverity,
112
- );
113
- if (entries.length === 0) return options.result;
114
-
115
- const primaryDiagnostics =
116
- entries.find((entry) => entry.file === resolvedFilePath)?.diagnostics ?? [];
117
- const augmentation = await augmentDiagnostics(
118
- resolvedFilePath,
119
- splitSuppressionDiagnostics(primaryDiagnostics, options.inlineSeverity).regular,
120
- options.manager,
121
- options.cwd,
122
- );
123
- const diagText = buildInlineDiagnosticsMessage(
124
- entries,
125
- options.cwd,
126
- options.inlineSeverity,
127
- augmentation ?? undefined,
128
- );
129
- if (!diagText) return options.result;
130
-
131
- const diagnosticContent = {
132
- type: "text" as const,
133
- text: `\n\n${diagText}`,
134
- } as T["content"][number];
135
-
136
- return {
137
- ...options.result,
138
- content: [...options.result.content, diagnosticContent],
139
- } as T;
140
- } catch {
141
- return options.result;
142
- }
143
- }
144
-
145
- export function buildInlineDiagnosticsMessage(
146
- entries: Array<{ file: string; diagnostics: Diagnostic[] }>,
147
- cwd: string,
148
- inlineSeverity: number = 1,
149
- augmentation?: string,
150
- ): string | null {
151
- const regularEntries: Array<{ file: string; diagnostics: Diagnostic[] }> = [];
152
- const suppressionEntries: Array<{ file: string; diagnostics: Diagnostic[] }> = [];
153
-
154
- for (const entry of entries) {
155
- const { regular, suppressions } = splitSuppressionDiagnostics(
156
- entry.diagnostics,
157
- inlineSeverity,
158
- );
159
- if (regular.length > 0) {
160
- regularEntries.push({ file: entry.file, diagnostics: regular });
161
- }
162
- if (suppressions.length > 0) {
163
- suppressionEntries.push({ file: entry.file, diagnostics: suppressions });
164
- }
165
- }
166
-
167
- if (regularEntries.length === 0 && suppressionEntries.length === 0) {
168
- return null;
169
- }
170
-
171
- const sections = ["⚠️ LSP Diagnostics — review before continuing:"];
172
-
173
- if (regularEntries.length > 0) {
174
- sections.push(formatGroupedDiagnostics(regularEntries, cwd));
175
- if (augmentation) {
176
- sections.push(augmentation);
177
- }
178
- }
179
-
180
- if (suppressionEntries.length > 0) {
181
- sections.push(
182
- `🗑️ Stale suppressions — cleanup available:\n${formatGroupedDiagnostics(suppressionEntries, cwd)}`,
183
- );
184
- }
185
-
186
- sections.push(
187
- "If these errors are unexpected or appear across multiple files, fix the root cause before editing more files.",
188
- );
189
- return sections.join("\n\n");
190
- }
191
-
192
- async function ensureFileOpen(
193
- manager: LspManager | null,
194
- cwd: string,
195
- filePath: string,
196
- ): Promise<void> {
197
- if (!manager) return;
198
-
199
- try {
200
- await manager.ensureFileOpen(resolveSessionPath(cwd, filePath));
201
- } catch {
202
- // Never block the agent on LSP errors
203
- }
204
- }