@mrclrchtr/supi-lsp 1.3.1 → 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 (61) hide show
  1. package/README.md +58 -39
  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-core/src/{context-provider-registry.ts → 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-core/src/{settings-registry.ts → settings/settings-registry.ts} +1 -1
  10. package/package.json +3 -2
  11. package/src/api.ts +16 -3
  12. package/src/client/client-refresh.ts +1 -1
  13. package/src/client/client.ts +27 -3
  14. package/src/client/transport.ts +61 -5
  15. package/src/config/tsconfig-scope.ts +244 -0
  16. package/src/{types.ts → config/types.ts} +4 -2
  17. package/src/coordinates.ts +11 -0
  18. package/src/diagnostics/diagnostic-augmentation.ts +5 -5
  19. package/src/diagnostics/diagnostic-context.ts +115 -0
  20. package/src/diagnostics/diagnostic-display.ts +1 -1
  21. package/src/diagnostics/diagnostic-summary.ts +3 -2
  22. package/src/diagnostics/diagnostics.ts +1 -1
  23. package/src/diagnostics/stale-diagnostics.ts +1 -1
  24. package/src/diagnostics/suppression-diagnostics.ts +1 -1
  25. package/src/{workspace-sentinels.ts → diagnostics/workspace-sentinels.ts} +2 -2
  26. package/src/format.ts +2 -23
  27. package/src/index.ts +18 -5
  28. package/src/lsp.ts +72 -120
  29. package/src/manager/manager-diagnostics.ts +1 -1
  30. package/src/manager/manager-helpers.ts +4 -2
  31. package/src/manager/manager-project-info.ts +10 -7
  32. package/src/manager/manager-workspace-recovery.ts +1 -1
  33. package/src/manager/manager-workspace-symbol.ts +158 -6
  34. package/src/manager/manager.ts +202 -43
  35. package/src/{lsp-state.ts → session/lsp-state.ts} +22 -11
  36. package/src/{scanner.ts → session/scanner.ts} +3 -3
  37. package/src/{service-registry.ts → session/service-registry.ts} +104 -12
  38. package/src/{settings-registration.ts → session/settings-registration.ts} +1 -1
  39. package/src/session/tree-persist.ts +75 -0
  40. package/src/summary.ts +1 -1
  41. package/src/tool/guidance.ts +138 -0
  42. package/src/tool/names.ts +19 -0
  43. package/src/{overrides.ts → tool/overrides.ts} +55 -24
  44. package/src/tool/register-tools.ts +224 -0
  45. package/src/tool/service-actions.ts +258 -0
  46. package/src/{ui.ts → ui/ui.ts} +4 -4
  47. package/src/utils.ts +11 -0
  48. package/src/guidance.ts +0 -163
  49. package/src/search-fallback.ts +0 -98
  50. package/src/tool-actions.ts +0 -430
  51. package/src/tree-persist.ts +0 -48
  52. package/src/tsconfig-scope.ts +0 -156
  53. /package/node_modules/@mrclrchtr/supi-core/src/{config.ts → config/config.ts} +0 -0
  54. /package/node_modules/@mrclrchtr/supi-core/src/{context-messages.ts → context/context-messages.ts} +0 -0
  55. /package/node_modules/@mrclrchtr/supi-core/src/{context-tag.ts → context/context-tag.ts} +0 -0
  56. /package/node_modules/@mrclrchtr/supi-core/src/{settings-command.ts → settings/settings-command.ts} +0 -0
  57. /package/node_modules/@mrclrchtr/supi-core/src/{settings-ui.ts → settings/settings-ui.ts} +0 -0
  58. /package/src/{capabilities.ts → config/capabilities.ts} +0 -0
  59. /package/src/{config.ts → config/config.ts} +0 -0
  60. /package/src/{defaults.json → config/defaults.json} +0 -0
  61. /package/src/{renderer.ts → ui/renderer.ts} +0 -0
@@ -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
+ }
package/src/summary.ts CHANGED
@@ -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,138 @@
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 {
6
+ LSP_DIAGNOSTICS_TOOL,
7
+ LSP_DOCUMENT_SYMBOLS_TOOL,
8
+ LSP_LOOKUP_TOOL,
9
+ LSP_RECOVER_TOOL,
10
+ LSP_REFACTOR_TOOL,
11
+ LSP_WORKSPACE_SYMBOLS_TOOL,
12
+ type LspToolName,
13
+ } from "./names.ts";
14
+
15
+ export interface LspToolPromptSurface {
16
+ description: string;
17
+ promptSnippet: string;
18
+ promptGuidelines: string[];
19
+ }
20
+
21
+ export type LspToolPromptSurfaceMap = Record<LspToolName, LspToolPromptSurface>;
22
+
23
+ const LOOKUP_GUIDELINES = [
24
+ 'Use lsp_lookup with `kind: "hover"` for semantic type or symbol information at a known `file`, `line`, and `character`.',
25
+ 'Use lsp_lookup with `kind: "definition"`, `"references"`, or `"implementation"` for semantic navigation at a known position.',
26
+ "Use lsp_lookup after code_intel or tree_sitter has already narrowed the target file and position.",
27
+ ];
28
+
29
+ const DOCUMENT_SYMBOL_GUIDELINES = [
30
+ "Use lsp_document_symbols(file) for semantic declarations in one supported file.",
31
+ ];
32
+
33
+ const WORKSPACE_SYMBOL_GUIDELINES = [
34
+ "Use lsp_workspace_symbols(query) for semantic symbol-name lookup across the current project.",
35
+ ];
36
+
37
+ const DIAGNOSTICS_GUIDELINES = [
38
+ "Use lsp_diagnostics(file?) when you need current diagnostics for one file or a workspace-level summary.",
39
+ ];
40
+
41
+ const REFACTOR_GUIDELINES = [
42
+ 'Use lsp_refactor with `kind: "rename"` for semantic rename planning at a known `file`, `line`, and `character`.',
43
+ 'Use lsp_refactor with `kind: "code_actions"` for semantic fixes or refactors at a known position.',
44
+ ];
45
+
46
+ const RECOVER_GUIDELINES = [
47
+ "Use lsp_recover() when diagnostics look stale after workspace-level changes or generated-file updates.",
48
+ ];
49
+
50
+ export const defaultLspToolPromptSurfaces = buildLspToolPromptSurfaces([], ".");
51
+
52
+ export function buildLspToolPromptSurfaces(
53
+ servers: ProjectServerInfo[],
54
+ cwd: string,
55
+ ): LspToolPromptSurfaceMap {
56
+ const coverageGuidelines = buildCoverageGuidelines(servers, cwd);
57
+
58
+ return {
59
+ [LSP_LOOKUP_TOOL]: {
60
+ description:
61
+ "Language Server Protocol lookup tool — semantic hover, definition, references, and implementation for supported files. Use lsp_lookup when you know the file and 1-based line/character position and need semantic drill-down rather than text search.",
62
+ promptSnippet:
63
+ "lsp_lookup — semantic hover/definition/references/implementation at a known file position",
64
+ promptGuidelines: [...LOOKUP_GUIDELINES, ...coverageGuidelines],
65
+ },
66
+ [LSP_DOCUMENT_SYMBOLS_TOOL]: {
67
+ description:
68
+ "Language Server Protocol document symbols tool — list semantic declarations in one supported file. Use lsp_document_symbols when you need a symbol-aware outline rather than raw text structure.",
69
+ promptSnippet: "lsp_document_symbols — semantic declarations for one supported file",
70
+ promptGuidelines: DOCUMENT_SYMBOL_GUIDELINES,
71
+ },
72
+ [LSP_WORKSPACE_SYMBOLS_TOOL]: {
73
+ description:
74
+ "Language Server Protocol workspace symbols tool — semantic symbol-name lookup across the current project. Use lsp_workspace_symbols to find declarations by name before opening a specific file.",
75
+ promptSnippet: "lsp_workspace_symbols — semantic symbol-name lookup across the project",
76
+ promptGuidelines: WORKSPACE_SYMBOL_GUIDELINES,
77
+ },
78
+ [LSP_DIAGNOSTICS_TOOL]: {
79
+ description:
80
+ "Language Server Protocol diagnostics tool — current diagnostics for one file or a workspace summary. Use lsp_diagnostics for semantic compiler or language-server issues instead of guessing from text alone.",
81
+ promptSnippet: "lsp_diagnostics — current diagnostics for one file or the workspace",
82
+ promptGuidelines: DIAGNOSTICS_GUIDELINES,
83
+ },
84
+ [LSP_REFACTOR_TOOL]: {
85
+ description:
86
+ "Language Server Protocol refactor tool — semantic rename planning and code actions at a known file position. Use lsp_refactor when you need language-server-backed edits or quick-fix suggestions.",
87
+ promptSnippet: "lsp_refactor — semantic rename planning and code actions at a known position",
88
+ promptGuidelines: REFACTOR_GUIDELINES,
89
+ },
90
+ [LSP_RECOVER_TOOL]: {
91
+ description:
92
+ "Language Server Protocol recover tool — refresh diagnostics after workspace changes and stale language-server state. Use lsp_recover when new files, generated types, or config updates leave diagnostics out of sync.",
93
+ promptSnippet: "lsp_recover — refresh stale diagnostics after workspace changes",
94
+ promptGuidelines: RECOVER_GUIDELINES,
95
+ },
96
+ };
97
+ }
98
+
99
+ function buildCoverageGuidelines(servers: ProjectServerInfo[], cwd: string): string[] {
100
+ const active = servers
101
+ .filter((server) => server.status === "running")
102
+ .map((server) => {
103
+ const root = displayRoot(server.root, cwd);
104
+ const fileTypes = server.fileTypes.map((entry) => `.${entry}`).join(",");
105
+ const actions = server.supportedActions.join(",");
106
+ const actionText = actions.length > 0 ? ` | actions: ${actions}` : "";
107
+ return `lsp server coverage: ${server.name} | root: ${root} | files: ${fileTypes}${actionText}`;
108
+ });
109
+
110
+ const unavailable = servers
111
+ .filter((server) => server.status !== "running")
112
+ .map((server) => server.name);
113
+
114
+ const dynamic = [...active];
115
+ if (unavailable.length > 0) {
116
+ dynamic.push(
117
+ `lsp server unavailable: ${unavailable.join(",")} — install or enable to extend semantic coverage`,
118
+ );
119
+ }
120
+
121
+ return dynamic;
122
+ }
123
+
124
+ function displayRoot(root: string, cwd: string): string {
125
+ const relative = path.relative(cwd, root);
126
+ if (relative === "") return ".";
127
+ if (relative.startsWith(`..${path.sep}`) || relative === "..") return root;
128
+ return relative.replaceAll(path.sep, "/");
129
+ }
130
+
131
+ // Compatibility exports for older internal tests and helper imports.
132
+ export const toolDescription = defaultLspToolPromptSurfaces[LSP_LOOKUP_TOOL].description;
133
+ export const promptSnippet = defaultLspToolPromptSurfaces[LSP_LOOKUP_TOOL].promptSnippet;
134
+ export const promptGuidelines = defaultLspToolPromptSurfaces[LSP_LOOKUP_TOOL].promptGuidelines;
135
+
136
+ export function buildProjectGuidelines(servers: ProjectServerInfo[], cwd: string): string[] {
137
+ return buildLspToolPromptSurfaces(servers, cwd)[LSP_LOOKUP_TOOL].promptGuidelines;
138
+ }
@@ -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,224 @@
1
+ import { StringEnum } from "@earendil-works/pi-ai";
2
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
3
+ import { Type } from "typebox";
4
+ import { getSessionLspService } from "../session/service-registry.ts";
5
+ import type { LspToolPromptSurfaceMap } from "./guidance.ts";
6
+ import {
7
+ LSP_DIAGNOSTICS_TOOL,
8
+ LSP_DOCUMENT_SYMBOLS_TOOL,
9
+ LSP_LOOKUP_TOOL,
10
+ LSP_RECOVER_TOOL,
11
+ LSP_REFACTOR_TOOL,
12
+ LSP_WORKSPACE_SYMBOLS_TOOL,
13
+ } from "./names.ts";
14
+ import {
15
+ executeDiagnostics,
16
+ executeDocumentSymbols,
17
+ executeLookup,
18
+ executeRecover,
19
+ executeRefactor,
20
+ executeWorkspaceSymbols,
21
+ } from "./service-actions.ts";
22
+
23
+ const FileParam = Type.String({ description: "File path (relative or absolute)" });
24
+ const LineParam = Type.Number({ description: "1-based line number", minimum: 1 });
25
+ const CharacterParam = Type.Number({ description: "1-based column number", minimum: 1 });
26
+ const QueryParam = Type.String({ description: "Symbol query string" });
27
+ const NewNameParam = Type.String({ description: "New name for rename" });
28
+
29
+ const LookupKindEnum = StringEnum(["hover", "definition", "references", "implementation"] as const);
30
+
31
+ const RefactorKindEnum = StringEnum(["rename", "code_actions"] as const);
32
+
33
+ const LookupParameters = Type.Object(
34
+ {
35
+ kind: LookupKindEnum,
36
+ file: FileParam,
37
+ line: LineParam,
38
+ character: CharacterParam,
39
+ },
40
+ { additionalProperties: false },
41
+ );
42
+
43
+ const DocumentSymbolsParameters = Type.Object(
44
+ {
45
+ file: FileParam,
46
+ },
47
+ { additionalProperties: false },
48
+ );
49
+
50
+ const WorkspaceSymbolsParameters = Type.Object(
51
+ {
52
+ query: QueryParam,
53
+ },
54
+ { additionalProperties: false },
55
+ );
56
+
57
+ const DiagnosticsParameters = Type.Object(
58
+ {
59
+ file: Type.Optional(FileParam),
60
+ },
61
+ { additionalProperties: false },
62
+ );
63
+
64
+ const RefactorParameters = Type.Object(
65
+ {
66
+ kind: RefactorKindEnum,
67
+ file: FileParam,
68
+ line: LineParam,
69
+ character: CharacterParam,
70
+ newName: Type.Optional(NewNameParam),
71
+ },
72
+ { additionalProperties: false },
73
+ );
74
+
75
+ const RecoverParameters = Type.Object({}, { additionalProperties: false });
76
+
77
+ /** Register the expert LSP toolset. Tools are re-registered on session_start to refresh guidance. */
78
+ export function registerLspTools(pi: ExtensionAPI, promptSurfaces: LspToolPromptSurfaceMap): void {
79
+ const lookupSurface = promptSurfaces[LSP_LOOKUP_TOOL];
80
+ pi.registerTool({
81
+ name: LSP_LOOKUP_TOOL,
82
+ label: "LSP Lookup",
83
+ description: lookupSurface.description,
84
+ promptSnippet: lookupSurface.promptSnippet,
85
+ promptGuidelines: lookupSurface.promptGuidelines,
86
+ parameters: LookupParameters,
87
+ execute: createToolExecutor((service, cwd, params) =>
88
+ executeLookup(service, cwd, params as Parameters<typeof executeLookup>[2]),
89
+ ),
90
+ });
91
+
92
+ const documentSymbolsSurface = promptSurfaces[LSP_DOCUMENT_SYMBOLS_TOOL];
93
+ pi.registerTool({
94
+ name: LSP_DOCUMENT_SYMBOLS_TOOL,
95
+ label: "LSP Document Symbols",
96
+ description: documentSymbolsSurface.description,
97
+ promptSnippet: documentSymbolsSurface.promptSnippet,
98
+ promptGuidelines: documentSymbolsSurface.promptGuidelines,
99
+ parameters: DocumentSymbolsParameters,
100
+ execute: createToolExecutor((service, cwd, params) =>
101
+ executeDocumentSymbols(service, cwd, params as Parameters<typeof executeDocumentSymbols>[2]),
102
+ ),
103
+ });
104
+
105
+ const workspaceSymbolsSurface = promptSurfaces[LSP_WORKSPACE_SYMBOLS_TOOL];
106
+ pi.registerTool({
107
+ name: LSP_WORKSPACE_SYMBOLS_TOOL,
108
+ label: "LSP Workspace Symbols",
109
+ description: workspaceSymbolsSurface.description,
110
+ promptSnippet: workspaceSymbolsSurface.promptSnippet,
111
+ promptGuidelines: workspaceSymbolsSurface.promptGuidelines,
112
+ parameters: WorkspaceSymbolsParameters,
113
+ execute: createToolExecutor((service, cwd, params) =>
114
+ executeWorkspaceSymbols(
115
+ service,
116
+ cwd,
117
+ params as Parameters<typeof executeWorkspaceSymbols>[2],
118
+ ),
119
+ ),
120
+ });
121
+
122
+ const diagnosticsSurface = promptSurfaces[LSP_DIAGNOSTICS_TOOL];
123
+ pi.registerTool({
124
+ name: LSP_DIAGNOSTICS_TOOL,
125
+ label: "LSP Diagnostics",
126
+ description: diagnosticsSurface.description,
127
+ promptSnippet: diagnosticsSurface.promptSnippet,
128
+ promptGuidelines: diagnosticsSurface.promptGuidelines,
129
+ parameters: DiagnosticsParameters,
130
+ execute: createToolExecutor((service, cwd, params) =>
131
+ executeDiagnostics(service, cwd, params as Parameters<typeof executeDiagnostics>[2]),
132
+ ),
133
+ });
134
+
135
+ const refactorSurface = promptSurfaces[LSP_REFACTOR_TOOL];
136
+ pi.registerTool({
137
+ name: LSP_REFACTOR_TOOL,
138
+ label: "LSP Refactor",
139
+ description: refactorSurface.description,
140
+ promptSnippet: refactorSurface.promptSnippet,
141
+ promptGuidelines: refactorSurface.promptGuidelines,
142
+ parameters: RefactorParameters,
143
+ execute: createToolExecutor((service, cwd, params) =>
144
+ executeRefactor(service, cwd, params as Parameters<typeof executeRefactor>[2]),
145
+ ),
146
+ });
147
+
148
+ const recoverSurface = promptSurfaces[LSP_RECOVER_TOOL];
149
+ pi.registerTool({
150
+ name: LSP_RECOVER_TOOL,
151
+ label: "LSP Recover",
152
+ description: recoverSurface.description,
153
+ promptSnippet: recoverSurface.promptSnippet,
154
+ promptGuidelines: recoverSurface.promptGuidelines,
155
+ parameters: RecoverParameters,
156
+ execute: createRecoverToolExecutor(),
157
+ });
158
+ }
159
+
160
+ function getReadyService(cwd: string) {
161
+ const state = getSessionLspService(cwd);
162
+ return state.kind === "ready" ? state.service : null;
163
+ }
164
+
165
+ function describeUnavailableService(cwd: string): string {
166
+ const state = getSessionLspService(cwd);
167
+ switch (state.kind) {
168
+ case "pending":
169
+ return "LSP is still starting for this workspace. Retry in a moment.";
170
+ case "inactive":
171
+ return `LSP is inactive on the current session branch for ${cwd}.`;
172
+ case "disabled":
173
+ return `LSP is disabled for ${cwd}.`;
174
+ case "unavailable":
175
+ return state.reason;
176
+ default:
177
+ return "LSP not initialized. Start a new session first.";
178
+ }
179
+ }
180
+
181
+ function createToolExecutor(
182
+ run: (
183
+ service: NonNullable<ReturnType<typeof getReadyService>>,
184
+ cwd: string,
185
+ params: unknown,
186
+ ) => Promise<string>,
187
+ ) {
188
+ // biome-ignore lint/complexity/useMaxParams: pi ToolDefinition.execute signature
189
+ return async (
190
+ _toolCallId: string,
191
+ params: unknown,
192
+ _signal: AbortSignal | undefined,
193
+ _onUpdate: unknown,
194
+ ctx: ExtensionContext,
195
+ ) => {
196
+ const service = getReadyService(ctx.cwd);
197
+ const text = service
198
+ ? await run(service, ctx.cwd, params)
199
+ : describeUnavailableService(ctx.cwd);
200
+ return makeTextResult(text);
201
+ };
202
+ }
203
+
204
+ function createRecoverToolExecutor() {
205
+ // biome-ignore lint/complexity/useMaxParams: pi ToolDefinition.execute signature
206
+ return async (
207
+ _toolCallId: string,
208
+ _params: unknown,
209
+ _signal: AbortSignal | undefined,
210
+ _onUpdate: unknown,
211
+ ctx: ExtensionContext,
212
+ ) => {
213
+ const service = getReadyService(ctx.cwd);
214
+ const text = service ? await executeRecover(service) : describeUnavailableService(ctx.cwd);
215
+ return makeTextResult(text);
216
+ };
217
+ }
218
+
219
+ function makeTextResult(text: string) {
220
+ return {
221
+ content: [{ type: "text" as const, text }],
222
+ details: {},
223
+ };
224
+ }