@oh-my-pi/pi-coding-agent 1.340.0 → 2.0.1337

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 (153) hide show
  1. package/CHANGELOG.md +115 -1
  2. package/README.md +1 -1
  3. package/examples/custom-tools/subagent/index.ts +1 -1
  4. package/package.json +5 -3
  5. package/src/cli/args.ts +13 -6
  6. package/src/cli/file-processor.ts +3 -3
  7. package/src/cli/list-models.ts +2 -2
  8. package/src/cli/plugin-cli.ts +1 -1
  9. package/src/cli/session-picker.ts +2 -2
  10. package/src/cli.ts +1 -1
  11. package/src/config.ts +3 -3
  12. package/src/core/agent-session.ts +189 -29
  13. package/src/core/bash-executor.ts +50 -10
  14. package/src/core/compaction/branch-summarization.ts +5 -5
  15. package/src/core/compaction/compaction.ts +3 -3
  16. package/src/core/compaction/index.ts +3 -3
  17. package/src/core/custom-commands/bundled/review/index.ts +156 -0
  18. package/src/core/custom-commands/index.ts +15 -0
  19. package/src/core/custom-commands/loader.ts +232 -0
  20. package/src/core/custom-commands/types.ts +112 -0
  21. package/src/core/custom-tools/index.ts +3 -3
  22. package/src/core/custom-tools/loader.ts +10 -8
  23. package/src/core/custom-tools/types.ts +11 -6
  24. package/src/core/custom-tools/wrapper.ts +2 -1
  25. package/src/core/exec.ts +22 -12
  26. package/src/core/export-html/index.ts +5 -5
  27. package/src/core/file-mentions.ts +54 -0
  28. package/src/core/hooks/index.ts +5 -5
  29. package/src/core/hooks/loader.ts +21 -16
  30. package/src/core/hooks/runner.ts +6 -6
  31. package/src/core/hooks/tool-wrapper.ts +2 -2
  32. package/src/core/hooks/types.ts +12 -15
  33. package/src/core/index.ts +6 -6
  34. package/src/core/logger.ts +112 -0
  35. package/src/core/mcp/client.ts +3 -3
  36. package/src/core/mcp/config.ts +1 -1
  37. package/src/core/mcp/index.ts +12 -12
  38. package/src/core/mcp/loader.ts +2 -2
  39. package/src/core/mcp/manager.ts +6 -6
  40. package/src/core/mcp/tool-bridge.ts +3 -3
  41. package/src/core/mcp/transports/http.ts +1 -1
  42. package/src/core/mcp/transports/index.ts +2 -2
  43. package/src/core/mcp/transports/stdio.ts +1 -1
  44. package/src/core/messages.ts +22 -0
  45. package/src/core/model-registry.ts +2 -2
  46. package/src/core/model-resolver.ts +103 -2
  47. package/src/core/plugins/doctor.ts +1 -1
  48. package/src/core/plugins/index.ts +6 -6
  49. package/src/core/plugins/installer.ts +4 -4
  50. package/src/core/plugins/loader.ts +4 -9
  51. package/src/core/plugins/manager.ts +5 -5
  52. package/src/core/plugins/paths.ts +3 -3
  53. package/src/core/sdk.ts +127 -52
  54. package/src/core/session-manager.ts +123 -20
  55. package/src/core/settings-manager.ts +106 -22
  56. package/src/core/skills.ts +5 -5
  57. package/src/core/slash-commands.ts +60 -45
  58. package/src/core/system-prompt.ts +6 -6
  59. package/src/core/title-generator.ts +94 -0
  60. package/src/core/tools/bash.ts +33 -157
  61. package/src/core/tools/context.ts +2 -2
  62. package/src/core/tools/edit-diff.ts +5 -5
  63. package/src/core/tools/edit.ts +60 -9
  64. package/src/core/tools/exa/company.ts +3 -3
  65. package/src/core/tools/exa/index.ts +16 -17
  66. package/src/core/tools/exa/linkedin.ts +3 -3
  67. package/src/core/tools/exa/mcp-client.ts +9 -9
  68. package/src/core/tools/exa/render.ts +5 -5
  69. package/src/core/tools/exa/researcher.ts +3 -3
  70. package/src/core/tools/exa/search.ts +6 -5
  71. package/src/core/tools/exa/types.ts +5 -6
  72. package/src/core/tools/exa/websets.ts +3 -3
  73. package/src/core/tools/find.ts +3 -3
  74. package/src/core/tools/grep.ts +6 -5
  75. package/src/core/tools/index.ts +114 -40
  76. package/src/core/tools/ls.ts +4 -4
  77. package/src/core/tools/lsp/client.ts +204 -108
  78. package/src/core/tools/lsp/config.ts +709 -35
  79. package/src/core/tools/lsp/edits.ts +2 -2
  80. package/src/core/tools/lsp/index.ts +432 -30
  81. package/src/core/tools/lsp/render.ts +2 -2
  82. package/src/core/tools/lsp/rust-analyzer.ts +3 -3
  83. package/src/core/tools/lsp/types.ts +5 -0
  84. package/src/core/tools/lsp/utils.ts +1 -1
  85. package/src/core/tools/notebook.ts +1 -1
  86. package/src/core/tools/output.ts +175 -0
  87. package/src/core/tools/read.ts +7 -7
  88. package/src/core/tools/renderers.ts +92 -13
  89. package/src/core/tools/review.ts +268 -0
  90. package/src/core/tools/task/agents.ts +1 -1
  91. package/src/core/tools/task/bundled-agents/explore.md +1 -1
  92. package/src/core/tools/task/bundled-agents/reviewer.md +53 -38
  93. package/src/core/tools/task/discovery.ts +2 -2
  94. package/src/core/tools/task/executor.ts +145 -28
  95. package/src/core/tools/task/index.ts +78 -30
  96. package/src/core/tools/task/model-resolver.ts +72 -13
  97. package/src/core/tools/task/parallel.ts +1 -1
  98. package/src/core/tools/task/render.ts +219 -30
  99. package/src/core/tools/task/subprocess-tool-registry.ts +89 -0
  100. package/src/core/tools/task/types.ts +36 -2
  101. package/src/core/tools/web-fetch.ts +5 -3
  102. package/src/core/tools/web-search/auth.ts +1 -1
  103. package/src/core/tools/web-search/index.ts +17 -15
  104. package/src/core/tools/web-search/providers/anthropic.ts +2 -2
  105. package/src/core/tools/web-search/providers/exa.ts +3 -5
  106. package/src/core/tools/web-search/providers/perplexity.ts +1 -1
  107. package/src/core/tools/web-search/render.ts +3 -3
  108. package/src/core/tools/write.ts +70 -7
  109. package/src/index.ts +33 -17
  110. package/src/main.ts +60 -34
  111. package/src/migrations.ts +3 -3
  112. package/src/modes/index.ts +5 -5
  113. package/src/modes/interactive/components/armin.ts +1 -1
  114. package/src/modes/interactive/components/assistant-message.ts +1 -1
  115. package/src/modes/interactive/components/bash-execution.ts +4 -4
  116. package/src/modes/interactive/components/bordered-loader.ts +2 -2
  117. package/src/modes/interactive/components/branch-summary-message.ts +2 -2
  118. package/src/modes/interactive/components/compaction-summary-message.ts +2 -2
  119. package/src/modes/interactive/components/diff.ts +1 -1
  120. package/src/modes/interactive/components/dynamic-border.ts +1 -1
  121. package/src/modes/interactive/components/footer.ts +5 -5
  122. package/src/modes/interactive/components/hook-editor.ts +2 -2
  123. package/src/modes/interactive/components/hook-input.ts +2 -2
  124. package/src/modes/interactive/components/hook-message.ts +3 -3
  125. package/src/modes/interactive/components/hook-selector.ts +2 -2
  126. package/src/modes/interactive/components/model-selector.ts +341 -41
  127. package/src/modes/interactive/components/oauth-selector.ts +3 -3
  128. package/src/modes/interactive/components/plugin-settings.ts +4 -4
  129. package/src/modes/interactive/components/queue-mode-selector.ts +2 -2
  130. package/src/modes/interactive/components/session-selector.ts +24 -11
  131. package/src/modes/interactive/components/settings-defs.ts +51 -3
  132. package/src/modes/interactive/components/settings-selector.ts +13 -16
  133. package/src/modes/interactive/components/show-images-selector.ts +2 -2
  134. package/src/modes/interactive/components/theme-selector.ts +2 -2
  135. package/src/modes/interactive/components/thinking-selector.ts +2 -2
  136. package/src/modes/interactive/components/tool-execution.ts +44 -8
  137. package/src/modes/interactive/components/tree-selector.ts +5 -5
  138. package/src/modes/interactive/components/user-message-selector.ts +2 -2
  139. package/src/modes/interactive/components/user-message.ts +1 -1
  140. package/src/modes/interactive/components/welcome.ts +42 -5
  141. package/src/modes/interactive/interactive-mode.ts +169 -48
  142. package/src/modes/interactive/theme/theme.ts +8 -7
  143. package/src/modes/print-mode.ts +4 -3
  144. package/src/modes/rpc/rpc-client.ts +4 -4
  145. package/src/modes/rpc/rpc-mode.ts +21 -11
  146. package/src/modes/rpc/rpc-types.ts +3 -3
  147. package/src/utils/changelog.ts +2 -2
  148. package/src/utils/clipboard.ts +1 -1
  149. package/src/utils/shell-snapshot.ts +218 -0
  150. package/src/utils/shell.ts +93 -13
  151. package/src/utils/tools-manager.ts +1 -1
  152. package/examples/custom-tools/subagent/agents/reviewer.md +0 -35
  153. package/src/core/tools/exa/logger.ts +0 -56
@@ -1,7 +1,7 @@
1
1
  import { mkdir, rename, rm } from "node:fs/promises";
2
2
  import path from "node:path";
3
- import type { CreateFile, DeleteFile, RenameFile, TextDocumentEdit, TextEdit, WorkspaceEdit } from "./types.js";
4
- import { uriToFile } from "./utils.js";
3
+ import type { CreateFile, DeleteFile, RenameFile, TextDocumentEdit, TextEdit, WorkspaceEdit } from "./types";
4
+ import { uriToFile } from "./utils";
5
5
 
6
6
  // =============================================================================
7
7
  // Text Edit Application
@@ -1,13 +1,22 @@
1
1
  import * as fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import type { AgentTool } from "@oh-my-pi/pi-agent-core";
4
- import type { Theme } from "../../../modes/interactive/theme/theme.js";
5
- import { resolveToCwd } from "../path-utils.js";
6
- import { ensureFileOpen, getOrCreateClient, refreshFile, sendRequest } from "./client.js";
7
- import { getServerForFile, hasCapability, type LspConfig, loadConfig } from "./config.js";
8
- import { applyWorkspaceEdit } from "./edits.js";
9
- import { renderCall, renderResult } from "./render.js";
10
- import * as rustAnalyzer from "./rust-analyzer.js";
4
+ import type { Theme } from "../../../modes/interactive/theme/theme";
5
+ import { logger } from "../../logger";
6
+ import { resolveToCwd } from "../path-utils";
7
+ import {
8
+ ensureFileOpen,
9
+ getActiveClients,
10
+ getOrCreateClient,
11
+ type LspServerStatus,
12
+ refreshFile,
13
+ sendRequest,
14
+ setIdleTimeout,
15
+ } from "./client";
16
+ import { getServerForFile, getServersForFile, hasCapability, type LspConfig, loadConfig } from "./config";
17
+ import { applyTextEdits, applyWorkspaceEdit } from "./edits";
18
+ import { renderCall, renderResult } from "./render";
19
+ import * as rustAnalyzer from "./rust-analyzer";
11
20
  import {
12
21
  type CallHierarchyIncomingCall,
13
22
  type CallHierarchyItem,
@@ -25,8 +34,9 @@ import {
25
34
  lspSchema,
26
35
  type ServerConfig,
27
36
  type SymbolInformation,
37
+ type TextEdit,
28
38
  type WorkspaceEdit,
29
- } from "./types.js";
39
+ } from "./types";
30
40
  import {
31
41
  extractHoverText,
32
42
  fileToUri,
@@ -39,9 +49,69 @@ import {
39
49
  sleep,
40
50
  symbolKindToIcon,
41
51
  uriToFile,
42
- } from "./utils.js";
52
+ } from "./utils";
53
+
54
+ export type { LspServerStatus } from "./client";
55
+ export type { LspToolDetails } from "./types";
56
+
57
+ /** Result from warming up LSP servers */
58
+ export interface LspWarmupResult {
59
+ servers: Array<{
60
+ name: string;
61
+ status: "ready" | "error";
62
+ fileTypes: string[];
63
+ error?: string;
64
+ }>;
65
+ }
66
+
67
+ /**
68
+ * Warm up LSP servers for a directory by connecting to all detected servers.
69
+ * This should be called at startup to avoid cold-start delays.
70
+ *
71
+ * @param cwd - Working directory to detect and start servers for
72
+ * @returns Status of each server that was started
73
+ */
74
+ export async function warmupLspServers(cwd: string): Promise<LspWarmupResult> {
75
+ const config = loadConfig(cwd);
76
+ setIdleTimeout(config.idleTimeoutMs);
77
+ const servers: LspWarmupResult["servers"] = [];
78
+
79
+ // Start all detected servers in parallel
80
+ const results = await Promise.allSettled(
81
+ Object.entries(config.servers).map(async ([name, serverConfig]) => {
82
+ const client = await getOrCreateClient(serverConfig, cwd);
83
+ return { name, client, fileTypes: serverConfig.fileTypes };
84
+ }),
85
+ );
86
+
87
+ for (const result of results) {
88
+ if (result.status === "fulfilled") {
89
+ servers.push({
90
+ name: result.value.name,
91
+ status: "ready",
92
+ fileTypes: result.value.fileTypes,
93
+ });
94
+ } else {
95
+ // Extract server name from error if possible
96
+ const errorMsg = result.reason?.message ?? String(result.reason);
97
+ servers.push({
98
+ name: "unknown",
99
+ status: "error",
100
+ fileTypes: [],
101
+ error: errorMsg,
102
+ });
103
+ }
104
+ }
105
+
106
+ return { servers };
107
+ }
43
108
 
44
- export type { LspToolDetails } from "./types.js";
109
+ /**
110
+ * Get status of currently active LSP servers.
111
+ */
112
+ export function getLspStatus(): LspServerStatus[] {
113
+ return getActiveClients();
114
+ }
45
115
 
46
116
  // Cache config per cwd to avoid repeated file I/O
47
117
  const configCache = new Map<string, LspConfig>();
@@ -50,6 +120,7 @@ function getConfig(cwd: string): LspConfig {
50
120
  let config = configCache.get(cwd);
51
121
  if (!config) {
52
122
  config = loadConfig(cwd);
123
+ setIdleTimeout(config.idleTimeoutMs);
53
124
  configCache.set(cwd, config);
54
125
  }
55
126
  return config;
@@ -139,6 +210,304 @@ async function waitForDiagnostics(client: LspClient, uri: string, timeoutMs = 30
139
210
  return client.diagnostics.get(uri) ?? [];
140
211
  }
141
212
 
213
+ /** Project type detection result */
214
+ interface ProjectType {
215
+ type: "rust" | "typescript" | "go" | "python" | "unknown";
216
+ command?: string[];
217
+ description: string;
218
+ }
219
+
220
+ /** Detect project type from root markers */
221
+ function detectProjectType(cwd: string): ProjectType {
222
+ // Check for Rust (Cargo.toml)
223
+ if (fs.existsSync(path.join(cwd, "Cargo.toml"))) {
224
+ return { type: "rust", command: ["cargo", "check", "--message-format=short"], description: "Rust (cargo check)" };
225
+ }
226
+
227
+ // Check for TypeScript (tsconfig.json)
228
+ if (fs.existsSync(path.join(cwd, "tsconfig.json"))) {
229
+ return { type: "typescript", command: ["npx", "tsc", "--noEmit"], description: "TypeScript (tsc --noEmit)" };
230
+ }
231
+
232
+ // Check for Go (go.mod)
233
+ if (fs.existsSync(path.join(cwd, "go.mod"))) {
234
+ return { type: "go", command: ["go", "build", "./..."], description: "Go (go build)" };
235
+ }
236
+
237
+ // Check for Python (pyproject.toml or pyrightconfig.json)
238
+ if (fs.existsSync(path.join(cwd, "pyproject.toml")) || fs.existsSync(path.join(cwd, "pyrightconfig.json"))) {
239
+ return { type: "python", command: ["pyright"], description: "Python (pyright)" };
240
+ }
241
+
242
+ return { type: "unknown", description: "Unknown project type" };
243
+ }
244
+
245
+ /** Run workspace diagnostics command and parse output */
246
+ async function runWorkspaceDiagnostics(
247
+ cwd: string,
248
+ config: LspConfig,
249
+ ): Promise<{ output: string; projectType: ProjectType }> {
250
+ const projectType = detectProjectType(cwd);
251
+
252
+ // For Rust, use flycheck via rust-analyzer if available
253
+ if (projectType.type === "rust") {
254
+ const rustServer = getRustServer(config);
255
+ if (rustServer && hasCapability(rustServer[1], "flycheck")) {
256
+ const [_serverName, serverConfig] = rustServer;
257
+ try {
258
+ const client = await getOrCreateClient(serverConfig, cwd);
259
+ await rustAnalyzer.flycheck(client);
260
+
261
+ const collected: Array<{ filePath: string; diagnostic: Diagnostic }> = [];
262
+ for (const [diagUri, diags] of client.diagnostics.entries()) {
263
+ const relPath = path.relative(cwd, uriToFile(diagUri));
264
+ for (const diag of diags) {
265
+ collected.push({ filePath: relPath, diagnostic: diag });
266
+ }
267
+ }
268
+
269
+ if (collected.length === 0) {
270
+ return { output: "No issues found", projectType };
271
+ }
272
+
273
+ const summary = formatDiagnosticsSummary(collected.map((d) => d.diagnostic));
274
+ const formatted = collected.slice(0, 50).map((d) => formatDiagnostic(d.diagnostic, d.filePath));
275
+ const more = collected.length > 50 ? `\n ... and ${collected.length - 50} more` : "";
276
+ return { output: `${summary}:\n${formatted.map((f) => ` ${f}`).join("\n")}${more}`, projectType };
277
+ } catch (err) {
278
+ logger.debug("LSP diagnostics failed, falling back to shell", { error: String(err) });
279
+ // Fall through to shell command
280
+ }
281
+ }
282
+ }
283
+
284
+ // Fall back to shell command
285
+ if (!projectType.command) {
286
+ return {
287
+ output: `Cannot detect project type. Supported: Rust (Cargo.toml), TypeScript (tsconfig.json), Go (go.mod), Python (pyproject.toml)`,
288
+ projectType,
289
+ };
290
+ }
291
+
292
+ try {
293
+ const proc = Bun.spawn(projectType.command, {
294
+ cwd,
295
+ stdout: "pipe",
296
+ stderr: "pipe",
297
+ });
298
+
299
+ const [stdout, stderr] = await Promise.all([new Response(proc.stdout).text(), new Response(proc.stderr).text()]);
300
+ await proc.exited;
301
+
302
+ const combined = (stdout + stderr).trim();
303
+ if (!combined) {
304
+ return { output: "No issues found", projectType };
305
+ }
306
+
307
+ // Limit output length
308
+ const lines = combined.split("\n");
309
+ if (lines.length > 50) {
310
+ return { output: `${lines.slice(0, 50).join("\n")}\n... and ${lines.length - 50} more lines`, projectType };
311
+ }
312
+
313
+ return { output: combined, projectType };
314
+ } catch (e) {
315
+ return { output: `Failed to run ${projectType.command.join(" ")}: ${e}`, projectType };
316
+ }
317
+ }
318
+
319
+ /** Result from getDiagnosticsForFile */
320
+ export interface FileDiagnosticsResult {
321
+ /** Whether an LSP server was available for the file type */
322
+ available: boolean;
323
+ /** Name of the LSP server used (if available) */
324
+ serverName?: string;
325
+ /** Formatted diagnostic messages */
326
+ diagnostics: string[];
327
+ /** Summary string (e.g., "2 error(s), 1 warning(s)") */
328
+ summary: string;
329
+ /** Whether there are any errors (severity 1) */
330
+ hasErrors: boolean;
331
+ /** Whether there are any warnings (severity 2) */
332
+ hasWarnings: boolean;
333
+ }
334
+
335
+ /**
336
+ * Get LSP diagnostics for a file after it has been written.
337
+ * Queries all applicable language servers (e.g., TypeScript + Biome) and merges results.
338
+ *
339
+ * @param absolutePath - Absolute path to the file
340
+ * @param cwd - Working directory for LSP config resolution
341
+ * @param timeoutMs - Timeout for waiting for diagnostics (default: 5000ms)
342
+ * @returns Diagnostic results or null if no LSP server available
343
+ */
344
+ export async function getDiagnosticsForFile(
345
+ absolutePath: string,
346
+ cwd: string,
347
+ timeoutMs = 5000,
348
+ ): Promise<FileDiagnosticsResult> {
349
+ const config = getConfig(cwd);
350
+ const servers = getServersForFile(config, absolutePath);
351
+
352
+ if (servers.length === 0) {
353
+ return {
354
+ available: false,
355
+ diagnostics: [],
356
+ summary: "",
357
+ hasErrors: false,
358
+ hasWarnings: false,
359
+ };
360
+ }
361
+
362
+ const uri = fileToUri(absolutePath);
363
+ const relPath = path.relative(cwd, absolutePath);
364
+ const allDiagnostics: Diagnostic[] = [];
365
+ const serverNames: string[] = [];
366
+
367
+ // Query all applicable servers in parallel
368
+ const results = await Promise.allSettled(
369
+ servers.map(async ([serverName, serverConfig]) => {
370
+ const client = await getOrCreateClient(serverConfig, cwd);
371
+ await refreshFile(client, absolutePath);
372
+ const diagnostics = await waitForDiagnostics(client, uri, timeoutMs);
373
+ return { serverName, diagnostics };
374
+ }),
375
+ );
376
+
377
+ for (const result of results) {
378
+ if (result.status === "fulfilled") {
379
+ serverNames.push(result.value.serverName);
380
+ allDiagnostics.push(...result.value.diagnostics);
381
+ }
382
+ }
383
+
384
+ if (serverNames.length === 0) {
385
+ // All servers failed
386
+ return {
387
+ available: false,
388
+ diagnostics: [],
389
+ summary: "",
390
+ hasErrors: false,
391
+ hasWarnings: false,
392
+ };
393
+ }
394
+
395
+ if (allDiagnostics.length === 0) {
396
+ return {
397
+ available: true,
398
+ serverName: serverNames.join(", "),
399
+ diagnostics: [],
400
+ summary: "No issues",
401
+ hasErrors: false,
402
+ hasWarnings: false,
403
+ };
404
+ }
405
+
406
+ // Deduplicate diagnostics by range + message (different servers might report similar issues)
407
+ const seen = new Set<string>();
408
+ const uniqueDiagnostics: Diagnostic[] = [];
409
+ for (const d of allDiagnostics) {
410
+ const key = `${d.range.start.line}:${d.range.start.character}:${d.range.end.line}:${d.range.end.character}:${d.message}`;
411
+ if (!seen.has(key)) {
412
+ seen.add(key);
413
+ uniqueDiagnostics.push(d);
414
+ }
415
+ }
416
+
417
+ const formatted = uniqueDiagnostics.map((d) => formatDiagnostic(d, relPath));
418
+ const summary = formatDiagnosticsSummary(uniqueDiagnostics);
419
+ const hasErrors = uniqueDiagnostics.some((d) => d.severity === 1);
420
+ const hasWarnings = uniqueDiagnostics.some((d) => d.severity === 2);
421
+
422
+ return {
423
+ available: true,
424
+ serverName: serverNames.join(", "),
425
+ diagnostics: formatted,
426
+ summary,
427
+ hasErrors,
428
+ hasWarnings,
429
+ };
430
+ }
431
+
432
+ /** Result from formatFile */
433
+ export interface FileFormatResult {
434
+ /** Whether an LSP server with formatting support was available */
435
+ available: boolean;
436
+ /** Name of the LSP server used (if available) */
437
+ serverName?: string;
438
+ /** Whether formatting was applied */
439
+ formatted: boolean;
440
+ /** Error message if formatting failed */
441
+ error?: string;
442
+ }
443
+
444
+ /** Default formatting options for LSP */
445
+ const DEFAULT_FORMAT_OPTIONS = {
446
+ tabSize: 3,
447
+ insertSpaces: true,
448
+ trimTrailingWhitespace: true,
449
+ insertFinalNewline: true,
450
+ trimFinalNewlines: true,
451
+ };
452
+
453
+ /**
454
+ * Format a file using LSP.
455
+ * Uses the first available server that supports formatting.
456
+ *
457
+ * @param absolutePath - Absolute path to the file
458
+ * @param cwd - Working directory for LSP config resolution
459
+ * @returns Format result indicating success/failure
460
+ */
461
+ export async function formatFile(absolutePath: string, cwd: string): Promise<FileFormatResult> {
462
+ const config = getConfig(cwd);
463
+ const servers = getServersForFile(config, absolutePath);
464
+
465
+ if (servers.length === 0) {
466
+ return { available: false, formatted: false };
467
+ }
468
+
469
+ const uri = fileToUri(absolutePath);
470
+
471
+ // Try each server until one successfully formats
472
+ for (const [serverName, serverConfig] of servers) {
473
+ try {
474
+ const client = await getOrCreateClient(serverConfig, cwd);
475
+
476
+ // Check if server supports formatting
477
+ const caps = client.serverCapabilities;
478
+ if (!caps?.documentFormattingProvider) {
479
+ continue;
480
+ }
481
+
482
+ // Ensure file is open and synced
483
+ await ensureFileOpen(client, absolutePath);
484
+ await refreshFile(client, absolutePath);
485
+
486
+ // Request formatting
487
+ const edits = (await sendRequest(client, "textDocument/formatting", {
488
+ textDocument: { uri },
489
+ options: DEFAULT_FORMAT_OPTIONS,
490
+ })) as TextEdit[] | null;
491
+
492
+ if (!edits || edits.length === 0) {
493
+ // No changes needed - file already formatted
494
+ return { available: true, serverName, formatted: false };
495
+ }
496
+
497
+ // Apply the formatting edits
498
+ await applyTextEdits(absolutePath, edits);
499
+
500
+ // Notify LSP of the change so diagnostics update
501
+ await refreshFile(client, absolutePath);
502
+
503
+ return { available: true, serverName, formatted: true };
504
+ } catch {}
505
+ }
506
+
507
+ // No server could format
508
+ return { available: false, formatted: false };
509
+ }
510
+
142
511
  export function createLspTool(cwd: string): AgentTool<typeof lspSchema, LspToolDetails, Theme> {
143
512
  return {
144
513
  name: "lsp",
@@ -147,6 +516,7 @@ export function createLspTool(cwd: string): AgentTool<typeof lspSchema, LspToolD
147
516
 
148
517
  Standard operations:
149
518
  - diagnostics: Get errors/warnings for a file
519
+ - workspace_diagnostics: Check entire project for errors (uses tsc, cargo check, go build, etc.)
150
520
  - definition: Go to symbol definition
151
521
  - references: Find all references to a symbol
152
522
  - hover: Get type info and documentation
@@ -201,7 +571,21 @@ Rust-analyzer specific (require rust-analyzer):
201
571
  };
202
572
  }
203
573
 
204
- // Diagnostics can be batch or single-file
574
+ // Workspace diagnostics - check entire project
575
+ if (action === "workspace_diagnostics") {
576
+ const result = await runWorkspaceDiagnostics(cwd, config);
577
+ return {
578
+ content: [
579
+ {
580
+ type: "text",
581
+ text: `Workspace diagnostics (${result.projectType.description}):\n${result.output}`,
582
+ },
583
+ ],
584
+ details: { action, success: true },
585
+ };
586
+ }
587
+
588
+ // Diagnostics can be batch or single-file - queries all applicable servers
205
589
  if (action === "diagnostics") {
206
590
  const targets = files?.length ? files : file ? [file] : null;
207
591
  if (!targets) {
@@ -213,49 +597,67 @@ Rust-analyzer specific (require rust-analyzer):
213
597
 
214
598
  const detailed = Boolean(files?.length);
215
599
  const results: string[] = [];
216
- let lastServerName: string | undefined;
600
+ const allServerNames = new Set<string>();
217
601
 
218
602
  for (const target of targets) {
219
603
  const resolved = resolveToCwd(target, cwd);
220
- const serverInfo = getServerForFile(config, resolved);
221
- if (!serverInfo) {
604
+ const servers = getServersForFile(config, resolved);
605
+ if (servers.length === 0) {
222
606
  results.push(`✗ ${target}: No language server found`);
223
607
  continue;
224
608
  }
225
609
 
226
- const [serverName, serverConfig] = serverInfo;
227
- lastServerName = serverName;
228
-
229
- const client = await getOrCreateClient(serverConfig, cwd);
230
- await refreshFile(client, resolved);
231
-
232
610
  const uri = fileToUri(resolved);
233
- const diagnostics = await waitForDiagnostics(client, uri);
234
611
  const relPath = path.relative(cwd, resolved);
612
+ const allDiagnostics: Diagnostic[] = [];
613
+
614
+ // Query all applicable servers for this file
615
+ for (const [serverName, serverConfig] of servers) {
616
+ allServerNames.add(serverName);
617
+ try {
618
+ const client = await getOrCreateClient(serverConfig, cwd);
619
+ await refreshFile(client, resolved);
620
+ const diagnostics = await waitForDiagnostics(client, uri);
621
+ allDiagnostics.push(...diagnostics);
622
+ } catch {
623
+ // Server failed, continue with others
624
+ }
625
+ }
626
+
627
+ // Deduplicate diagnostics
628
+ const seen = new Set<string>();
629
+ const uniqueDiagnostics: Diagnostic[] = [];
630
+ for (const d of allDiagnostics) {
631
+ const key = `${d.range.start.line}:${d.range.start.character}:${d.range.end.line}:${d.range.end.character}:${d.message}`;
632
+ if (!seen.has(key)) {
633
+ seen.add(key);
634
+ uniqueDiagnostics.push(d);
635
+ }
636
+ }
235
637
 
236
638
  if (!detailed && targets.length === 1) {
237
- if (diagnostics.length === 0) {
639
+ if (uniqueDiagnostics.length === 0) {
238
640
  return {
239
641
  content: [{ type: "text", text: "No diagnostics" }],
240
- details: { action, serverName, success: true },
642
+ details: { action, serverName: Array.from(allServerNames).join(", "), success: true },
241
643
  };
242
644
  }
243
645
 
244
- const summary = formatDiagnosticsSummary(diagnostics);
245
- const formatted = diagnostics.map((d) => formatDiagnostic(d, relPath));
646
+ const summary = formatDiagnosticsSummary(uniqueDiagnostics);
647
+ const formatted = uniqueDiagnostics.map((d) => formatDiagnostic(d, relPath));
246
648
  const output = `${summary}:\n${formatted.map((f) => ` ${f}`).join("\n")}`;
247
649
  return {
248
650
  content: [{ type: "text", text: output }],
249
- details: { action, serverName, success: true },
651
+ details: { action, serverName: Array.from(allServerNames).join(", "), success: true },
250
652
  };
251
653
  }
252
654
 
253
- if (diagnostics.length === 0) {
655
+ if (uniqueDiagnostics.length === 0) {
254
656
  results.push(`✓ ${relPath}: no issues`);
255
657
  } else {
256
- const summary = formatDiagnosticsSummary(diagnostics);
658
+ const summary = formatDiagnosticsSummary(uniqueDiagnostics);
257
659
  results.push(`✗ ${relPath}: ${summary}`);
258
- for (const diag of diagnostics) {
660
+ for (const diag of uniqueDiagnostics) {
259
661
  results.push(` ${formatDiagnostic(diag, relPath)}`);
260
662
  }
261
663
  }
@@ -263,7 +665,7 @@ Rust-analyzer specific (require rust-analyzer):
263
665
 
264
666
  return {
265
667
  content: [{ type: "text", text: results.join("\n") }],
266
- details: { action, serverName: lastServerName, success: true },
668
+ details: { action, serverName: Array.from(allServerNames).join(", "), success: true },
267
669
  };
268
670
  }
269
671
 
@@ -11,8 +11,8 @@
11
11
  import type { AgentToolResult, RenderResultOptions } from "@oh-my-pi/pi-agent-core";
12
12
  import { Text } from "@oh-my-pi/pi-tui";
13
13
  import { highlight, supportsLanguage } from "cli-highlight";
14
- import type { Theme } from "../../../modes/interactive/theme/theme.js";
15
- import type { LspParams, LspToolDetails } from "./types.js";
14
+ import type { Theme } from "../../../modes/interactive/theme/theme";
15
+ import type { LspParams, LspToolDetails } from "./types";
16
16
 
17
17
  // =============================================================================
18
18
  // Tree Drawing Characters
@@ -1,6 +1,6 @@
1
- import { sendNotification, sendRequest } from "./client.js";
2
- import type { Diagnostic, ExpandMacroResult, LspClient, RelatedTest, Runnable, WorkspaceEdit } from "./types.js";
3
- import { fileToUri } from "./utils.js";
1
+ import { sendNotification, sendRequest } from "./client";
2
+ import type { Diagnostic, ExpandMacroResult, LspClient, RelatedTest, Runnable, WorkspaceEdit } from "./types";
3
+ import { fileToUri } from "./utils";
4
4
 
5
5
  /**
6
6
  * Wait for specified milliseconds.
@@ -10,6 +10,7 @@ export const lspSchema = Type.Object({
10
10
  [
11
11
  // Standard LSP operations
12
12
  Type.Literal("diagnostics"),
13
+ Type.Literal("workspace_diagnostics"),
13
14
  Type.Literal("references"),
14
15
  Type.Literal("definition"),
15
16
  Type.Literal("hover"),
@@ -336,6 +337,10 @@ export interface ServerConfig {
336
337
  settings?: Record<string, unknown>;
337
338
  disabled?: boolean;
338
339
  capabilities?: ServerCapabilities;
340
+ /** If true, this is a linter/formatter server (e.g., Biome) - used only for diagnostics/actions, not type intelligence */
341
+ isLinter?: boolean;
342
+ /** Resolved absolute path to the command binary (set during config loading) */
343
+ resolvedCommand?: string;
339
344
  }
340
345
 
341
346
  // =============================================================================
@@ -8,7 +8,7 @@ import type {
8
8
  SymbolKind,
9
9
  TextEdit,
10
10
  WorkspaceEdit,
11
- } from "./types.js";
11
+ } from "./types";
12
12
 
13
13
  // =============================================================================
14
14
  // Language Detection
@@ -1,6 +1,6 @@
1
1
  import type { AgentTool } from "@oh-my-pi/pi-agent-core";
2
2
  import { Type } from "@sinclair/typebox";
3
- import { resolveToCwd } from "./path-utils.js";
3
+ import { resolveToCwd } from "./path-utils";
4
4
 
5
5
  const notebookSchema = Type.Object({
6
6
  action: Type.Union([Type.Literal("edit"), Type.Literal("insert"), Type.Literal("delete")], {