@trim21/personal-pi-extensions 0.1.684 → 0.1.685

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.
@@ -18,8 +18,9 @@ import { Type } from "typebox";
18
18
  import type { Hover } from "vscode-languageserver-types";
19
19
 
20
20
  import { resolvePathArg } from "../path.js";
21
- import type { ToolBus } from "../tool-bus.js";
21
+ import { defineStructuredTool, type ToolBus } from "../tool-bus.js";
22
22
  import type { InspectLocation } from "./client.js";
23
+ import { lspHoverStructuredSchema, lspLocationsStructuredSchema } from "./inspect-schemas.js";
23
24
  import type { LspService } from "./lsp.js";
24
25
  import { type LspPosition, symbolCandidates } from "./rename.js";
25
26
 
@@ -29,12 +30,31 @@ const MAX_ENTRIES_PER_FILE = 10;
29
30
  const MAX_FILES_LISTED = 30;
30
31
  const MAX_SNIPPET_LENGTH = 200;
31
32
 
32
- /** 一次探测的产出:text 是消歧分组键,subtitle 供 pendant 摘要。 */
33
- interface InspectOutput {
33
+ /** 渲染产出:text 也是消歧分组键。 */
34
+ interface RenderedOutput {
34
35
  text: string;
35
36
  subtitle: string;
36
37
  }
37
38
 
39
+ /**
40
+ * 一次探测的产出:渲染文本 + 结构化结果里除 `text` 之外的字段(选中哪一组,就取那一组的
41
+ * payload)。泛型让每个工具自己决定载荷。
42
+ */
43
+ interface InspectOutput<TPayload> extends RenderedOutput {
44
+ payload: TPayload;
45
+ }
46
+
47
+ /** 位置载荷:服务层归一化后的绝对路径 + 1-based 行列号。 */
48
+ function toLocationPayload(
49
+ locations: readonly InspectLocation[],
50
+ ): { path: string; line: number; character: number }[] {
51
+ return locations.map((location) => ({
52
+ path: location.path,
53
+ line: location.line + 1,
54
+ character: location.character + 1,
55
+ }));
56
+ }
57
+
38
58
  function loadPrompt(fileName: string): string {
39
59
  return readFileSync(fileURLToPath(new URL(fileName, import.meta.url)), "utf8").trim();
40
60
  }
@@ -113,7 +133,7 @@ function toCoordinates(location: InspectLocation): string {
113
133
  async function formatDefinitionLocations(
114
134
  locations: InspectLocation[],
115
135
  cache: Map<string, string[]>,
116
- ): Promise<InspectOutput> {
136
+ ): Promise<RenderedOutput> {
117
137
  if (locations.length === 0) {
118
138
  return { text: "No definition found for this symbol.", subtitle: "0 definition(s)" };
119
139
  }
@@ -131,7 +151,7 @@ async function formatDefinitionLocations(
131
151
  async function formatReferenceLocations(
132
152
  locations: InspectLocation[],
133
153
  cache: Map<string, string[]>,
134
- ): Promise<InspectOutput> {
154
+ ): Promise<RenderedOutput> {
135
155
  if (locations.length === 0) {
136
156
  return { text: "No references found for this symbol.", subtitle: "0 reference(s)" };
137
157
  }
@@ -198,15 +218,15 @@ function formatHoverContents(contents: Hover["contents"]): string {
198
218
  * 行内同名候选逐个探测:输出一致即同一符号的多次出现;不一致报歧义并列出
199
219
  * 各候选的 1-based 列号(与 lsp-rename 的消歧行为一致)。
200
220
  */
201
- async function probeSymbolCandidates(options: {
221
+ async function probeSymbolCandidates<TPayload>(options: {
202
222
  content: string;
203
223
  filePath: string;
204
224
  line: number;
205
225
  symbol: string;
206
226
  character?: number;
207
227
  signal?: AbortSignal;
208
- probe: (position: LspPosition) => Promise<InspectOutput>;
209
- }): Promise<InspectOutput> {
228
+ probe: (position: LspPosition) => Promise<InspectOutput<TPayload>>;
229
+ }): Promise<InspectOutput<TPayload>> {
210
230
  const candidates = symbolCandidates(
211
231
  options.content,
212
232
  options.line - 1,
@@ -218,12 +238,12 @@ async function probeSymbolCandidates(options: {
218
238
  `Symbol "${options.symbol}" not found on line ${options.line} of ${options.filePath}. Read the file again and locate the symbol.`,
219
239
  );
220
240
  }
221
- const outputs: InspectOutput[] = [];
241
+ const outputs: InspectOutput<TPayload>[] = [];
222
242
  for (const candidate of candidates) {
223
243
  options.signal?.throwIfAborted();
224
244
  outputs.push(await options.probe(candidate));
225
245
  }
226
- const groups = new Map<string, { output: InspectOutput; candidates: LspPosition[] }>();
246
+ const groups = new Map<string, { output: InspectOutput<TPayload>; candidates: LspPosition[] }>();
227
247
  for (const [index, candidate] of candidates.entries()) {
228
248
  const output = outputs.at(index);
229
249
  if (!output) {
@@ -256,137 +276,168 @@ async function probeSymbolCandidates(options: {
256
276
  }
257
277
 
258
278
  export function registerLspInspectTools(bus: ToolBus, service: LspService): void {
259
- bus.register({
260
- name: "lsp-find-definition",
261
- label: "Lsp Find Definition",
262
- description:
263
- "Find where a code symbol is defined via LSP. Returns 1-based path:line:col locations with source line snippets.",
264
- promptSnippet: "Find symbol definitions via LSP",
265
- promptGuidelines: [FIND_DEFINITION_PROMPT],
266
- parameters: POSITION_SCHEMA,
267
- async execute(_id, params, signal, _onUpdate, ctx) {
268
- signal?.throwIfAborted();
269
- const { path: filePath, content } = await readSymbolFile(ctx.cwd, params.file_path);
270
- const cache = new Map<string, string[]>();
271
- const output = await probeSymbolCandidates({
272
- content,
273
- filePath,
274
- line: params.line,
275
- symbol: params.symbol,
276
- character: params.character,
277
- signal,
278
- probe: async (position) => {
279
- const result = await service.inspect({
280
- file: filePath,
281
- cwd: ctx.cwd,
282
- line: position.line,
283
- character: position.character,
284
- query: "definition",
285
- options: { signal },
286
- });
287
- return formatDefinitionLocations(result.locations, cache);
288
- },
289
- });
290
- return {
291
- content: [{ type: "text" as const, text: output.text }],
292
- details: {
293
- pendant: {
294
- title: "lsp-find-definition",
295
- subtitle: `${params.symbol} · ${output.subtitle}`,
279
+ bus.register(
280
+ defineStructuredTool({
281
+ name: "lsp-find-definition",
282
+ label: "Lsp Find Definition",
283
+ description:
284
+ "Find where a code symbol is defined via LSP. Returns 1-based path:line:col locations with source line snippets.",
285
+ promptSnippet: "Find symbol definitions via LSP",
286
+ promptGuidelines: [FIND_DEFINITION_PROMPT],
287
+ parameters: POSITION_SCHEMA,
288
+ structuredSchema: lspLocationsStructuredSchema,
289
+ async execute(_id, params, signal, _onUpdate, ctx) {
290
+ signal?.throwIfAborted();
291
+ const { path: filePath, content } = await readSymbolFile(ctx.cwd, params.file_path);
292
+ const cache = new Map<string, string[]>();
293
+ const output = await probeSymbolCandidates({
294
+ content,
295
+ filePath,
296
+ line: params.line,
297
+ symbol: params.symbol,
298
+ character: params.character,
299
+ signal,
300
+ probe: async (position) => {
301
+ const result = await service.inspect({
302
+ file: filePath,
303
+ cwd: ctx.cwd,
304
+ line: position.line,
305
+ character: position.character,
306
+ query: "definition",
307
+ options: { signal },
308
+ });
309
+ const rendered = await formatDefinitionLocations(result.locations, cache);
310
+ return {
311
+ ...rendered,
312
+ payload: {
313
+ serverID: result.serverID,
314
+ locations: toLocationPayload(result.locations),
315
+ },
316
+ };
296
317
  },
297
- },
298
- };
299
- },
300
- });
318
+ });
319
+ return {
320
+ content: [{ type: "text" as const, text: output.text }],
321
+ details: {
322
+ pendant: {
323
+ title: "lsp-find-definition",
324
+ subtitle: `${params.symbol} · ${output.subtitle}`,
325
+ },
326
+ },
327
+ structuredResult: { ok: true as const, value: { text: output.text, ...output.payload } },
328
+ };
329
+ },
330
+ }),
331
+ );
301
332
 
302
- bus.register({
303
- name: "lsp-find-reference",
304
- label: "Lsp Find Reference",
305
- description:
306
- "Find all references to a code symbol across the workspace via LSP (includes the declaration). Grouped by file with 1-based line:col and source snippets.",
307
- promptSnippet: "Find symbol references via LSP",
308
- promptGuidelines: [FIND_REFERENCE_PROMPT],
309
- parameters: POSITION_SCHEMA,
310
- async execute(_id, params, signal, _onUpdate, ctx) {
311
- signal?.throwIfAborted();
312
- const { path: filePath, content } = await readSymbolFile(ctx.cwd, params.file_path);
313
- const cache = new Map<string, string[]>();
314
- const output = await probeSymbolCandidates({
315
- content,
316
- filePath,
317
- line: params.line,
318
- symbol: params.symbol,
319
- character: params.character,
320
- signal,
321
- probe: async (position) => {
322
- const result = await service.inspect({
323
- file: filePath,
324
- cwd: ctx.cwd,
325
- line: position.line,
326
- character: position.character,
327
- query: "references",
328
- options: { signal },
329
- });
330
- return formatReferenceLocations(result.locations, cache);
331
- },
332
- });
333
- return {
334
- content: [{ type: "text" as const, text: output.text }],
335
- details: {
336
- pendant: {
337
- title: "lsp-find-reference",
338
- subtitle: `${params.symbol} · ${output.subtitle}`,
333
+ bus.register(
334
+ defineStructuredTool({
335
+ name: "lsp-find-reference",
336
+ label: "Lsp Find Reference",
337
+ description:
338
+ "Find all references to a code symbol across the workspace via LSP (includes the declaration). Grouped by file with 1-based line:col and source snippets.",
339
+ promptSnippet: "Find symbol references via LSP",
340
+ promptGuidelines: [FIND_REFERENCE_PROMPT],
341
+ parameters: POSITION_SCHEMA,
342
+ structuredSchema: lspLocationsStructuredSchema,
343
+ async execute(_id, params, signal, _onUpdate, ctx) {
344
+ signal?.throwIfAborted();
345
+ const { path: filePath, content } = await readSymbolFile(ctx.cwd, params.file_path);
346
+ const cache = new Map<string, string[]>();
347
+ const output = await probeSymbolCandidates({
348
+ content,
349
+ filePath,
350
+ line: params.line,
351
+ symbol: params.symbol,
352
+ character: params.character,
353
+ signal,
354
+ probe: async (position) => {
355
+ const result = await service.inspect({
356
+ file: filePath,
357
+ cwd: ctx.cwd,
358
+ line: position.line,
359
+ character: position.character,
360
+ query: "references",
361
+ options: { signal },
362
+ });
363
+ const rendered = await formatReferenceLocations(result.locations, cache);
364
+ return {
365
+ ...rendered,
366
+ // 文本为上下文做了截断(每文件 10 条、最多 30 个文件),载荷给全部位置
367
+ payload: {
368
+ serverID: result.serverID,
369
+ locations: toLocationPayload(result.locations),
370
+ },
371
+ };
339
372
  },
340
- },
341
- };
342
- },
343
- });
373
+ });
374
+ return {
375
+ content: [{ type: "text" as const, text: output.text }],
376
+ details: {
377
+ pendant: {
378
+ title: "lsp-find-reference",
379
+ subtitle: `${params.symbol} · ${output.subtitle}`,
380
+ },
381
+ },
382
+ structuredResult: { ok: true as const, value: { text: output.text, ...output.payload } },
383
+ };
384
+ },
385
+ }),
386
+ );
344
387
 
345
- bus.register({
346
- name: "lsp-inspect",
347
- label: "Lsp Inspect",
348
- description:
349
- "Get hover information (type signature, documentation) for a code symbol via LSP. Content is passed through from the language server.",
350
- promptSnippet: "Get hover info for a symbol via LSP",
351
- promptGuidelines: [INSPECT_PROMPT],
352
- parameters: POSITION_SCHEMA,
353
- async execute(_id, params, signal, _onUpdate, ctx) {
354
- signal?.throwIfAborted();
355
- const { path: filePath, content } = await readSymbolFile(ctx.cwd, params.file_path);
356
- const output = await probeSymbolCandidates({
357
- content,
358
- filePath,
359
- line: params.line,
360
- symbol: params.symbol,
361
- character: params.character,
362
- signal,
363
- probe: async (position) => {
364
- const result = await service.inspect({
365
- file: filePath,
366
- cwd: ctx.cwd,
367
- line: position.line,
368
- character: position.character,
369
- query: "hover",
370
- options: { signal },
371
- });
372
- if (result.hover === null) {
388
+ bus.register(
389
+ defineStructuredTool({
390
+ name: "lsp-inspect",
391
+ label: "Lsp Inspect",
392
+ description:
393
+ "Get hover information (type signature, documentation) for a code symbol via LSP. Content is passed through from the language server.",
394
+ promptSnippet: "Get hover info for a symbol via LSP",
395
+ promptGuidelines: [INSPECT_PROMPT],
396
+ parameters: POSITION_SCHEMA,
397
+ structuredSchema: lspHoverStructuredSchema,
398
+ async execute(_id, params, signal, _onUpdate, ctx) {
399
+ signal?.throwIfAborted();
400
+ const { path: filePath, content } = await readSymbolFile(ctx.cwd, params.file_path);
401
+ const output = await probeSymbolCandidates({
402
+ content,
403
+ filePath,
404
+ line: params.line,
405
+ symbol: params.symbol,
406
+ character: params.character,
407
+ signal,
408
+ probe: async (position) => {
409
+ const result = await service.inspect({
410
+ file: filePath,
411
+ cwd: ctx.cwd,
412
+ line: position.line,
413
+ character: position.character,
414
+ query: "hover",
415
+ options: { signal },
416
+ });
417
+ // hover 的内容是服务器给的异构结构(string / MarkedString / MarkupContent),
418
+ // 渲染成文本给它;载荷里只另带是谁回答的
419
+ if (result.hover === null) {
420
+ return {
421
+ text: `No hover information for '${params.symbol}' at line ${params.line} of ${filePath}.`,
422
+ subtitle: "no hover info",
423
+ payload: { serverID: result.serverID },
424
+ };
425
+ }
373
426
  return {
374
- text: `No hover information for '${params.symbol}' at line ${params.line} of ${filePath}.`,
375
- subtitle: "no hover info",
427
+ text: formatHoverContents(result.hover.contents),
428
+ subtitle: "hover",
429
+ payload: { serverID: result.serverID },
376
430
  };
377
- }
378
- return {
379
- text: formatHoverContents(result.hover.contents),
380
- subtitle: "hover",
381
- };
382
- },
383
- });
384
- return {
385
- content: [{ type: "text" as const, text: output.text }],
386
- details: {
387
- pendant: { title: "lsp-inspect", subtitle: `${params.symbol} · ${output.subtitle}` },
388
- },
389
- };
390
- },
391
- });
431
+ },
432
+ });
433
+ return {
434
+ content: [{ type: "text" as const, text: output.text }],
435
+ details: {
436
+ pendant: { title: "lsp-inspect", subtitle: `${params.symbol} · ${output.subtitle}` },
437
+ },
438
+ structuredResult: { ok: true as const, value: { text: output.text, ...output.payload } },
439
+ };
440
+ },
441
+ }),
442
+ );
392
443
  }