@oh-my-pi/pi-coding-agent 13.3.7 → 13.3.8

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 (49) hide show
  1. package/CHANGELOG.md +82 -0
  2. package/package.json +9 -18
  3. package/scripts/format-prompts.ts +7 -172
  4. package/src/config/prompt-templates.ts +2 -54
  5. package/src/config/settings-schema.ts +24 -0
  6. package/src/discovery/codex.ts +1 -2
  7. package/src/discovery/helpers.ts +0 -5
  8. package/src/lsp/client.ts +8 -0
  9. package/src/lsp/config.ts +2 -3
  10. package/src/lsp/index.ts +379 -99
  11. package/src/lsp/render.ts +21 -31
  12. package/src/lsp/types.ts +21 -8
  13. package/src/lsp/utils.ts +193 -1
  14. package/src/mcp/config-writer.ts +3 -0
  15. package/src/modes/components/settings-defs.ts +9 -0
  16. package/src/modes/interactive-mode.ts +8 -1
  17. package/src/modes/theme/mermaid-cache.ts +4 -4
  18. package/src/modes/theme/theme.ts +33 -0
  19. package/src/prompts/system/subagent-user-prompt.md +2 -0
  20. package/src/prompts/system/system-prompt.md +12 -1
  21. package/src/prompts/tools/ast-find.md +20 -0
  22. package/src/prompts/tools/ast-replace.md +21 -0
  23. package/src/prompts/tools/bash.md +2 -0
  24. package/src/prompts/tools/hashline.md +26 -8
  25. package/src/prompts/tools/lsp.md +22 -5
  26. package/src/sdk.ts +11 -1
  27. package/src/session/agent-session.ts +261 -82
  28. package/src/task/executor.ts +8 -5
  29. package/src/tools/ast-find.ts +316 -0
  30. package/src/tools/ast-replace.ts +294 -0
  31. package/src/tools/bash.ts +2 -1
  32. package/src/tools/browser.ts +2 -8
  33. package/src/tools/fetch.ts +55 -18
  34. package/src/tools/index.ts +8 -0
  35. package/src/tools/path-utils.ts +34 -0
  36. package/src/tools/python.ts +2 -1
  37. package/src/tools/renderers.ts +4 -0
  38. package/src/tools/ssh.ts +2 -1
  39. package/src/tools/todo-write.ts +34 -0
  40. package/src/tools/tool-timeouts.ts +29 -0
  41. package/src/utils/mime.ts +37 -14
  42. package/src/utils/prompt-format.ts +172 -0
  43. package/src/web/scrapers/arxiv.ts +12 -12
  44. package/src/web/scrapers/go-pkg.ts +2 -2
  45. package/src/web/scrapers/iacr.ts +17 -9
  46. package/src/web/scrapers/readthedocs.ts +3 -3
  47. package/src/web/scrapers/twitter.ts +11 -11
  48. package/src/web/scrapers/wikipedia.ts +4 -5
  49. package/src/utils/ignore-files.ts +0 -119
package/src/lsp/index.ts CHANGED
@@ -9,6 +9,7 @@ import lspDescription from "../prompts/tools/lsp.md" with { type: "text" };
9
9
  import type { ToolSession } from "../tools";
10
10
  import { resolveToCwd } from "../tools/path-utils";
11
11
  import { ToolAbortError, throwIfAborted } from "../tools/tool-errors";
12
+ import { clampTimeout } from "../tools/tool-timeouts";
12
13
  import {
13
14
  ensureFileOpen,
14
15
  getActiveClients,
@@ -27,6 +28,9 @@ import { applyTextEditsToString, applyWorkspaceEdit } from "./edits";
27
28
  import { detectLspmux } from "./lspmux";
28
29
  import { renderCall, renderResult } from "./render";
29
30
  import {
31
+ type CodeAction,
32
+ type CodeActionContext,
33
+ type Command,
30
34
  type Diagnostic,
31
35
  type DocumentSymbol,
32
36
  type Hover,
@@ -42,16 +46,25 @@ import {
42
46
  type WorkspaceEdit,
43
47
  } from "./types";
44
48
  import {
49
+ applyCodeAction,
50
+ collectGlobMatches,
51
+ dedupeWorkspaceSymbols,
45
52
  extractHoverText,
46
53
  fileToUri,
54
+ filterWorkspaceSymbols,
55
+ formatCodeAction,
47
56
  formatDiagnostic,
48
57
  formatDiagnosticsSummary,
49
58
  formatDocumentSymbol,
50
59
  formatLocation,
51
60
  formatSymbolInformation,
52
61
  formatWorkspaceEdit,
62
+ hasGlobPattern,
63
+ readLocationContext,
64
+ resolveSymbolColumn,
53
65
  sortDiagnostics,
54
66
  symbolKindToIcon,
67
+ uriToFile,
55
68
  } from "./utils";
56
69
 
57
70
  export type { LspServerStatus } from "./client";
@@ -238,6 +251,10 @@ function getLspServerForFile(config: LspConfig, filePath: string): [string, Serv
238
251
  }
239
252
 
240
253
  const DIAGNOSTIC_MESSAGE_LIMIT = 50;
254
+ const SINGLE_DIAGNOSTICS_WAIT_TIMEOUT_MS = 3000;
255
+ const BATCH_DIAGNOSTICS_WAIT_TIMEOUT_MS = 400;
256
+ const MAX_GLOB_DIAGNOSTIC_TARGETS = 20;
257
+ const WORKSPACE_SYMBOL_LIMIT = 200;
241
258
 
242
259
  function limitDiagnosticMessages(messages: string[]): string[] {
243
260
  if (messages.length <= DIAGNOSTIC_MESSAGE_LIMIT) {
@@ -246,15 +263,52 @@ function limitDiagnosticMessages(messages: string[]): string[] {
246
263
  return messages.slice(0, DIAGNOSTIC_MESSAGE_LIMIT);
247
264
  }
248
265
 
249
- function getServerForWorkspaceAction(config: LspConfig, action: string): [string, ServerConfig] | null {
250
- const entries = getLspServers(config);
251
- if (entries.length === 0) return null;
266
+ const LOCATION_CONTEXT_LINES = 1;
267
+ const REFERENCE_CONTEXT_LIMIT = 50;
252
268
 
253
- if (action === "symbols" || action === "reload") {
254
- return entries[0];
255
- }
269
+ function normalizeLocationResult(result: Location | Location[] | LocationLink | LocationLink[] | null): Location[] {
270
+ if (!result) return [];
271
+ const raw = Array.isArray(result) ? result : [result];
272
+ return raw.flatMap(loc => {
273
+ if ("uri" in loc) {
274
+ return [loc as Location];
275
+ }
276
+ if ("targetUri" in loc) {
277
+ const link = loc as LocationLink;
278
+ return [{ uri: link.targetUri, range: link.targetSelectionRange ?? link.targetRange }];
279
+ }
280
+ return [];
281
+ });
282
+ }
256
283
 
257
- return null;
284
+ async function formatLocationWithContext(location: Location, cwd: string): Promise<string> {
285
+ const header = ` ${formatLocation(location, cwd)}`;
286
+ const context = await readLocationContext(
287
+ uriToFile(location.uri),
288
+ location.range.start.line + 1,
289
+ LOCATION_CONTEXT_LINES,
290
+ );
291
+ if (context.length === 0) {
292
+ return header;
293
+ }
294
+ return `${header}\n${context.map(lineText => ` ${lineText}`).join("\n")}`;
295
+ }
296
+ async function reloadServer(client: LspClient, serverName: string, signal?: AbortSignal): Promise<string> {
297
+ let output = `Restarted ${serverName}`;
298
+ const reloadMethods = ["rust-analyzer/reloadWorkspace", "workspace/didChangeConfiguration"];
299
+ for (const method of reloadMethods) {
300
+ try {
301
+ await sendRequest(client, method, method.includes("Configuration") ? { settings: {} } : null, signal);
302
+ output = `Reloaded ${serverName}`;
303
+ break;
304
+ } catch {
305
+ // Method not supported, try next
306
+ }
307
+ }
308
+ if (output.startsWith("Restarted")) {
309
+ client.proc.kill();
310
+ }
311
+ return output;
258
312
  }
259
313
 
260
314
  async function waitForDiagnostics(
@@ -887,7 +941,10 @@ export class LspTool implements AgentTool<typeof lspSchema, LspToolDetails, Them
887
941
  _onUpdate?: AgentToolUpdateCallback<LspToolDetails>,
888
942
  _context?: AgentToolContext,
889
943
  ): Promise<AgentToolResult<LspToolDetails>> {
890
- const { action, file, files, line, column, query, new_name, apply, include_declaration } = params;
944
+ const { action, file, line, symbol, occurrence, query, new_name, apply, timeout } = params;
945
+ const timeoutSec = clampTimeout("lsp", timeout);
946
+ const timeoutSignal = AbortSignal.timeout(timeoutSec * 1000);
947
+ signal = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
891
948
  throwIfAborted(signal);
892
949
 
893
950
  const config = getConfig(this.session.cwd);
@@ -916,8 +973,7 @@ export class LspTool implements AgentTool<typeof lspSchema, LspToolDetails, Them
916
973
 
917
974
  // Diagnostics can be batch or single-file - queries all applicable servers
918
975
  if (action === "diagnostics") {
919
- const targets = files?.length ? files : file ? [file] : null;
920
- if (!targets) {
976
+ if (!file) {
921
977
  // No file specified - run workspace diagnostics
922
978
  const result = await runWorkspaceDiagnostics(this.session.cwd, signal);
923
979
  return {
@@ -931,9 +987,34 @@ export class LspTool implements AgentTool<typeof lspSchema, LspToolDetails, Them
931
987
  };
932
988
  }
933
989
 
934
- const detailed = Boolean(files?.length);
990
+ let targets: string[];
991
+ let truncatedGlobTargets = false;
992
+ if (hasGlobPattern(file)) {
993
+ const globMatches = await collectGlobMatches(file, this.session.cwd, MAX_GLOB_DIAGNOSTIC_TARGETS);
994
+ targets = globMatches.matches;
995
+ truncatedGlobTargets = globMatches.truncated;
996
+ } else {
997
+ targets = [file];
998
+ }
999
+
1000
+ if (targets.length === 0) {
1001
+ return {
1002
+ content: [{ type: "text", text: `No files matched pattern: ${file}` }],
1003
+ details: { action, success: true, request: params },
1004
+ };
1005
+ }
1006
+
1007
+ const detailed = targets.length > 1 || truncatedGlobTargets;
1008
+ const diagnosticsWaitTimeoutMs = detailed
1009
+ ? Math.min(BATCH_DIAGNOSTICS_WAIT_TIMEOUT_MS, timeoutSec * 1000)
1010
+ : Math.min(SINGLE_DIAGNOSTICS_WAIT_TIMEOUT_MS, timeoutSec * 1000);
935
1011
  const results: string[] = [];
936
1012
  const allServerNames = new Set<string>();
1013
+ if (truncatedGlobTargets) {
1014
+ results.push(
1015
+ `${theme.status.warning} Pattern matched more than ${MAX_GLOB_DIAGNOSTIC_TARGETS} files; showing first ${MAX_GLOB_DIAGNOSTIC_TARGETS}. Narrow the glob or use workspace diagnostics.`,
1016
+ );
1017
+ }
937
1018
 
938
1019
  for (const target of targets) {
939
1020
  throwIfAborted(signal);
@@ -962,7 +1043,13 @@ export class LspTool implements AgentTool<typeof lspSchema, LspToolDetails, Them
962
1043
  const client = await getOrCreateClient(serverConfig, this.session.cwd);
963
1044
  const minVersion = client.diagnosticsVersion;
964
1045
  await refreshFile(client, resolved, signal);
965
- const diagnostics = await waitForDiagnostics(client, uri, 3000, signal, minVersion);
1046
+ const diagnostics = await waitForDiagnostics(
1047
+ client,
1048
+ uri,
1049
+ diagnosticsWaitTimeoutMs,
1050
+ signal,
1051
+ minVersion,
1052
+ );
966
1053
  allDiagnostics.push(...diagnostics);
967
1054
  } catch (err) {
968
1055
  if (err instanceof ToolAbortError || signal?.aborted) {
@@ -1029,10 +1116,107 @@ export class LspTool implements AgentTool<typeof lspSchema, LspToolDetails, Them
1029
1116
  }
1030
1117
 
1031
1118
  const resolvedFile = file ? resolveToCwd(file, this.session.cwd) : null;
1032
- const serverInfo = resolvedFile
1033
- ? getLspServerForFile(config, resolvedFile)
1034
- : getServerForWorkspaceAction(config, action);
1119
+ if (action === "symbols" && !resolvedFile) {
1120
+ const normalizedQuery = query?.trim();
1121
+ if (!normalizedQuery) {
1122
+ return {
1123
+ content: [{ type: "text", text: "Error: query parameter required for workspace symbol search" }],
1124
+ details: { action, success: false, request: params },
1125
+ };
1126
+ }
1127
+ const servers = getLspServers(config);
1128
+ if (servers.length === 0) {
1129
+ return {
1130
+ content: [{ type: "text", text: "No language server found for this action" }],
1131
+ details: { action, success: false, request: params },
1132
+ };
1133
+ }
1134
+ const aggregatedSymbols: SymbolInformation[] = [];
1135
+ const respondingServers = new Set<string>();
1136
+ for (const [workspaceServerName, workspaceServerConfig] of servers) {
1137
+ throwIfAborted(signal);
1138
+ try {
1139
+ const workspaceClient = await getOrCreateClient(workspaceServerConfig, this.session.cwd);
1140
+ const workspaceResult = (await sendRequest(
1141
+ workspaceClient,
1142
+ "workspace/symbol",
1143
+ { query: normalizedQuery },
1144
+ signal,
1145
+ )) as SymbolInformation[] | null;
1146
+ if (!workspaceResult || workspaceResult.length === 0) {
1147
+ continue;
1148
+ }
1149
+ respondingServers.add(workspaceServerName);
1150
+ aggregatedSymbols.push(...filterWorkspaceSymbols(workspaceResult, normalizedQuery));
1151
+ } catch (err) {
1152
+ if (err instanceof ToolAbortError || signal?.aborted) {
1153
+ throw err;
1154
+ }
1155
+ }
1156
+ }
1157
+ const dedupedSymbols = dedupeWorkspaceSymbols(aggregatedSymbols);
1158
+ if (dedupedSymbols.length === 0) {
1159
+ return {
1160
+ content: [{ type: "text", text: `No symbols matching "${normalizedQuery}"` }],
1161
+ details: {
1162
+ action,
1163
+ serverName: Array.from(respondingServers).join(", "),
1164
+ success: true,
1165
+ request: params,
1166
+ },
1167
+ };
1168
+ }
1169
+ const limitedSymbols = dedupedSymbols.slice(0, WORKSPACE_SYMBOL_LIMIT);
1170
+ const lines = limitedSymbols.map(s => formatSymbolInformation(s, this.session.cwd));
1171
+ const truncationLine =
1172
+ dedupedSymbols.length > WORKSPACE_SYMBOL_LIMIT
1173
+ ? `\n... ${dedupedSymbols.length - WORKSPACE_SYMBOL_LIMIT} additional symbol(s) omitted`
1174
+ : "";
1175
+ return {
1176
+ content: [
1177
+ {
1178
+ type: "text",
1179
+ text: `Found ${dedupedSymbols.length} symbol(s) matching "${normalizedQuery}":\n${lines.map(l => ` ${l}`).join("\n")}${truncationLine}`,
1180
+ },
1181
+ ],
1182
+ details: {
1183
+ action,
1184
+ serverName: Array.from(respondingServers).join(", "),
1185
+ success: true,
1186
+ request: params,
1187
+ },
1188
+ };
1189
+ }
1035
1190
 
1191
+ if (action === "reload" && !resolvedFile) {
1192
+ const servers = getLspServers(config);
1193
+ if (servers.length === 0) {
1194
+ return {
1195
+ content: [{ type: "text", text: "No language server found for this action" }],
1196
+ details: { action, success: false, request: params },
1197
+ };
1198
+ }
1199
+ const outputs: string[] = [];
1200
+ for (const [workspaceServerName, workspaceServerConfig] of servers) {
1201
+ throwIfAborted(signal);
1202
+ try {
1203
+ const workspaceClient = await getOrCreateClient(workspaceServerConfig, this.session.cwd);
1204
+ outputs.push(await reloadServer(workspaceClient, workspaceServerName, signal));
1205
+ } catch (err) {
1206
+ if (err instanceof ToolAbortError || signal?.aborted) {
1207
+ throw err;
1208
+ }
1209
+ const errorMessage = err instanceof Error ? err.message : String(err);
1210
+ outputs.push(`Failed to reload ${workspaceServerName}: ${errorMessage}`);
1211
+ }
1212
+ }
1213
+ return {
1214
+ content: [{ type: "text", text: outputs.join("\n") }],
1215
+ details: { action, serverName: servers.map(([name]) => name).join(", "), success: true, request: params },
1216
+ };
1217
+ }
1218
+
1219
+ const serverInfo = resolvedFile ? getLspServerForFile(config, resolvedFile) : null;
1036
1220
  if (!serverInfo) {
1037
1221
  return {
1038
1222
  content: [{ type: "text", text: "No language server found for this action" }],
@@ -1051,7 +1235,11 @@ export class LspTool implements AgentTool<typeof lspSchema, LspToolDetails, Them
1051
1235
  }
1052
1236
 
1053
1237
  const uri = targetFile ? fileToUri(targetFile) : "";
1054
- const position = { line: (line || 1) - 1, character: (column || 1) - 1 };
1238
+ const resolvedLine = line ?? 1;
1239
+ const resolvedCharacter = targetFile
1240
+ ? await resolveSymbolColumn(targetFile, resolvedLine, symbol, occurrence)
1241
+ : 0;
1242
+ const position = { line: resolvedLine - 1, character: resolvedCharacter };
1055
1243
 
1056
1244
  let output: string;
1057
1245
 
@@ -1071,33 +1259,66 @@ export class LspTool implements AgentTool<typeof lspSchema, LspToolDetails, Them
1071
1259
  signal,
1072
1260
  )) as Location | Location[] | LocationLink | LocationLink[] | null;
1073
1261
 
1074
- if (!result) {
1262
+ const locations = normalizeLocationResult(result);
1263
+
1264
+ if (locations.length === 0) {
1075
1265
  output = "No definition found";
1076
1266
  } else {
1077
- const raw = Array.isArray(result) ? result : [result];
1078
- const locations = raw.flatMap(loc => {
1079
- if ("uri" in loc) {
1080
- return [loc as Location];
1081
- }
1082
- if ("targetUri" in loc) {
1083
- // Use targetSelectionRange (the precise identifier range) with fallback to targetRange
1084
- const link = loc as LocationLink;
1085
- return [{ uri: link.targetUri, range: link.targetSelectionRange ?? link.targetRange }];
1086
- }
1087
- return [];
1088
- });
1267
+ const lines = await Promise.all(
1268
+ locations.map(location => formatLocationWithContext(location, this.session.cwd)),
1269
+ );
1270
+ output = `Found ${locations.length} definition(s):\n${lines.join("\n")}`;
1271
+ }
1272
+ break;
1273
+ }
1089
1274
 
1090
- if (locations.length === 0) {
1091
- output = "No definition found";
1092
- } else {
1093
- output = `Found ${locations.length} definition(s):\n${locations
1094
- .map(loc => ` ${formatLocation(loc, this.session.cwd)}`)
1095
- .join("\n")}`;
1096
- }
1275
+ case "type_definition": {
1276
+ const result = (await sendRequest(
1277
+ client,
1278
+ "textDocument/typeDefinition",
1279
+ {
1280
+ textDocument: { uri },
1281
+ position,
1282
+ },
1283
+ signal,
1284
+ )) as Location | Location[] | LocationLink | LocationLink[] | null;
1285
+
1286
+ const locations = normalizeLocationResult(result);
1287
+
1288
+ if (locations.length === 0) {
1289
+ output = "No type definition found";
1290
+ } else {
1291
+ const lines = await Promise.all(
1292
+ locations.map(location => formatLocationWithContext(location, this.session.cwd)),
1293
+ );
1294
+ output = `Found ${locations.length} type definition(s):\n${lines.join("\n")}`;
1097
1295
  }
1098
1296
  break;
1099
1297
  }
1100
1298
 
1299
+ case "implementation": {
1300
+ const result = (await sendRequest(
1301
+ client,
1302
+ "textDocument/implementation",
1303
+ {
1304
+ textDocument: { uri },
1305
+ position,
1306
+ },
1307
+ signal,
1308
+ )) as Location | Location[] | LocationLink | LocationLink[] | null;
1309
+
1310
+ const locations = normalizeLocationResult(result);
1311
+
1312
+ if (locations.length === 0) {
1313
+ output = "No implementation found";
1314
+ } else {
1315
+ const lines = await Promise.all(
1316
+ locations.map(location => formatLocationWithContext(location, this.session.cwd)),
1317
+ );
1318
+ output = `Found ${locations.length} implementation(s):\n${lines.join("\n")}`;
1319
+ }
1320
+ break;
1321
+ }
1101
1322
  case "references": {
1102
1323
  const result = (await sendRequest(
1103
1324
  client,
@@ -1105,7 +1326,7 @@ export class LspTool implements AgentTool<typeof lspSchema, LspToolDetails, Them
1105
1326
  {
1106
1327
  textDocument: { uri },
1107
1328
  position,
1108
- context: { includeDeclaration: include_declaration ?? true },
1329
+ context: { includeDeclaration: true },
1109
1330
  },
1110
1331
  signal,
1111
1332
  )) as Location[] | null;
@@ -1113,7 +1334,19 @@ export class LspTool implements AgentTool<typeof lspSchema, LspToolDetails, Them
1113
1334
  if (!result || result.length === 0) {
1114
1335
  output = "No references found";
1115
1336
  } else {
1116
- const lines = result.map(loc => ` ${formatLocation(loc, this.session.cwd)}`);
1337
+ const contextualReferences = result.slice(0, REFERENCE_CONTEXT_LIMIT);
1338
+ const plainReferences = result.slice(REFERENCE_CONTEXT_LIMIT);
1339
+ const contextualLines = await Promise.all(
1340
+ contextualReferences.map(location => formatLocationWithContext(location, this.session.cwd)),
1341
+ );
1342
+ const plainLines = plainReferences.map(location => ` ${formatLocation(location, this.session.cwd)}`);
1343
+ const lines = plainLines.length
1344
+ ? [
1345
+ ...contextualLines,
1346
+ ` ... ${plainLines.length} additional reference(s) shown without context`,
1347
+ ...plainLines,
1348
+ ]
1349
+ : contextualLines;
1117
1350
  output = `Found ${result.length} reference(s):\n${lines.join("\n")}`;
1118
1351
  }
1119
1352
  break;
@@ -1138,52 +1371,118 @@ export class LspTool implements AgentTool<typeof lspSchema, LspToolDetails, Them
1138
1371
  break;
1139
1372
  }
1140
1373
 
1141
- case "symbols": {
1142
- // If no file, do workspace symbol search (requires query)
1143
- if (!targetFile) {
1144
- if (!query) {
1145
- return {
1146
- content: [
1147
- { type: "text", text: "Error: query parameter required for workspace symbol search" },
1148
- ],
1149
- details: { action, serverName, success: false },
1150
- };
1374
+ case "code_actions": {
1375
+ const diagnostics = client.diagnostics.get(uri) ?? [];
1376
+ const context: CodeActionContext = {
1377
+ diagnostics,
1378
+ only: !apply && query ? [query] : undefined,
1379
+ triggerKind: 1,
1380
+ };
1381
+
1382
+ const result = (await sendRequest(
1383
+ client,
1384
+ "textDocument/codeAction",
1385
+ {
1386
+ textDocument: { uri },
1387
+ range: { start: position, end: position },
1388
+ context,
1389
+ },
1390
+ signal,
1391
+ )) as (CodeAction | Command)[] | null;
1392
+
1393
+ if (!result || result.length === 0) {
1394
+ output = "No code actions available";
1395
+ break;
1396
+ }
1397
+
1398
+ if (apply === true && query) {
1399
+ const normalizedQuery = query.trim();
1400
+ if (normalizedQuery.length === 0) {
1401
+ output = "Error: query parameter required when apply=true for code_actions";
1402
+ break;
1151
1403
  }
1152
- const result = (await sendRequest(client, "workspace/symbol", { query }, signal)) as
1153
- | SymbolInformation[]
1154
- | null;
1155
- if (!result || result.length === 0) {
1156
- output = `No symbols matching "${query}"`;
1157
- } else {
1158
- const lines = result.map(s => formatSymbolInformation(s, this.session.cwd));
1159
- output = `Found ${result.length} symbol(s) matching "${query}":\n${lines.map(l => ` ${l}`).join("\n")}`;
1404
+ const parsedIndex = /^\d+$/.test(normalizedQuery) ? Number.parseInt(normalizedQuery, 10) : null;
1405
+ const selectedAction = result.find(
1406
+ (actionItem, index) =>
1407
+ (parsedIndex !== null && index === parsedIndex) ||
1408
+ actionItem.title.toLowerCase().includes(normalizedQuery.toLowerCase()),
1409
+ );
1410
+
1411
+ if (!selectedAction) {
1412
+ const actionLines = result.map((actionItem, index) => ` ${formatCodeAction(actionItem, index)}`);
1413
+ output = `No code action matches "${normalizedQuery}". Available actions:\n${actionLines.join("\n")}`;
1414
+ break;
1160
1415
  }
1161
- } else {
1162
- // File-based document symbols
1163
- const result = (await sendRequest(
1164
- client,
1165
- "textDocument/documentSymbol",
1166
- {
1167
- textDocument: { uri },
1416
+
1417
+ const appliedAction = await applyCodeAction(selectedAction, {
1418
+ resolveCodeAction: async actionItem =>
1419
+ (await sendRequest(client, "codeAction/resolve", actionItem, signal)) as CodeAction,
1420
+ applyWorkspaceEdit: async edit => applyWorkspaceEdit(edit, this.session.cwd),
1421
+ executeCommand: async commandItem => {
1422
+ await sendRequest(
1423
+ client,
1424
+ "workspace/executeCommand",
1425
+ {
1426
+ command: commandItem.command,
1427
+ arguments: commandItem.arguments ?? [],
1428
+ },
1429
+ signal,
1430
+ );
1168
1431
  },
1169
- signal,
1170
- )) as (DocumentSymbol | SymbolInformation)[] | null;
1432
+ });
1171
1433
 
1172
- if (!result || result.length === 0) {
1173
- output = "No symbols found";
1434
+ if (!appliedAction) {
1435
+ output = `Action "${selectedAction.title}" has no workspace edit or command to apply`;
1436
+ break;
1437
+ }
1438
+
1439
+ const summaryLines: string[] = [];
1440
+ if (appliedAction.edits.length > 0) {
1441
+ summaryLines.push(" Workspace edit:");
1442
+ summaryLines.push(...appliedAction.edits.map(item => ` ${item}`));
1443
+ }
1444
+ if (appliedAction.executedCommands.length > 0) {
1445
+ summaryLines.push(" Executed command(s):");
1446
+ summaryLines.push(...appliedAction.executedCommands.map(commandName => ` ${commandName}`));
1447
+ }
1448
+
1449
+ output = `Applied "${appliedAction.title}":\n${summaryLines.join("\n")}`;
1450
+ break;
1451
+ }
1452
+
1453
+ const actionLines = result.map((actionItem, index) => ` ${formatCodeAction(actionItem, index)}`);
1454
+ output = `${result.length} code action(s):\n${actionLines.join("\n")}`;
1455
+ break;
1456
+ }
1457
+ case "symbols": {
1458
+ if (!targetFile) {
1459
+ output = "Error: file parameter required for document symbols";
1460
+ break;
1461
+ }
1462
+ // File-based document symbols
1463
+ const result = (await sendRequest(
1464
+ client,
1465
+ "textDocument/documentSymbol",
1466
+ {
1467
+ textDocument: { uri },
1468
+ },
1469
+ signal,
1470
+ )) as (DocumentSymbol | SymbolInformation)[] | null;
1471
+
1472
+ if (!result || result.length === 0) {
1473
+ output = "No symbols found";
1474
+ } else {
1475
+ const relPath = path.relative(this.session.cwd, targetFile);
1476
+ if ("selectionRange" in result[0]) {
1477
+ const lines = (result as DocumentSymbol[]).flatMap(s => formatDocumentSymbol(s));
1478
+ output = `Symbols in ${relPath}:\n${lines.join("\n")}`;
1174
1479
  } else {
1175
- const relPath = path.relative(this.session.cwd, targetFile);
1176
- if ("selectionRange" in result[0]) {
1177
- const lines = (result as DocumentSymbol[]).flatMap(s => formatDocumentSymbol(s));
1178
- output = `Symbols in ${relPath}:\n${lines.join("\n")}`;
1179
- } else {
1180
- const lines = (result as SymbolInformation[]).map(s => {
1181
- const line = s.location.range.start.line + 1;
1182
- const icon = symbolKindToIcon(s.kind);
1183
- return `${icon} ${s.name} @ line ${line}`;
1184
- });
1185
- output = `Symbols in ${relPath}:\n${lines.join("\n")}`;
1186
- }
1480
+ const lines = (result as SymbolInformation[]).map(s => {
1481
+ const line = s.location.range.start.line + 1;
1482
+ const icon = symbolKindToIcon(s.kind);
1483
+ return `${icon} ${s.name} @ line ${line}`;
1484
+ });
1485
+ output = `Symbols in ${relPath}:\n${lines.join("\n")}`;
1187
1486
  }
1188
1487
  }
1189
1488
  break;
@@ -1224,26 +1523,7 @@ export class LspTool implements AgentTool<typeof lspSchema, LspToolDetails, Them
1224
1523
  }
1225
1524
 
1226
1525
  case "reload": {
1227
- // Try graceful reload first, fall back to kill
1228
- output = `Restarted ${serverName}`;
1229
- const reloadMethods = ["rust-analyzer/reloadWorkspace", "workspace/didChangeConfiguration"];
1230
- for (const method of reloadMethods) {
1231
- try {
1232
- await sendRequest(
1233
- client,
1234
- method,
1235
- method.includes("Configuration") ? { settings: {} } : null,
1236
- signal,
1237
- );
1238
- output = `Reloaded ${serverName}`;
1239
- break;
1240
- } catch {
1241
- // Method not supported, try next
1242
- }
1243
- }
1244
- if (output.startsWith("Restarted")) {
1245
- client.proc.kill();
1246
- }
1526
+ output = await reloadServer(client, serverName, signal);
1247
1527
  break;
1248
1528
  }
1249
1529