@ian-pascoe/pi-lsp 0.1.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.
- package/LICENSE +21 -0
- package/README.md +187 -0
- package/package.json +57 -0
- package/src/index.ts +1 -0
- package/src/lsp-position-encoding.ts +134 -0
- package/src/lsp-post-edit-diagnostics-rendering.ts +249 -0
- package/src/lsp-post-edit-diagnostics.ts +291 -0
- package/src/lsp-server-client.ts +1237 -0
- package/src/lsp-server-manager.ts +519 -0
- package/src/lsp-session-files.ts +107 -0
- package/src/lsp-tool-contract.ts +468 -0
- package/src/lsp-tool-output.ts +64 -0
- package/src/lsp-tool-rendering.ts +312 -0
- package/src/lsp-tool.ts +1214 -0
- package/src/lsp-workspace-edit.ts +872 -0
- package/src/pi-lsp-extension.ts +379 -0
- package/src/pi-lsp-settings.ts +263 -0
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import type { ToolResultEvent } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { type Static, Type } from "typebox";
|
|
3
|
+
import { Value } from "typebox/value";
|
|
4
|
+
import { MutationManifestSchema } from "./lsp-tool-contract.js";
|
|
5
|
+
|
|
6
|
+
const NativeMutationInputSchema = Type.Object(
|
|
7
|
+
{ path: Type.String() },
|
|
8
|
+
{ additionalProperties: true },
|
|
9
|
+
);
|
|
10
|
+
const ApplyPatchDetailsSchema = Type.Object(
|
|
11
|
+
{
|
|
12
|
+
status: Type.Union([Type.Literal("success"), Type.Literal("partial_failure")]),
|
|
13
|
+
result: Type.Object(
|
|
14
|
+
{
|
|
15
|
+
changedFiles: Type.Array(Type.String()),
|
|
16
|
+
createdFiles: Type.Array(Type.String()),
|
|
17
|
+
deletedFiles: Type.Array(Type.String()),
|
|
18
|
+
movedFiles: Type.Array(
|
|
19
|
+
Type.Object({ from: Type.String(), to: Type.String() }, { additionalProperties: true }),
|
|
20
|
+
),
|
|
21
|
+
fuzz: Type.Optional(Type.Number()),
|
|
22
|
+
},
|
|
23
|
+
{ additionalProperties: true },
|
|
24
|
+
),
|
|
25
|
+
},
|
|
26
|
+
{ additionalProperties: true },
|
|
27
|
+
);
|
|
28
|
+
const WorkspaceEditApplyDetailsSchema = Type.Object(
|
|
29
|
+
{
|
|
30
|
+
kind: Type.Literal("workspace_edit_apply"),
|
|
31
|
+
state: Type.Union([Type.Literal("applied"), Type.Literal("partial_failure")]),
|
|
32
|
+
changed_paths: Type.Array(Type.String()),
|
|
33
|
+
},
|
|
34
|
+
{ additionalProperties: true },
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
type ApplyPatchDetails = Static<typeof ApplyPatchDetailsSchema>;
|
|
38
|
+
|
|
39
|
+
/** A path changed by a Supported Mutation Tool and eligible for document diagnostics. */
|
|
40
|
+
export interface PostEditDiagnosticPath {
|
|
41
|
+
/** Absolute or tool-relative file path after the mutation. */
|
|
42
|
+
readonly path: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Runtime schema for one normalized LSP Diagnostic appended to a mutation result. */
|
|
46
|
+
export const PostEditLspDiagnosticSchema = Type.Object(
|
|
47
|
+
{
|
|
48
|
+
serverId: Type.String({ minLength: 1 }),
|
|
49
|
+
path: Type.String({ minLength: 1 }),
|
|
50
|
+
line: Type.Integer({ minimum: 1 }),
|
|
51
|
+
character: Type.Integer({ minimum: 1 }),
|
|
52
|
+
severity: Type.Number(),
|
|
53
|
+
message: Type.String(),
|
|
54
|
+
},
|
|
55
|
+
{ additionalProperties: false },
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
/** A normalized LSP Diagnostic appended to a mutation result. */
|
|
59
|
+
export type PostEditLspDiagnostic = Static<typeof PostEditLspDiagnosticSchema>;
|
|
60
|
+
|
|
61
|
+
/** Runtime schema for reportable and intentionally silent Post-edit Diagnostic outcomes. */
|
|
62
|
+
export const PostEditDiagnosticOutcomeSchema = Type.Union([
|
|
63
|
+
Type.Object(
|
|
64
|
+
{ kind: Type.Literal("diagnostic"), diagnostic: PostEditLspDiagnosticSchema },
|
|
65
|
+
{ additionalProperties: false },
|
|
66
|
+
),
|
|
67
|
+
Type.Object(
|
|
68
|
+
{ kind: Type.Literal("no_diagnostics"), path: Type.String({ minLength: 1 }) },
|
|
69
|
+
{ additionalProperties: false },
|
|
70
|
+
),
|
|
71
|
+
Type.Object(
|
|
72
|
+
{ kind: Type.Literal("no_configured_server"), path: Type.String({ minLength: 1 }) },
|
|
73
|
+
{ additionalProperties: false },
|
|
74
|
+
),
|
|
75
|
+
Type.Object(
|
|
76
|
+
{
|
|
77
|
+
kind: Type.Literal("timeout"),
|
|
78
|
+
path: Type.String({ minLength: 1 }),
|
|
79
|
+
serverId: Type.Optional(Type.String({ minLength: 1 })),
|
|
80
|
+
},
|
|
81
|
+
{ additionalProperties: false },
|
|
82
|
+
),
|
|
83
|
+
Type.Object(
|
|
84
|
+
{
|
|
85
|
+
kind: Type.Literal("unavailable_server"),
|
|
86
|
+
path: Type.String({ minLength: 1 }),
|
|
87
|
+
serverId: Type.Optional(Type.String({ minLength: 1 })),
|
|
88
|
+
},
|
|
89
|
+
{ additionalProperties: false },
|
|
90
|
+
),
|
|
91
|
+
Type.Object(
|
|
92
|
+
{ kind: Type.Literal("warning"), message: Type.String() },
|
|
93
|
+
{ additionalProperties: false },
|
|
94
|
+
),
|
|
95
|
+
]);
|
|
96
|
+
|
|
97
|
+
/** An explicit outcome when fresh diagnostics cannot be represented as a diagnostic. */
|
|
98
|
+
export type PostEditDiagnosticOutcome = Static<typeof PostEditDiagnosticOutcomeSchema>;
|
|
99
|
+
|
|
100
|
+
/** Runs fresh Post-edit Diagnostics for changed paths after a Supported Mutation Tool result. */
|
|
101
|
+
export interface PostEditDiagnosticsRunner {
|
|
102
|
+
/** Return every fresh diagnostic and explicit non-diagnostic outcome for the supplied paths. */
|
|
103
|
+
runPostEditDiagnostics(
|
|
104
|
+
paths: readonly PostEditDiagnosticPath[],
|
|
105
|
+
): Promise<readonly PostEditDiagnosticOutcome[]>;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Minimal Tool Result shape accepted by the structural post-edit adapters. */
|
|
109
|
+
export interface PostEditToolResult {
|
|
110
|
+
/** Tool name supplied by Pi's central tool-result event. */
|
|
111
|
+
readonly toolName: string;
|
|
112
|
+
/** Original tool arguments supplied to the central event. */
|
|
113
|
+
readonly input: ToolResultEvent["input"];
|
|
114
|
+
/** Tool-result details owned by the mutation implementation. */
|
|
115
|
+
readonly details: ToolResultEvent["details"];
|
|
116
|
+
/** Existing Pi content, retained verbatim before the appended LSP section. */
|
|
117
|
+
readonly content: ToolResultEvent["content"];
|
|
118
|
+
/** Existing tool failure state, which diagnostics must not change. */
|
|
119
|
+
readonly isError: boolean;
|
|
120
|
+
/** Existing usage accounting, which diagnostics must not change. */
|
|
121
|
+
readonly usage?: ToolResultEvent["usage"];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Tool-result fields returned by Post-edit Diagnostics middleware without changing mutation state. */
|
|
125
|
+
export interface PostEditDiagnosticsResultPatch {
|
|
126
|
+
/** Original content with exactly one deterministic LSP section appended. */
|
|
127
|
+
readonly content: ToolResultEvent["content"];
|
|
128
|
+
/** Original details retained exactly for downstream middleware and session replay. */
|
|
129
|
+
readonly details: ToolResultEvent["details"];
|
|
130
|
+
/** Original mutation error state retained exactly. */
|
|
131
|
+
readonly isError: boolean;
|
|
132
|
+
/** Original usage retained when Pi supplied it. */
|
|
133
|
+
readonly usage?: ToolResultEvent["usage"];
|
|
134
|
+
/** Fresh outcomes retained for model-invisible transcript presentation. */
|
|
135
|
+
readonly outcomes: readonly PostEditDiagnosticOutcome[];
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
type ExtractedMutation = {
|
|
139
|
+
readonly paths: readonly PostEditDiagnosticPath[];
|
|
140
|
+
readonly warnings: readonly string[];
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
function mutationResult(details: ApplyPatchDetails) {
|
|
144
|
+
return {
|
|
145
|
+
changedPaths: [
|
|
146
|
+
...details.result.changedFiles,
|
|
147
|
+
...details.result.createdFiles,
|
|
148
|
+
...details.result.movedFiles.map(({ to }) => to),
|
|
149
|
+
],
|
|
150
|
+
deletedPaths: details.result.deletedFiles,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function manifestDestinationPaths(manifest: Static<typeof MutationManifestSchema>): string[] {
|
|
155
|
+
const paths: string[] = [];
|
|
156
|
+
for (const entry of manifest) {
|
|
157
|
+
if (entry.operation === "delete") continue;
|
|
158
|
+
if (entry.operation === "rename") {
|
|
159
|
+
paths.push(entry.destination_path);
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
paths.push(entry.path);
|
|
163
|
+
}
|
|
164
|
+
return paths;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function pathsAfterMutation(result: ReturnType<typeof mutationResult>): PostEditDiagnosticPath[] {
|
|
168
|
+
const deletedPaths = new Set(result.deletedPaths);
|
|
169
|
+
return [...new Set(result.changedPaths)]
|
|
170
|
+
.filter((path) => !deletedPaths.has(path))
|
|
171
|
+
.sort((left, right) => left.localeCompare(right))
|
|
172
|
+
.map((path) => ({ path }));
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Extract exact changed destination paths from one Supported Mutation Tool result. */
|
|
176
|
+
export function extractPostEditDiagnosticPaths(
|
|
177
|
+
event: Pick<PostEditToolResult, "toolName" | "input" | "details" | "isError">,
|
|
178
|
+
): ExtractedMutation | undefined {
|
|
179
|
+
if (event.toolName === "edit" || event.toolName === "write") {
|
|
180
|
+
if (event.isError || !Value.Check(NativeMutationInputSchema, event.input)) return undefined;
|
|
181
|
+
return { paths: [{ path: event.input.path }], warnings: [] };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (event.toolName === "apply_patch") {
|
|
185
|
+
if (!Value.Check(ApplyPatchDetailsSchema, event.details)) {
|
|
186
|
+
return {
|
|
187
|
+
paths: [],
|
|
188
|
+
warnings: [
|
|
189
|
+
"Pi LSP: apply_patch diagnostics adapter skipped an unknown Codex result shape.",
|
|
190
|
+
],
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
return { paths: pathsAfterMutation(mutationResult(event.details)), warnings: [] };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (event.toolName === "lsp" && event.input.operation === "apply") {
|
|
197
|
+
if (
|
|
198
|
+
!Value.Check(MutationManifestSchema, event.input.mutation_manifest) ||
|
|
199
|
+
!Value.Check(WorkspaceEditApplyDetailsSchema, event.details)
|
|
200
|
+
) {
|
|
201
|
+
return undefined;
|
|
202
|
+
}
|
|
203
|
+
const verifiedManifestPaths = manifestDestinationPaths(event.input.mutation_manifest);
|
|
204
|
+
const actualPaths = new Set(event.details.changed_paths);
|
|
205
|
+
return {
|
|
206
|
+
paths: verifiedManifestPaths
|
|
207
|
+
.filter((path) => actualPaths.has(path))
|
|
208
|
+
.sort((left, right) => left.localeCompare(right))
|
|
209
|
+
.map((path) => ({ path })),
|
|
210
|
+
warnings: [],
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return undefined;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function formatOutcome(outcome: PostEditDiagnosticOutcome): string {
|
|
218
|
+
switch (outcome.kind) {
|
|
219
|
+
case "diagnostic": {
|
|
220
|
+
const diagnostic = outcome.diagnostic;
|
|
221
|
+
return `${diagnostic.path}:${diagnostic.line}:${diagnostic.character} [${diagnostic.serverId}] severity ${diagnostic.severity}: ${diagnostic.message}`;
|
|
222
|
+
}
|
|
223
|
+
case "no_diagnostics":
|
|
224
|
+
return `${outcome.path}: no diagnostics`;
|
|
225
|
+
case "no_configured_server":
|
|
226
|
+
return `${outcome.path}: no configured server`;
|
|
227
|
+
case "timeout":
|
|
228
|
+
return `${outcome.path}: diagnostics timeout${outcome.serverId === undefined ? "" : ` (${outcome.serverId})`}`;
|
|
229
|
+
case "unavailable_server":
|
|
230
|
+
return `${outcome.path}: unavailable server${outcome.serverId === undefined ? "" : ` (${outcome.serverId})`}`;
|
|
231
|
+
case "warning":
|
|
232
|
+
return outcome.message;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function compareOutcomes(
|
|
237
|
+
left: PostEditDiagnosticOutcome,
|
|
238
|
+
right: PostEditDiagnosticOutcome,
|
|
239
|
+
): number {
|
|
240
|
+
if (left.kind === "diagnostic" && right.kind === "diagnostic") {
|
|
241
|
+
const leftDiagnostic = left.diagnostic;
|
|
242
|
+
const rightDiagnostic = right.diagnostic;
|
|
243
|
+
return (
|
|
244
|
+
leftDiagnostic.severity - rightDiagnostic.severity ||
|
|
245
|
+
leftDiagnostic.path.localeCompare(rightDiagnostic.path) ||
|
|
246
|
+
leftDiagnostic.line - rightDiagnostic.line ||
|
|
247
|
+
leftDiagnostic.character - rightDiagnostic.character ||
|
|
248
|
+
leftDiagnostic.serverId.localeCompare(rightDiagnostic.serverId)
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
if (left.kind === "diagnostic") return -1;
|
|
252
|
+
if (right.kind === "diagnostic") return 1;
|
|
253
|
+
return formatOutcome(left).localeCompare(formatOutcome(right));
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** Render one compact deterministic LSP section without deduplicating independent server diagnostics. */
|
|
257
|
+
export function formatPostEditDiagnostics(outcomes: readonly PostEditDiagnosticOutcome[]): string {
|
|
258
|
+
const lines = [...outcomes].sort(compareOutcomes).map(formatOutcome);
|
|
259
|
+
return `\n\nLSP diagnostics\n${lines.length === 0 ? "no diagnostics" : lines.join("\n")}`;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** Append fresh Post-edit Diagnostics while preserving every mutation-result field Pi already owns. */
|
|
263
|
+
export async function appendPostEditDiagnostics(
|
|
264
|
+
event: PostEditToolResult,
|
|
265
|
+
diagnostics: PostEditDiagnosticsRunner,
|
|
266
|
+
): Promise<PostEditDiagnosticsResultPatch | undefined> {
|
|
267
|
+
const extracted = extractPostEditDiagnosticPaths(event);
|
|
268
|
+
if (extracted === undefined) return undefined;
|
|
269
|
+
const outcomes = [
|
|
270
|
+
...extracted.warnings.map((message): PostEditDiagnosticOutcome => ({
|
|
271
|
+
kind: "warning",
|
|
272
|
+
message,
|
|
273
|
+
})),
|
|
274
|
+
...(await diagnostics.runPostEditDiagnostics(extracted.paths)),
|
|
275
|
+
];
|
|
276
|
+
const patch: PostEditDiagnosticsResultPatch = {
|
|
277
|
+
content: [...event.content, { type: "text", text: formatPostEditDiagnostics(outcomes) }],
|
|
278
|
+
details: event.details,
|
|
279
|
+
isError: event.isError,
|
|
280
|
+
outcomes,
|
|
281
|
+
};
|
|
282
|
+
return event.usage === undefined ? patch : { ...patch, usage: event.usage };
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/** Adapt Pi's central ToolResultEvent shape to Post-edit Diagnostics middleware. */
|
|
286
|
+
export async function appendPiPostEditDiagnostics(
|
|
287
|
+
event: ToolResultEvent,
|
|
288
|
+
diagnostics: PostEditDiagnosticsRunner,
|
|
289
|
+
): Promise<PostEditDiagnosticsResultPatch | undefined> {
|
|
290
|
+
return appendPostEditDiagnostics(event, diagnostics);
|
|
291
|
+
}
|