@oh-my-pi/pi-coding-agent 6.2.0 → 6.7.67

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 (93) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/docs/sdk.md +1 -1
  3. package/package.json +5 -5
  4. package/scripts/generate-template.ts +6 -6
  5. package/src/cli/args.ts +3 -0
  6. package/src/core/agent-session.ts +39 -0
  7. package/src/core/bash-executor.ts +3 -3
  8. package/src/core/cursor/exec-bridge.ts +95 -88
  9. package/src/core/custom-commands/bundled/review/index.ts +142 -145
  10. package/src/core/custom-commands/bundled/wt/index.ts +68 -66
  11. package/src/core/custom-commands/loader.ts +4 -6
  12. package/src/core/custom-tools/index.ts +2 -2
  13. package/src/core/custom-tools/loader.ts +66 -61
  14. package/src/core/custom-tools/types.ts +4 -4
  15. package/src/core/custom-tools/wrapper.ts +61 -25
  16. package/src/core/event-bus.ts +19 -47
  17. package/src/core/extensions/index.ts +8 -4
  18. package/src/core/extensions/loader.ts +160 -120
  19. package/src/core/extensions/types.ts +4 -4
  20. package/src/core/extensions/wrapper.ts +149 -100
  21. package/src/core/hooks/index.ts +1 -1
  22. package/src/core/hooks/tool-wrapper.ts +96 -70
  23. package/src/core/hooks/types.ts +1 -2
  24. package/src/core/index.ts +1 -0
  25. package/src/core/mcp/index.ts +6 -2
  26. package/src/core/mcp/json-rpc.ts +88 -0
  27. package/src/core/mcp/loader.ts +22 -4
  28. package/src/core/mcp/manager.ts +202 -48
  29. package/src/core/mcp/tool-bridge.ts +143 -55
  30. package/src/core/mcp/tool-cache.ts +122 -0
  31. package/src/core/python-executor.ts +3 -9
  32. package/src/core/sdk.ts +33 -32
  33. package/src/core/session-manager.ts +30 -0
  34. package/src/core/settings-manager.ts +54 -1
  35. package/src/core/ssh/ssh-executor.ts +6 -84
  36. package/src/core/streaming-output.ts +107 -53
  37. package/src/core/tools/ask.ts +92 -93
  38. package/src/core/tools/bash.ts +103 -94
  39. package/src/core/tools/calculator.ts +41 -26
  40. package/src/core/tools/complete.ts +76 -66
  41. package/src/core/tools/context.ts +22 -24
  42. package/src/core/tools/exa/index.ts +1 -1
  43. package/src/core/tools/exa/mcp-client.ts +56 -101
  44. package/src/core/tools/find.ts +250 -253
  45. package/src/core/tools/git.ts +39 -33
  46. package/src/core/tools/grep.ts +440 -427
  47. package/src/core/tools/index.ts +63 -61
  48. package/src/core/tools/ls.ts +119 -114
  49. package/src/core/tools/lsp/clients/biome-client.ts +5 -7
  50. package/src/core/tools/lsp/clients/index.ts +4 -4
  51. package/src/core/tools/lsp/clients/lsp-linter-client.ts +5 -7
  52. package/src/core/tools/lsp/config.ts +2 -2
  53. package/src/core/tools/lsp/index.ts +604 -578
  54. package/src/core/tools/notebook.ts +121 -119
  55. package/src/core/tools/output.ts +163 -147
  56. package/src/core/tools/patch/applicator.ts +1100 -0
  57. package/src/core/tools/patch/diff.ts +362 -0
  58. package/src/core/tools/patch/fuzzy.ts +647 -0
  59. package/src/core/tools/patch/index.ts +430 -0
  60. package/src/core/tools/patch/normalize.ts +220 -0
  61. package/src/core/tools/patch/normative.ts +73 -0
  62. package/src/core/tools/patch/parser.ts +528 -0
  63. package/src/core/tools/patch/shared.ts +257 -0
  64. package/src/core/tools/patch/types.ts +244 -0
  65. package/src/core/tools/python.ts +139 -136
  66. package/src/core/tools/read.ts +239 -216
  67. package/src/core/tools/render-utils.ts +196 -77
  68. package/src/core/tools/renderers.ts +6 -2
  69. package/src/core/tools/ssh.ts +99 -80
  70. package/src/core/tools/task/executor.ts +11 -7
  71. package/src/core/tools/task/index.ts +352 -343
  72. package/src/core/tools/task/worker.ts +13 -23
  73. package/src/core/tools/todo-write.ts +74 -59
  74. package/src/core/tools/web-fetch.ts +54 -47
  75. package/src/core/tools/web-search/index.ts +27 -16
  76. package/src/core/tools/write.ts +108 -47
  77. package/src/core/ttsr.ts +106 -152
  78. package/src/core/voice.ts +49 -39
  79. package/src/index.ts +16 -12
  80. package/src/lib/worktree/index.ts +1 -9
  81. package/src/modes/interactive/components/diff.ts +15 -8
  82. package/src/modes/interactive/components/settings-defs.ts +42 -0
  83. package/src/modes/interactive/components/tool-execution.ts +46 -8
  84. package/src/modes/interactive/controllers/event-controller.ts +6 -19
  85. package/src/modes/interactive/controllers/input-controller.ts +1 -1
  86. package/src/modes/interactive/utils/ui-helpers.ts +5 -1
  87. package/src/modes/rpc/rpc-mode.ts +99 -81
  88. package/src/prompts/tools/patch.md +76 -0
  89. package/src/prompts/tools/read.md +1 -1
  90. package/src/prompts/tools/{edit.md → replace.md} +1 -0
  91. package/src/utils/shell.ts +0 -40
  92. package/src/core/tools/edit-diff.ts +0 -574
  93. package/src/core/tools/edit.ts +0 -345
@@ -1,7 +1,7 @@
1
1
  import type { Dirent } from "node:fs";
2
2
  import { existsSync, statSync } from "node:fs";
3
3
  import path from "node:path";
4
- import type { AgentTool } from "@oh-my-pi/pi-agent-core";
4
+ import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
5
5
  import type { BunFile } from "bun";
6
6
  import { type Theme, theme } from "../../../modes/interactive/theme/theme";
7
7
  import lspDescription from "../../../prompts/tools/lsp.md" with { type: "text" };
@@ -718,6 +718,19 @@ function getOrCreateWritethroughBatch(id: string, options: Required<Writethrough
718
718
  return batch;
719
719
  }
720
720
 
721
+ export async function flushLspWritethroughBatch(
722
+ id: string,
723
+ cwd: string,
724
+ signal?: AbortSignal,
725
+ ): Promise<FileDiagnosticsResult | undefined> {
726
+ const state = writethroughBatches.get(id);
727
+ if (!state) {
728
+ return undefined;
729
+ }
730
+ writethroughBatches.delete(id);
731
+ return flushWritethroughBatch(Array.from(state.entries.values()), cwd, state.options, signal);
732
+ }
733
+
721
734
  function summarizeDiagnosticMessages(messages: string[]): { summary: string; errored: boolean } {
722
735
  const counts = { error: 0, warning: 0, info: 0, hint: 0 };
723
736
  for (const message of messages) {
@@ -923,692 +936,705 @@ export function createLspWritethrough(cwd: string, options?: WritethroughOptions
923
936
  };
924
937
  }
925
938
 
926
- /** Create an LSP tool */
927
- export function createLspTool(session: ToolSession): AgentTool<typeof lspSchema, LspToolDetails, Theme> | null {
928
- if (session.enableLsp === false) {
929
- return null;
939
+ /**
940
+ * LSP tool for language server protocol operations.
941
+ */
942
+ export class LspTool implements AgentTool<typeof lspSchema, LspToolDetails, Theme> {
943
+ public readonly name = "lsp";
944
+ public readonly label = "LSP";
945
+ public readonly description: string;
946
+ public readonly parameters = lspSchema;
947
+ public readonly renderCall = renderCall;
948
+ public readonly renderResult = renderResult;
949
+
950
+ private readonly session: ToolSession;
951
+
952
+ constructor(session: ToolSession) {
953
+ this.session = session;
954
+ this.description = renderPromptTemplate(lspDescription);
930
955
  }
931
- return {
932
- name: "lsp",
933
- label: "LSP",
934
- description: renderPromptTemplate(lspDescription),
935
- parameters: lspSchema,
936
- renderCall,
937
- renderResult,
938
- execute: async (_toolCallId, params: LspParams, _signal) => {
939
- const {
940
- action,
941
- file,
942
- files,
943
- line,
944
- column,
945
- end_line,
946
- end_character,
947
- query,
948
- new_name,
949
- replacement,
950
- kind,
951
- apply,
952
- action_index,
953
- include_declaration,
954
- } = params;
955
-
956
- const config = await getConfig(session.cwd);
957
-
958
- // Status action doesn't need a file
959
- if (action === "status") {
960
- const servers = Object.keys(config.servers);
961
- const lspmuxState = await detectLspmux();
962
- const lspmuxStatus = lspmuxState.available
963
- ? lspmuxState.running
964
- ? "lspmux: active (multiplexing enabled)"
965
- : "lspmux: installed but server not running"
966
- : "";
967
-
968
- const serverStatus =
969
- servers.length > 0
970
- ? `Active language servers: ${servers.join(", ")}`
971
- : "No language servers configured for this project";
972
-
973
- const output = lspmuxStatus ? `${serverStatus}\n${lspmuxStatus}` : serverStatus;
974
- return {
975
- content: [{ type: "text", text: output }],
976
- details: { action, success: true },
977
- };
978
- }
979
956
 
980
- // Workspace diagnostics - check entire project
981
- if (action === "workspace_diagnostics") {
982
- const result = await runWorkspaceDiagnostics(session.cwd, config);
957
+ static createIf(session: ToolSession): LspTool | null {
958
+ return session.enableLsp === false ? null : new LspTool(session);
959
+ }
960
+
961
+ public async execute(
962
+ _toolCallId: string,
963
+ params: LspParams,
964
+ _signal?: AbortSignal,
965
+ _onUpdate?: AgentToolUpdateCallback<LspToolDetails>,
966
+ _context?: AgentToolContext,
967
+ ): Promise<AgentToolResult<LspToolDetails>> {
968
+ const {
969
+ action,
970
+ file,
971
+ files,
972
+ line,
973
+ column,
974
+ end_line,
975
+ end_character,
976
+ query,
977
+ new_name,
978
+ replacement,
979
+ kind,
980
+ apply,
981
+ action_index,
982
+ include_declaration,
983
+ } = params;
984
+
985
+ const config = await getConfig(this.session.cwd);
986
+
987
+ // Status action doesn't need a file
988
+ if (action === "status") {
989
+ const servers = Object.keys(config.servers);
990
+ const lspmuxState = await detectLspmux();
991
+ const lspmuxStatus = lspmuxState.available
992
+ ? lspmuxState.running
993
+ ? "lspmux: active (multiplexing enabled)"
994
+ : "lspmux: installed but server not running"
995
+ : "";
996
+
997
+ const serverStatus =
998
+ servers.length > 0
999
+ ? `Active language servers: ${servers.join(", ")}`
1000
+ : "No language servers configured for this project";
1001
+
1002
+ const output = lspmuxStatus ? `${serverStatus}\n${lspmuxStatus}` : serverStatus;
1003
+ return {
1004
+ content: [{ type: "text", text: output }],
1005
+ details: { action, success: true },
1006
+ };
1007
+ }
1008
+
1009
+ // Workspace diagnostics - check entire project
1010
+ if (action === "workspace_diagnostics") {
1011
+ const result = await runWorkspaceDiagnostics(this.session.cwd, config);
1012
+ return {
1013
+ content: [
1014
+ {
1015
+ type: "text",
1016
+ text: `Workspace diagnostics (${result.projectType.description}):\n${result.output}`,
1017
+ },
1018
+ ],
1019
+ details: { action, success: true },
1020
+ };
1021
+ }
1022
+
1023
+ // Diagnostics can be batch or single-file - queries all applicable servers
1024
+ if (action === "diagnostics") {
1025
+ const targets = files?.length ? files : file ? [file] : null;
1026
+ if (!targets) {
983
1027
  return {
984
- content: [
985
- {
986
- type: "text",
987
- text: `Workspace diagnostics (${result.projectType.description}):\n${result.output}`,
988
- },
989
- ],
990
- details: { action, success: true },
1028
+ content: [{ type: "text", text: "Error: file or files parameter required for diagnostics" }],
1029
+ details: { action, success: false },
991
1030
  };
992
1031
  }
993
1032
 
994
- // Diagnostics can be batch or single-file - queries all applicable servers
995
- if (action === "diagnostics") {
996
- const targets = files?.length ? files : file ? [file] : null;
997
- if (!targets) {
998
- return {
999
- content: [{ type: "text", text: "Error: file or files parameter required for diagnostics" }],
1000
- details: { action, success: false },
1001
- };
1002
- }
1033
+ const detailed = Boolean(files?.length);
1034
+ const results: string[] = [];
1035
+ const allServerNames = new Set<string>();
1003
1036
 
1004
- const detailed = Boolean(files?.length);
1005
- const results: string[] = [];
1006
- const allServerNames = new Set<string>();
1007
-
1008
- for (const target of targets) {
1009
- const resolved = resolveToCwd(target, session.cwd);
1010
- const servers = getServersForFile(config, resolved);
1011
- if (servers.length === 0) {
1012
- results.push(`${theme.status.error} ${target}: No language server found`);
1013
- continue;
1014
- }
1037
+ for (const target of targets) {
1038
+ const resolved = resolveToCwd(target, this.session.cwd);
1039
+ const servers = getServersForFile(config, resolved);
1040
+ if (servers.length === 0) {
1041
+ results.push(`${theme.status.error} ${target}: No language server found`);
1042
+ continue;
1043
+ }
1015
1044
 
1016
- const uri = fileToUri(resolved);
1017
- const relPath = path.relative(session.cwd, resolved);
1018
- const allDiagnostics: Diagnostic[] = [];
1019
-
1020
- // Query all applicable servers for this file
1021
- for (const [serverName, serverConfig] of servers) {
1022
- allServerNames.add(serverName);
1023
- try {
1024
- if (serverConfig.createClient) {
1025
- const linterClient = getLinterClient(serverName, serverConfig, session.cwd);
1026
- const diagnostics = await linterClient.lint(resolved);
1027
- allDiagnostics.push(...diagnostics);
1028
- continue;
1029
- }
1030
- const client = await getOrCreateClient(serverConfig, session.cwd);
1031
- const minVersion = client.diagnosticsVersion;
1032
- await refreshFile(client, resolved);
1033
- const diagnostics = await waitForDiagnostics(client, uri, 3000, undefined, minVersion);
1045
+ const uri = fileToUri(resolved);
1046
+ const relPath = path.relative(this.session.cwd, resolved);
1047
+ const allDiagnostics: Diagnostic[] = [];
1048
+
1049
+ // Query all applicable servers for this file
1050
+ for (const [serverName, serverConfig] of servers) {
1051
+ allServerNames.add(serverName);
1052
+ try {
1053
+ if (serverConfig.createClient) {
1054
+ const linterClient = getLinterClient(serverName, serverConfig, this.session.cwd);
1055
+ const diagnostics = await linterClient.lint(resolved);
1034
1056
  allDiagnostics.push(...diagnostics);
1035
- } catch {
1036
- // Server failed, continue with others
1057
+ continue;
1037
1058
  }
1059
+ const client = await getOrCreateClient(serverConfig, this.session.cwd);
1060
+ const minVersion = client.diagnosticsVersion;
1061
+ await refreshFile(client, resolved);
1062
+ const diagnostics = await waitForDiagnostics(client, uri, 3000, undefined, minVersion);
1063
+ allDiagnostics.push(...diagnostics);
1064
+ } catch {
1065
+ // Server failed, continue with others
1038
1066
  }
1067
+ }
1039
1068
 
1040
- // Deduplicate diagnostics
1041
- const seen = new Set<string>();
1042
- const uniqueDiagnostics: Diagnostic[] = [];
1043
- for (const d of allDiagnostics) {
1044
- const key = `${d.range.start.line}:${d.range.start.character}:${d.range.end.line}:${d.range.end.character}:${d.message}`;
1045
- if (!seen.has(key)) {
1046
- seen.add(key);
1047
- uniqueDiagnostics.push(d);
1048
- }
1069
+ // Deduplicate diagnostics
1070
+ const seen = new Set<string>();
1071
+ const uniqueDiagnostics: Diagnostic[] = [];
1072
+ for (const d of allDiagnostics) {
1073
+ const key = `${d.range.start.line}:${d.range.start.character}:${d.range.end.line}:${d.range.end.character}:${d.message}`;
1074
+ if (!seen.has(key)) {
1075
+ seen.add(key);
1076
+ uniqueDiagnostics.push(d);
1049
1077
  }
1078
+ }
1050
1079
 
1051
- if (!detailed && targets.length === 1) {
1052
- if (uniqueDiagnostics.length === 0) {
1053
- return {
1054
- content: [{ type: "text", text: "No diagnostics" }],
1055
- details: { action, serverName: Array.from(allServerNames).join(", "), success: true },
1056
- };
1057
- }
1058
-
1059
- const summary = formatDiagnosticsSummary(uniqueDiagnostics);
1060
- const formatted = uniqueDiagnostics.map((d) => formatDiagnostic(d, relPath));
1061
- const output = `${summary}:\n${formatted.map((f) => ` ${f}`).join("\n")}`;
1080
+ if (!detailed && targets.length === 1) {
1081
+ if (uniqueDiagnostics.length === 0) {
1062
1082
  return {
1063
- content: [{ type: "text", text: output }],
1083
+ content: [{ type: "text", text: "No diagnostics" }],
1064
1084
  details: { action, serverName: Array.from(allServerNames).join(", "), success: true },
1065
1085
  };
1066
1086
  }
1067
1087
 
1068
- if (uniqueDiagnostics.length === 0) {
1069
- results.push(`${theme.status.success} ${relPath}: no issues`);
1070
- } else {
1071
- const summary = formatDiagnosticsSummary(uniqueDiagnostics);
1072
- results.push(`${theme.status.error} ${relPath}: ${summary}`);
1073
- for (const diag of uniqueDiagnostics) {
1074
- results.push(` ${formatDiagnostic(diag, relPath)}`);
1075
- }
1076
- }
1088
+ const summary = formatDiagnosticsSummary(uniqueDiagnostics);
1089
+ const formatted = uniqueDiagnostics.map((d) => formatDiagnostic(d, relPath));
1090
+ const output = `${summary}:\n${formatted.map((f) => ` ${f}`).join("\n")}`;
1091
+ return {
1092
+ content: [{ type: "text", text: output }],
1093
+ details: { action, serverName: Array.from(allServerNames).join(", "), success: true },
1094
+ };
1077
1095
  }
1078
1096
 
1079
- return {
1080
- content: [{ type: "text", text: results.join("\n") }],
1081
- details: { action, serverName: Array.from(allServerNames).join(", "), success: true },
1082
- };
1097
+ if (uniqueDiagnostics.length === 0) {
1098
+ results.push(`${theme.status.success} ${relPath}: no issues`);
1099
+ } else {
1100
+ const summary = formatDiagnosticsSummary(uniqueDiagnostics);
1101
+ results.push(`${theme.status.error} ${relPath}: ${summary}`);
1102
+ for (const diag of uniqueDiagnostics) {
1103
+ results.push(` ${formatDiagnostic(diag, relPath)}`);
1104
+ }
1105
+ }
1083
1106
  }
1084
1107
 
1085
- const requiresFile =
1086
- !file &&
1087
- action !== "workspace_symbols" &&
1088
- action !== "flycheck" &&
1089
- action !== "ssr" &&
1090
- action !== "runnables" &&
1091
- action !== "reload_workspace";
1108
+ return {
1109
+ content: [{ type: "text", text: results.join("\n") }],
1110
+ details: { action, serverName: Array.from(allServerNames).join(", "), success: true },
1111
+ };
1112
+ }
1092
1113
 
1093
- if (requiresFile) {
1094
- return {
1095
- content: [{ type: "text", text: "Error: file parameter required for this action" }],
1096
- details: { action, success: false },
1097
- };
1098
- }
1114
+ const requiresFile =
1115
+ !file &&
1116
+ action !== "workspace_symbols" &&
1117
+ action !== "flycheck" &&
1118
+ action !== "ssr" &&
1119
+ action !== "runnables" &&
1120
+ action !== "reload_workspace";
1121
+
1122
+ if (requiresFile) {
1123
+ return {
1124
+ content: [{ type: "text", text: "Error: file parameter required for this action" }],
1125
+ details: { action, success: false },
1126
+ };
1127
+ }
1099
1128
 
1100
- const resolvedFile = file ? resolveToCwd(file, session.cwd) : null;
1101
- const serverInfo = resolvedFile
1102
- ? getLspServerForFile(config, resolvedFile)
1103
- : getServerForWorkspaceAction(config, action);
1129
+ const resolvedFile = file ? resolveToCwd(file, this.session.cwd) : null;
1130
+ const serverInfo = resolvedFile
1131
+ ? getLspServerForFile(config, resolvedFile)
1132
+ : getServerForWorkspaceAction(config, action);
1104
1133
 
1105
- if (!serverInfo) {
1106
- return {
1107
- content: [{ type: "text", text: "No language server found for this action" }],
1108
- details: { action, success: false },
1109
- };
1110
- }
1134
+ if (!serverInfo) {
1135
+ return {
1136
+ content: [{ type: "text", text: "No language server found for this action" }],
1137
+ details: { action, success: false },
1138
+ };
1139
+ }
1111
1140
 
1112
- const [serverName, serverConfig] = serverInfo;
1141
+ const [serverName, serverConfig] = serverInfo;
1113
1142
 
1114
- try {
1115
- const client = await getOrCreateClient(serverConfig, session.cwd);
1116
- let targetFile = resolvedFile;
1117
- if (action === "runnables" && !targetFile) {
1118
- targetFile = findFileForServer(session.cwd, serverConfig);
1119
- if (!targetFile) {
1120
- return {
1121
- content: [{ type: "text", text: "Error: no matching files found for runnables" }],
1122
- details: { action, serverName, success: false },
1123
- };
1124
- }
1143
+ try {
1144
+ const client = await getOrCreateClient(serverConfig, this.session.cwd);
1145
+ let targetFile = resolvedFile;
1146
+ if (action === "runnables" && !targetFile) {
1147
+ targetFile = findFileForServer(this.session.cwd, serverConfig);
1148
+ if (!targetFile) {
1149
+ return {
1150
+ content: [{ type: "text", text: "Error: no matching files found for runnables" }],
1151
+ details: { action, serverName, success: false },
1152
+ };
1125
1153
  }
1154
+ }
1126
1155
 
1127
- if (targetFile) {
1128
- await ensureFileOpen(client, targetFile);
1129
- }
1156
+ if (targetFile) {
1157
+ await ensureFileOpen(client, targetFile);
1158
+ }
1130
1159
 
1131
- const uri = targetFile ? fileToUri(targetFile) : "";
1132
- const position = { line: (line || 1) - 1, character: (column || 1) - 1 };
1160
+ const uri = targetFile ? fileToUri(targetFile) : "";
1161
+ const position = { line: (line || 1) - 1, character: (column || 1) - 1 };
1133
1162
 
1134
- let output: string;
1163
+ let output: string;
1135
1164
 
1136
- switch (action) {
1137
- // =====================================================================
1138
- // Standard LSP Operations
1139
- // =====================================================================
1165
+ switch (action) {
1166
+ // =====================================================================
1167
+ // Standard LSP Operations
1168
+ // =====================================================================
1140
1169
 
1141
- case "definition": {
1142
- const result = (await sendRequest(client, "textDocument/definition", {
1143
- textDocument: { uri },
1144
- position,
1145
- })) as Location | Location[] | LocationLink | LocationLink[] | null;
1170
+ case "definition": {
1171
+ const result = (await sendRequest(client, "textDocument/definition", {
1172
+ textDocument: { uri },
1173
+ position,
1174
+ })) as Location | Location[] | LocationLink | LocationLink[] | null;
1175
+
1176
+ if (!result) {
1177
+ output = "No definition found";
1178
+ } else {
1179
+ const raw = Array.isArray(result) ? result : [result];
1180
+ const locations = raw.flatMap((loc) => {
1181
+ if ("uri" in loc) {
1182
+ return [loc as Location];
1183
+ }
1184
+ if ("targetUri" in loc) {
1185
+ // Use targetSelectionRange (the precise identifier range) with fallback to targetRange
1186
+ const link = loc as LocationLink;
1187
+ return [{ uri: link.targetUri, range: link.targetSelectionRange ?? link.targetRange }];
1188
+ }
1189
+ return [];
1190
+ });
1146
1191
 
1147
- if (!result) {
1192
+ if (locations.length === 0) {
1148
1193
  output = "No definition found";
1149
1194
  } else {
1150
- const raw = Array.isArray(result) ? result : [result];
1151
- const locations = raw.flatMap((loc) => {
1152
- if ("uri" in loc) {
1153
- return [loc as Location];
1154
- }
1155
- if ("targetUri" in loc) {
1156
- // Use targetSelectionRange (the precise identifier range) with fallback to targetRange
1157
- const link = loc as LocationLink;
1158
- return [{ uri: link.targetUri, range: link.targetSelectionRange ?? link.targetRange }];
1159
- }
1160
- return [];
1161
- });
1162
-
1163
- if (locations.length === 0) {
1164
- output = "No definition found";
1165
- } else {
1166
- output = `Found ${locations.length} definition(s):\n${locations
1167
- .map((loc) => ` ${formatLocation(loc, session.cwd)}`)
1168
- .join("\n")}`;
1169
- }
1195
+ output = `Found ${locations.length} definition(s):\n${locations
1196
+ .map((loc) => ` ${formatLocation(loc, this.session.cwd)}`)
1197
+ .join("\n")}`;
1170
1198
  }
1171
- break;
1172
1199
  }
1200
+ break;
1201
+ }
1173
1202
 
1174
- case "references": {
1175
- const result = (await sendRequest(client, "textDocument/references", {
1176
- textDocument: { uri },
1177
- position,
1178
- context: { includeDeclaration: include_declaration ?? true },
1179
- })) as Location[] | null;
1203
+ case "references": {
1204
+ const result = (await sendRequest(client, "textDocument/references", {
1205
+ textDocument: { uri },
1206
+ position,
1207
+ context: { includeDeclaration: include_declaration ?? true },
1208
+ })) as Location[] | null;
1180
1209
 
1181
- if (!result || result.length === 0) {
1182
- output = "No references found";
1183
- } else {
1184
- const lines = result.map((loc) => ` ${formatLocation(loc, session.cwd)}`);
1185
- output = `Found ${result.length} reference(s):\n${lines.join("\n")}`;
1186
- }
1187
- break;
1210
+ if (!result || result.length === 0) {
1211
+ output = "No references found";
1212
+ } else {
1213
+ const lines = result.map((loc) => ` ${formatLocation(loc, this.session.cwd)}`);
1214
+ output = `Found ${result.length} reference(s):\n${lines.join("\n")}`;
1188
1215
  }
1216
+ break;
1217
+ }
1189
1218
 
1190
- case "hover": {
1191
- const result = (await sendRequest(client, "textDocument/hover", {
1192
- textDocument: { uri },
1193
- position,
1194
- })) as Hover | null;
1219
+ case "hover": {
1220
+ const result = (await sendRequest(client, "textDocument/hover", {
1221
+ textDocument: { uri },
1222
+ position,
1223
+ })) as Hover | null;
1195
1224
 
1196
- if (!result || !result.contents) {
1197
- output = "No hover information";
1198
- } else {
1199
- output = extractHoverText(result.contents);
1200
- }
1201
- break;
1225
+ if (!result || !result.contents) {
1226
+ output = "No hover information";
1227
+ } else {
1228
+ output = extractHoverText(result.contents);
1202
1229
  }
1230
+ break;
1231
+ }
1203
1232
 
1204
- case "symbols": {
1205
- const result = (await sendRequest(client, "textDocument/documentSymbol", {
1206
- textDocument: { uri },
1207
- })) as (DocumentSymbol | SymbolInformation)[] | null;
1233
+ case "symbols": {
1234
+ const result = (await sendRequest(client, "textDocument/documentSymbol", {
1235
+ textDocument: { uri },
1236
+ })) as (DocumentSymbol | SymbolInformation)[] | null;
1208
1237
 
1209
- if (!result || result.length === 0) {
1210
- output = "No symbols found";
1211
- } else if (!targetFile) {
1212
- return {
1213
- content: [{ type: "text", text: "Error: file parameter required for symbols" }],
1214
- details: { action, serverName, success: false },
1215
- };
1238
+ if (!result || result.length === 0) {
1239
+ output = "No symbols found";
1240
+ } else if (!targetFile) {
1241
+ return {
1242
+ content: [{ type: "text", text: "Error: file parameter required for symbols" }],
1243
+ details: { action, serverName, success: false },
1244
+ };
1245
+ } else {
1246
+ const relPath = path.relative(this.session.cwd, targetFile);
1247
+ // Check if hierarchical (DocumentSymbol) or flat (SymbolInformation)
1248
+ if ("selectionRange" in result[0]) {
1249
+ // Hierarchical
1250
+ const lines = (result as DocumentSymbol[]).flatMap((s) => formatDocumentSymbol(s));
1251
+ output = `Symbols in ${relPath}:\n${lines.join("\n")}`;
1216
1252
  } else {
1217
- const relPath = path.relative(session.cwd, targetFile);
1218
- // Check if hierarchical (DocumentSymbol) or flat (SymbolInformation)
1219
- if ("selectionRange" in result[0]) {
1220
- // Hierarchical
1221
- const lines = (result as DocumentSymbol[]).flatMap((s) => formatDocumentSymbol(s));
1222
- output = `Symbols in ${relPath}:\n${lines.join("\n")}`;
1223
- } else {
1224
- // Flat
1225
- const lines = (result as SymbolInformation[]).map((s) => {
1226
- const line = s.location.range.start.line + 1;
1227
- const icon = symbolKindToIcon(s.kind);
1228
- return `${icon} ${s.name} @ line ${line}`;
1229
- });
1230
- output = `Symbols in ${relPath}:\n${lines.join("\n")}`;
1231
- }
1253
+ // Flat
1254
+ const lines = (result as SymbolInformation[]).map((s) => {
1255
+ const line = s.location.range.start.line + 1;
1256
+ const icon = symbolKindToIcon(s.kind);
1257
+ return `${icon} ${s.name} @ line ${line}`;
1258
+ });
1259
+ output = `Symbols in ${relPath}:\n${lines.join("\n")}`;
1232
1260
  }
1233
- break;
1234
1261
  }
1262
+ break;
1263
+ }
1235
1264
 
1236
- case "workspace_symbols": {
1237
- if (!query) {
1238
- return {
1239
- content: [{ type: "text", text: "Error: query parameter required for workspace_symbols" }],
1240
- details: { action, serverName, success: false },
1241
- };
1242
- }
1265
+ case "workspace_symbols": {
1266
+ if (!query) {
1267
+ return {
1268
+ content: [{ type: "text", text: "Error: query parameter required for workspace_symbols" }],
1269
+ details: { action, serverName, success: false },
1270
+ };
1271
+ }
1243
1272
 
1244
- const result = (await sendRequest(client, "workspace/symbol", { query })) as
1245
- | SymbolInformation[]
1246
- | null;
1273
+ const result = (await sendRequest(client, "workspace/symbol", { query })) as SymbolInformation[] | null;
1247
1274
 
1248
- if (!result || result.length === 0) {
1249
- output = `No symbols matching "${query}"`;
1250
- } else {
1251
- const lines = result.map((s) => formatSymbolInformation(s, session.cwd));
1252
- output = `Found ${result.length} symbol(s) matching "${query}":\n${lines.map((l) => ` ${l}`).join("\n")}`;
1253
- }
1254
- break;
1275
+ if (!result || result.length === 0) {
1276
+ output = `No symbols matching "${query}"`;
1277
+ } else {
1278
+ const lines = result.map((s) => formatSymbolInformation(s, this.session.cwd));
1279
+ output = `Found ${result.length} symbol(s) matching "${query}":\n${lines.map((l) => ` ${l}`).join("\n")}`;
1255
1280
  }
1281
+ break;
1282
+ }
1256
1283
 
1257
- case "rename": {
1258
- if (!new_name) {
1259
- return {
1260
- content: [{ type: "text", text: "Error: new_name parameter required for rename" }],
1261
- details: { action, serverName, success: false },
1262
- };
1263
- }
1284
+ case "rename": {
1285
+ if (!new_name) {
1286
+ return {
1287
+ content: [{ type: "text", text: "Error: new_name parameter required for rename" }],
1288
+ details: { action, serverName, success: false },
1289
+ };
1290
+ }
1264
1291
 
1265
- const result = (await sendRequest(client, "textDocument/rename", {
1266
- textDocument: { uri },
1267
- position,
1268
- newName: new_name,
1269
- })) as WorkspaceEdit | null;
1292
+ const result = (await sendRequest(client, "textDocument/rename", {
1293
+ textDocument: { uri },
1294
+ position,
1295
+ newName: new_name,
1296
+ })) as WorkspaceEdit | null;
1270
1297
 
1271
- if (!result) {
1272
- output = "Rename returned no edits";
1298
+ if (!result) {
1299
+ output = "Rename returned no edits";
1300
+ } else {
1301
+ const shouldApply = apply !== false;
1302
+ if (shouldApply) {
1303
+ const applied = await applyWorkspaceEdit(result, this.session.cwd);
1304
+ output = `Applied rename:\n${applied.map((a) => ` ${a}`).join("\n")}`;
1273
1305
  } else {
1274
- const shouldApply = apply !== false;
1275
- if (shouldApply) {
1276
- const applied = await applyWorkspaceEdit(result, session.cwd);
1277
- output = `Applied rename:\n${applied.map((a) => ` ${a}`).join("\n")}`;
1278
- } else {
1279
- const preview = formatWorkspaceEdit(result, session.cwd);
1280
- output = `Rename preview:\n${preview.map((p) => ` ${p}`).join("\n")}`;
1281
- }
1306
+ const preview = formatWorkspaceEdit(result, this.session.cwd);
1307
+ output = `Rename preview:\n${preview.map((p) => ` ${p}`).join("\n")}`;
1282
1308
  }
1283
- break;
1309
+ }
1310
+ break;
1311
+ }
1312
+
1313
+ case "actions": {
1314
+ if (!targetFile) {
1315
+ return {
1316
+ content: [{ type: "text", text: "Error: file parameter required for actions" }],
1317
+ details: { action, serverName, success: false },
1318
+ };
1319
+ }
1320
+
1321
+ const actionsMinVersion = client.diagnosticsVersion;
1322
+ await refreshFile(client, targetFile);
1323
+ const diagnostics = await waitForDiagnostics(client, uri, 3000, undefined, actionsMinVersion);
1324
+ const endLine = (end_line ?? line ?? 1) - 1;
1325
+ const endCharacter = (end_character ?? column ?? 1) - 1;
1326
+ const range = { start: position, end: { line: endLine, character: endCharacter } };
1327
+ const relevantDiagnostics = diagnostics.filter(
1328
+ (d) => d.range.start.line <= range.end.line && d.range.end.line >= range.start.line,
1329
+ );
1330
+
1331
+ const codeActionContext: { diagnostics: Diagnostic[]; only?: string[] } = {
1332
+ diagnostics: relevantDiagnostics,
1333
+ };
1334
+ if (kind) {
1335
+ codeActionContext.only = [kind];
1284
1336
  }
1285
1337
 
1286
- case "actions": {
1287
- if (!targetFile) {
1338
+ const result = (await sendRequest(client, "textDocument/codeAction", {
1339
+ textDocument: { uri },
1340
+ range,
1341
+ context: codeActionContext,
1342
+ })) as Array<CodeAction | Command> | null;
1343
+
1344
+ if (!result || result.length === 0) {
1345
+ output = "No code actions available";
1346
+ } else if (action_index !== undefined) {
1347
+ // Apply specific action
1348
+ if (action_index < 0 || action_index >= result.length) {
1288
1349
  return {
1289
- content: [{ type: "text", text: "Error: file parameter required for actions" }],
1350
+ content: [
1351
+ {
1352
+ type: "text",
1353
+ text: `Error: action_index ${action_index} out of range (0-${result.length - 1})`,
1354
+ },
1355
+ ],
1290
1356
  details: { action, serverName, success: false },
1291
1357
  };
1292
1358
  }
1293
1359
 
1294
- const actionsMinVersion = client.diagnosticsVersion;
1295
- await refreshFile(client, targetFile);
1296
- const diagnostics = await waitForDiagnostics(client, uri, 3000, undefined, actionsMinVersion);
1297
- const endLine = (end_line ?? line ?? 1) - 1;
1298
- const endCharacter = (end_character ?? column ?? 1) - 1;
1299
- const range = { start: position, end: { line: endLine, character: endCharacter } };
1300
- const relevantDiagnostics = diagnostics.filter(
1301
- (d) => d.range.start.line <= range.end.line && d.range.end.line >= range.start.line,
1302
- );
1303
-
1304
- const codeActionContext: { diagnostics: Diagnostic[]; only?: string[] } = {
1305
- diagnostics: relevantDiagnostics,
1306
- };
1307
- if (kind) {
1308
- codeActionContext.only = [kind];
1309
- }
1310
-
1311
- const result = (await sendRequest(client, "textDocument/codeAction", {
1312
- textDocument: { uri },
1313
- range,
1314
- context: codeActionContext,
1315
- })) as Array<CodeAction | Command> | null;
1316
-
1317
- if (!result || result.length === 0) {
1318
- output = "No code actions available";
1319
- } else if (action_index !== undefined) {
1320
- // Apply specific action
1321
- if (action_index < 0 || action_index >= result.length) {
1322
- return {
1323
- content: [
1324
- {
1325
- type: "text",
1326
- text: `Error: action_index ${action_index} out of range (0-${result.length - 1})`,
1327
- },
1328
- ],
1329
- details: { action, serverName, success: false },
1330
- };
1360
+ const isCommand = (candidate: CodeAction | Command): candidate is Command =>
1361
+ typeof (candidate as Command).command === "string";
1362
+ const isCodeAction = (candidate: CodeAction | Command): candidate is CodeAction =>
1363
+ !isCommand(candidate);
1364
+ const getCommandPayload = (
1365
+ candidate: CodeAction | Command,
1366
+ ): { command: string; arguments?: unknown[] } | null => {
1367
+ if (isCommand(candidate)) {
1368
+ return { command: candidate.command, arguments: candidate.arguments };
1331
1369
  }
1332
-
1333
- const isCommand = (candidate: CodeAction | Command): candidate is Command =>
1334
- typeof (candidate as Command).command === "string";
1335
- const isCodeAction = (candidate: CodeAction | Command): candidate is CodeAction =>
1336
- !isCommand(candidate);
1337
- const getCommandPayload = (
1338
- candidate: CodeAction | Command,
1339
- ): { command: string; arguments?: unknown[] } | null => {
1340
- if (isCommand(candidate)) {
1341
- return { command: candidate.command, arguments: candidate.arguments };
1342
- }
1343
- if (candidate.command) {
1344
- return { command: candidate.command.command, arguments: candidate.command.arguments };
1345
- }
1346
- return null;
1347
- };
1348
-
1349
- const codeAction = result[action_index];
1350
-
1351
- // Resolve if needed
1352
- let resolvedAction = codeAction;
1353
- if (
1354
- isCodeAction(codeAction) &&
1355
- !codeAction.edit &&
1356
- codeAction.data &&
1357
- client.serverCapabilities?.codeActionProvider
1358
- ) {
1359
- const provider = client.serverCapabilities.codeActionProvider;
1360
- if (typeof provider === "object" && provider.resolveProvider) {
1361
- resolvedAction = (await sendRequest(client, "codeAction/resolve", codeAction)) as CodeAction;
1362
- }
1370
+ if (candidate.command) {
1371
+ return { command: candidate.command.command, arguments: candidate.command.arguments };
1363
1372
  }
1373
+ return null;
1374
+ };
1364
1375
 
1365
- if (isCodeAction(resolvedAction) && resolvedAction.edit) {
1366
- const applied = await applyWorkspaceEdit(resolvedAction.edit, session.cwd);
1367
- output = `Applied "${codeAction.title}":\n${applied.map((a) => ` ${a}`).join("\n")}`;
1368
- } else {
1369
- const commandPayload = getCommandPayload(resolvedAction);
1370
- if (commandPayload) {
1371
- await sendRequest(client, "workspace/executeCommand", commandPayload);
1372
- output = `Executed "${codeAction.title}"`;
1373
- } else {
1374
- output = `Code action "${codeAction.title}" has no edits or command to apply`;
1375
- }
1376
+ const codeAction = result[action_index];
1377
+
1378
+ // Resolve if needed
1379
+ let resolvedAction = codeAction;
1380
+ if (
1381
+ isCodeAction(codeAction) &&
1382
+ !codeAction.edit &&
1383
+ codeAction.data &&
1384
+ client.serverCapabilities?.codeActionProvider
1385
+ ) {
1386
+ const provider = client.serverCapabilities.codeActionProvider;
1387
+ if (typeof provider === "object" && provider.resolveProvider) {
1388
+ resolvedAction = (await sendRequest(client, "codeAction/resolve", codeAction)) as CodeAction;
1376
1389
  }
1377
- } else {
1378
- // List available actions
1379
- const lines = result.map((actionItem, i) => {
1380
- if ("kind" in actionItem || "isPreferred" in actionItem || "edit" in actionItem) {
1381
- const actionDetails = actionItem as CodeAction;
1382
- const preferred = actionDetails.isPreferred ? " (preferred)" : "";
1383
- const kindInfo = actionDetails.kind ? ` [${actionDetails.kind}]` : "";
1384
- return ` [${i}] ${actionDetails.title}${kindInfo}${preferred}`;
1385
- }
1386
- return ` [${i}] ${actionItem.title}`;
1387
- });
1388
- output = `Available code actions:\n${lines.join("\n")}\n\nUse action_index parameter to apply a specific action.`;
1389
- }
1390
- break;
1391
- }
1392
-
1393
- case "incoming_calls":
1394
- case "outgoing_calls": {
1395
- // First, prepare the call hierarchy item at the cursor position
1396
- const prepareResult = (await sendRequest(client, "textDocument/prepareCallHierarchy", {
1397
- textDocument: { uri },
1398
- position,
1399
- })) as CallHierarchyItem[] | null;
1400
-
1401
- if (!prepareResult || prepareResult.length === 0) {
1402
- output = "No callable symbol found at this position";
1403
- break;
1404
1390
  }
1405
1391
 
1406
- const item = prepareResult[0];
1407
-
1408
- if (action === "incoming_calls") {
1409
- const calls = (await sendRequest(client, "callHierarchy/incomingCalls", { item })) as
1410
- | CallHierarchyIncomingCall[]
1411
- | null;
1412
-
1413
- if (!calls || calls.length === 0) {
1414
- output = `No callers found for "${item.name}"`;
1415
- } else {
1416
- const lines = calls.map((call) => {
1417
- const loc = { uri: call.from.uri, range: call.from.selectionRange };
1418
- const detail = call.from.detail ? ` (${call.from.detail})` : "";
1419
- return ` ${call.from.name}${detail} @ ${formatLocation(loc, session.cwd)}`;
1420
- });
1421
- output = `Found ${calls.length} caller(s) of "${item.name}":\n${lines.join("\n")}`;
1422
- }
1392
+ if (isCodeAction(resolvedAction) && resolvedAction.edit) {
1393
+ const applied = await applyWorkspaceEdit(resolvedAction.edit, this.session.cwd);
1394
+ output = `Applied "${codeAction.title}":\n${applied.map((a) => ` ${a}`).join("\n")}`;
1423
1395
  } else {
1424
- const calls = (await sendRequest(client, "callHierarchy/outgoingCalls", { item })) as
1425
- | CallHierarchyOutgoingCall[]
1426
- | null;
1427
-
1428
- if (!calls || calls.length === 0) {
1429
- output = `"${item.name}" doesn't call any functions`;
1396
+ const commandPayload = getCommandPayload(resolvedAction);
1397
+ if (commandPayload) {
1398
+ await sendRequest(client, "workspace/executeCommand", commandPayload);
1399
+ output = `Executed "${codeAction.title}"`;
1430
1400
  } else {
1431
- const lines = calls.map((call) => {
1432
- const loc = { uri: call.to.uri, range: call.to.selectionRange };
1433
- const detail = call.to.detail ? ` (${call.to.detail})` : "";
1434
- return ` ${call.to.name}${detail} @ ${formatLocation(loc, session.cwd)}`;
1435
- });
1436
- output = `"${item.name}" calls ${calls.length} function(s):\n${lines.join("\n")}`;
1401
+ output = `Code action "${codeAction.title}" has no edits or command to apply`;
1437
1402
  }
1438
1403
  }
1404
+ } else {
1405
+ // List available actions
1406
+ const lines = result.map((actionItem, i) => {
1407
+ if ("kind" in actionItem || "isPreferred" in actionItem || "edit" in actionItem) {
1408
+ const actionDetails = actionItem as CodeAction;
1409
+ const preferred = actionDetails.isPreferred ? " (preferred)" : "";
1410
+ const kindInfo = actionDetails.kind ? ` [${actionDetails.kind}]` : "";
1411
+ return ` [${i}] ${actionDetails.title}${kindInfo}${preferred}`;
1412
+ }
1413
+ return ` [${i}] ${actionItem.title}`;
1414
+ });
1415
+ output = `Available code actions:\n${lines.join("\n")}\n\nUse action_index parameter to apply a specific action.`;
1416
+ }
1417
+ break;
1418
+ }
1419
+
1420
+ case "incoming_calls":
1421
+ case "outgoing_calls": {
1422
+ // First, prepare the call hierarchy item at the cursor position
1423
+ const prepareResult = (await sendRequest(client, "textDocument/prepareCallHierarchy", {
1424
+ textDocument: { uri },
1425
+ position,
1426
+ })) as CallHierarchyItem[] | null;
1427
+
1428
+ if (!prepareResult || prepareResult.length === 0) {
1429
+ output = "No callable symbol found at this position";
1439
1430
  break;
1440
1431
  }
1441
1432
 
1442
- // =====================================================================
1443
- // Rust-Analyzer Specific Operations
1444
- // =====================================================================
1433
+ const item = prepareResult[0];
1445
1434
 
1446
- case "flycheck": {
1447
- if (!hasCapability(serverConfig, "flycheck")) {
1448
- return {
1449
- content: [{ type: "text", text: "Error: flycheck requires rust-analyzer" }],
1450
- details: { action, serverName, success: false },
1451
- };
1452
- }
1435
+ if (action === "incoming_calls") {
1436
+ const calls = (await sendRequest(client, "callHierarchy/incomingCalls", { item })) as
1437
+ | CallHierarchyIncomingCall[]
1438
+ | null;
1453
1439
 
1454
- await rustAnalyzer.flycheck(client, resolvedFile ?? undefined);
1455
- const collected: Array<{ filePath: string; diagnostic: Diagnostic }> = [];
1456
- for (const [diagUri, diags] of client.diagnostics.entries()) {
1457
- const relPath = path.relative(session.cwd, uriToFile(diagUri));
1458
- for (const diag of diags) {
1459
- collected.push({ filePath: relPath, diagnostic: diag });
1460
- }
1440
+ if (!calls || calls.length === 0) {
1441
+ output = `No callers found for "${item.name}"`;
1442
+ } else {
1443
+ const lines = calls.map((call) => {
1444
+ const loc = { uri: call.from.uri, range: call.from.selectionRange };
1445
+ const detail = call.from.detail ? ` (${call.from.detail})` : "";
1446
+ return ` ${call.from.name}${detail} @ ${formatLocation(loc, this.session.cwd)}`;
1447
+ });
1448
+ output = `Found ${calls.length} caller(s) of "${item.name}":\n${lines.join("\n")}`;
1461
1449
  }
1450
+ } else {
1451
+ const calls = (await sendRequest(client, "callHierarchy/outgoingCalls", { item })) as
1452
+ | CallHierarchyOutgoingCall[]
1453
+ | null;
1462
1454
 
1463
- if (collected.length === 0) {
1464
- output = "Flycheck: no issues found";
1455
+ if (!calls || calls.length === 0) {
1456
+ output = `"${item.name}" doesn't call any functions`;
1465
1457
  } else {
1466
- const summary = formatDiagnosticsSummary(collected.map((d) => d.diagnostic));
1467
- const formatted = collected.slice(0, 20).map((d) => formatDiagnostic(d.diagnostic, d.filePath));
1468
- const more = collected.length > 20 ? `\n ... and ${collected.length - 20} more` : "";
1469
- output = `Flycheck ${summary}:\n${formatted.map((f) => ` ${f}`).join("\n")}${more}`;
1458
+ const lines = calls.map((call) => {
1459
+ const loc = { uri: call.to.uri, range: call.to.selectionRange };
1460
+ const detail = call.to.detail ? ` (${call.to.detail})` : "";
1461
+ return ` ${call.to.name}${detail} @ ${formatLocation(loc, this.session.cwd)}`;
1462
+ });
1463
+ output = `"${item.name}" calls ${calls.length} function(s):\n${lines.join("\n")}`;
1470
1464
  }
1471
- break;
1472
1465
  }
1466
+ break;
1467
+ }
1473
1468
 
1474
- case "expand_macro": {
1475
- if (!hasCapability(serverConfig, "expandMacro")) {
1476
- return {
1477
- content: [{ type: "text", text: "Error: expand_macro requires rust-analyzer" }],
1478
- details: { action, serverName, success: false },
1479
- };
1480
- }
1469
+ // =====================================================================
1470
+ // Rust-Analyzer Specific Operations
1471
+ // =====================================================================
1481
1472
 
1482
- if (!targetFile) {
1483
- return {
1484
- content: [{ type: "text", text: "Error: file parameter required for expand_macro" }],
1485
- details: { action, serverName, success: false },
1486
- };
1487
- }
1473
+ case "flycheck": {
1474
+ if (!hasCapability(serverConfig, "flycheck")) {
1475
+ return {
1476
+ content: [{ type: "text", text: "Error: flycheck requires rust-analyzer" }],
1477
+ details: { action, serverName, success: false },
1478
+ };
1479
+ }
1488
1480
 
1489
- const result = await rustAnalyzer.expandMacro(client, targetFile, line || 1, column || 1);
1490
- if (!result) {
1491
- output = "No macro expansion at this position";
1492
- } else {
1493
- output = `Macro: ${result.name}\n\nExpansion:\n${result.expansion}`;
1481
+ await rustAnalyzer.flycheck(client, resolvedFile ?? undefined);
1482
+ const collected: Array<{ filePath: string; diagnostic: Diagnostic }> = [];
1483
+ for (const [diagUri, diags] of client.diagnostics.entries()) {
1484
+ const relPath = path.relative(this.session.cwd, uriToFile(diagUri));
1485
+ for (const diag of diags) {
1486
+ collected.push({ filePath: relPath, diagnostic: diag });
1494
1487
  }
1495
- break;
1496
1488
  }
1497
1489
 
1498
- case "ssr": {
1499
- if (!hasCapability(serverConfig, "ssr")) {
1500
- return {
1501
- content: [{ type: "text", text: "Error: ssr requires rust-analyzer" }],
1502
- details: { action, serverName, success: false },
1503
- };
1504
- }
1490
+ if (collected.length === 0) {
1491
+ output = "Flycheck: no issues found";
1492
+ } else {
1493
+ const summary = formatDiagnosticsSummary(collected.map((d) => d.diagnostic));
1494
+ const formatted = collected.slice(0, 20).map((d) => formatDiagnostic(d.diagnostic, d.filePath));
1495
+ const more = collected.length > 20 ? `\n ... and ${collected.length - 20} more` : "";
1496
+ output = `Flycheck ${summary}:\n${formatted.map((f) => ` ${f}`).join("\n")}${more}`;
1497
+ }
1498
+ break;
1499
+ }
1505
1500
 
1506
- if (!query) {
1507
- return {
1508
- content: [{ type: "text", text: "Error: query parameter (pattern) required for ssr" }],
1509
- details: { action, serverName, success: false },
1510
- };
1511
- }
1501
+ case "expand_macro": {
1502
+ if (!hasCapability(serverConfig, "expandMacro")) {
1503
+ return {
1504
+ content: [{ type: "text", text: "Error: expand_macro requires rust-analyzer" }],
1505
+ details: { action, serverName, success: false },
1506
+ };
1507
+ }
1512
1508
 
1513
- if (!replacement) {
1514
- return {
1515
- content: [{ type: "text", text: "Error: replacement parameter required for ssr" }],
1516
- details: { action, serverName, success: false },
1517
- };
1518
- }
1509
+ if (!targetFile) {
1510
+ return {
1511
+ content: [{ type: "text", text: "Error: file parameter required for expand_macro" }],
1512
+ details: { action, serverName, success: false },
1513
+ };
1514
+ }
1519
1515
 
1520
- const shouldApply = apply === true;
1521
- const result = await rustAnalyzer.ssr(client, query, replacement, !shouldApply);
1516
+ const result = await rustAnalyzer.expandMacro(client, targetFile, line || 1, column || 1);
1517
+ if (!result) {
1518
+ output = "No macro expansion at this position";
1519
+ } else {
1520
+ output = `Macro: ${result.name}\n\nExpansion:\n${result.expansion}`;
1521
+ }
1522
+ break;
1523
+ }
1522
1524
 
1523
- if (shouldApply) {
1524
- const applied = await applyWorkspaceEdit(result, session.cwd);
1525
- output =
1526
- applied.length > 0
1527
- ? `Applied SSR:\n${applied.map((a) => ` ${a}`).join("\n")}`
1528
- : "SSR: no matches found";
1529
- } else {
1530
- const preview = formatWorkspaceEdit(result, session.cwd);
1531
- output =
1532
- preview.length > 0
1533
- ? `SSR preview:\n${preview.map((p) => ` ${p}`).join("\n")}`
1534
- : "SSR: no matches found";
1535
- }
1536
- break;
1525
+ case "ssr": {
1526
+ if (!hasCapability(serverConfig, "ssr")) {
1527
+ return {
1528
+ content: [{ type: "text", text: "Error: ssr requires rust-analyzer" }],
1529
+ details: { action, serverName, success: false },
1530
+ };
1537
1531
  }
1538
1532
 
1539
- case "runnables": {
1540
- if (!hasCapability(serverConfig, "runnables")) {
1541
- return {
1542
- content: [{ type: "text", text: "Error: runnables requires rust-analyzer" }],
1543
- details: { action, serverName, success: false },
1544
- };
1545
- }
1533
+ if (!query) {
1534
+ return {
1535
+ content: [{ type: "text", text: "Error: query parameter (pattern) required for ssr" }],
1536
+ details: { action, serverName, success: false },
1537
+ };
1538
+ }
1546
1539
 
1547
- if (!targetFile) {
1548
- return {
1549
- content: [{ type: "text", text: "Error: file parameter required for runnables" }],
1550
- details: { action, serverName, success: false },
1551
- };
1552
- }
1540
+ if (!replacement) {
1541
+ return {
1542
+ content: [{ type: "text", text: "Error: replacement parameter required for ssr" }],
1543
+ details: { action, serverName, success: false },
1544
+ };
1545
+ }
1553
1546
 
1554
- const result = await rustAnalyzer.runnables(client, targetFile, line);
1555
- if (result.length === 0) {
1556
- output = "No runnables found";
1557
- } else {
1558
- const lines = result.map((r) => {
1559
- const args = r.args?.cargoArgs?.join(" ") || "";
1560
- return ` [${r.kind}] ${r.label}${args ? ` (cargo ${args})` : ""}`;
1561
- });
1562
- output = `Found ${result.length} runnable(s):\n${lines.join("\n")}`;
1563
- }
1564
- break;
1547
+ const shouldApply = apply === true;
1548
+ const result = await rustAnalyzer.ssr(client, query, replacement, !shouldApply);
1549
+
1550
+ if (shouldApply) {
1551
+ const applied = await applyWorkspaceEdit(result, this.session.cwd);
1552
+ output =
1553
+ applied.length > 0
1554
+ ? `Applied SSR:\n${applied.map((a) => ` ${a}`).join("\n")}`
1555
+ : "SSR: no matches found";
1556
+ } else {
1557
+ const preview = formatWorkspaceEdit(result, this.session.cwd);
1558
+ output =
1559
+ preview.length > 0
1560
+ ? `SSR preview:\n${preview.map((p) => ` ${p}`).join("\n")}`
1561
+ : "SSR: no matches found";
1565
1562
  }
1563
+ break;
1564
+ }
1566
1565
 
1567
- case "related_tests": {
1568
- if (!hasCapability(serverConfig, "relatedTests")) {
1569
- return {
1570
- content: [{ type: "text", text: "Error: related_tests requires rust-analyzer" }],
1571
- details: { action, serverName, success: false },
1572
- };
1573
- }
1566
+ case "runnables": {
1567
+ if (!hasCapability(serverConfig, "runnables")) {
1568
+ return {
1569
+ content: [{ type: "text", text: "Error: runnables requires rust-analyzer" }],
1570
+ details: { action, serverName, success: false },
1571
+ };
1572
+ }
1574
1573
 
1575
- if (!targetFile) {
1576
- return {
1577
- content: [{ type: "text", text: "Error: file parameter required for related_tests" }],
1578
- details: { action, serverName, success: false },
1579
- };
1580
- }
1574
+ if (!targetFile) {
1575
+ return {
1576
+ content: [{ type: "text", text: "Error: file parameter required for runnables" }],
1577
+ details: { action, serverName, success: false },
1578
+ };
1579
+ }
1581
1580
 
1582
- const result = await rustAnalyzer.relatedTests(client, targetFile, line || 1, column || 1);
1583
- if (result.length === 0) {
1584
- output = "No related tests found";
1585
- } else {
1586
- output = `Found ${result.length} related test(s):\n${result.map((t) => ` ${t}`).join("\n")}`;
1587
- }
1588
- break;
1581
+ const result = await rustAnalyzer.runnables(client, targetFile, line);
1582
+ if (result.length === 0) {
1583
+ output = "No runnables found";
1584
+ } else {
1585
+ const lines = result.map((r) => {
1586
+ const args = r.args?.cargoArgs?.join(" ") || "";
1587
+ return ` [${r.kind}] ${r.label}${args ? ` (cargo ${args})` : ""}`;
1588
+ });
1589
+ output = `Found ${result.length} runnable(s):\n${lines.join("\n")}`;
1589
1590
  }
1591
+ break;
1592
+ }
1590
1593
 
1591
- case "reload_workspace": {
1592
- await rustAnalyzer.reloadWorkspace(client);
1593
- output = "Workspace reloaded successfully";
1594
- break;
1594
+ case "related_tests": {
1595
+ if (!hasCapability(serverConfig, "relatedTests")) {
1596
+ return {
1597
+ content: [{ type: "text", text: "Error: related_tests requires rust-analyzer" }],
1598
+ details: { action, serverName, success: false },
1599
+ };
1600
+ }
1601
+
1602
+ if (!targetFile) {
1603
+ return {
1604
+ content: [{ type: "text", text: "Error: file parameter required for related_tests" }],
1605
+ details: { action, serverName, success: false },
1606
+ };
1595
1607
  }
1596
1608
 
1597
- default:
1598
- output = `Unknown action: ${action}`;
1609
+ const result = await rustAnalyzer.relatedTests(client, targetFile, line || 1, column || 1);
1610
+ if (result.length === 0) {
1611
+ output = "No related tests found";
1612
+ } else {
1613
+ output = `Found ${result.length} related test(s):\n${result.map((t) => ` ${t}`).join("\n")}`;
1614
+ }
1615
+ break;
1599
1616
  }
1600
1617
 
1601
- return {
1602
- content: [{ type: "text", text: output }],
1603
- details: { serverName, action, success: true },
1604
- };
1605
- } catch (err) {
1606
- const errorMessage = err instanceof Error ? err.message : String(err);
1607
- return {
1608
- content: [{ type: "text", text: `LSP error: ${errorMessage}` }],
1609
- details: { serverName, action, success: false },
1610
- };
1618
+ case "reload_workspace": {
1619
+ await rustAnalyzer.reloadWorkspace(client);
1620
+ output = "Workspace reloaded successfully";
1621
+ break;
1622
+ }
1623
+
1624
+ default:
1625
+ output = `Unknown action: ${action}`;
1611
1626
  }
1612
- },
1613
- };
1627
+
1628
+ return {
1629
+ content: [{ type: "text", text: output }],
1630
+ details: { serverName, action, success: true },
1631
+ };
1632
+ } catch (err) {
1633
+ const errorMessage = err instanceof Error ? err.message : String(err);
1634
+ return {
1635
+ content: [{ type: "text", text: `LSP error: ${errorMessage}` }],
1636
+ details: { serverName, action, success: false },
1637
+ };
1638
+ }
1639
+ }
1614
1640
  }