@oh-my-pi/pi-coding-agent 1.338.0 → 1.341.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +60 -1
- package/package.json +3 -3
- package/src/cli/args.ts +8 -0
- package/src/core/agent-session.ts +32 -14
- package/src/core/export-html/index.ts +48 -15
- package/src/core/export-html/template.html +3 -11
- package/src/core/mcp/client.ts +43 -16
- package/src/core/mcp/config.ts +152 -6
- package/src/core/mcp/index.ts +6 -2
- package/src/core/mcp/loader.ts +30 -3
- package/src/core/mcp/manager.ts +69 -10
- package/src/core/mcp/types.ts +9 -3
- package/src/core/model-resolver.ts +101 -0
- package/src/core/sdk.ts +65 -18
- package/src/core/session-manager.ts +117 -14
- package/src/core/settings-manager.ts +107 -19
- package/src/core/title-generator.ts +94 -0
- package/src/core/tools/bash.ts +1 -2
- package/src/core/tools/edit-diff.ts +2 -2
- package/src/core/tools/edit.ts +43 -5
- package/src/core/tools/grep.ts +3 -2
- package/src/core/tools/index.ts +73 -13
- package/src/core/tools/lsp/client.ts +45 -20
- package/src/core/tools/lsp/config.ts +708 -34
- package/src/core/tools/lsp/index.ts +423 -23
- package/src/core/tools/lsp/types.ts +5 -0
- package/src/core/tools/task/bundled-agents/explore.md +1 -1
- package/src/core/tools/task/bundled-agents/reviewer.md +1 -1
- package/src/core/tools/task/model-resolver.ts +52 -3
- package/src/core/tools/write.ts +67 -4
- package/src/index.ts +5 -0
- package/src/main.ts +23 -2
- package/src/modes/interactive/components/model-selector.ts +96 -18
- package/src/modes/interactive/components/session-selector.ts +20 -7
- package/src/modes/interactive/components/settings-defs.ts +59 -2
- package/src/modes/interactive/components/settings-selector.ts +8 -11
- package/src/modes/interactive/components/tool-execution.ts +18 -0
- package/src/modes/interactive/components/tree-selector.ts +2 -2
- package/src/modes/interactive/components/welcome.ts +40 -3
- package/src/modes/interactive/interactive-mode.ts +87 -10
- package/src/core/export-html/vendor/highlight.min.js +0 -1213
- package/src/core/export-html/vendor/marked.min.js +0 -6
|
@@ -3,9 +3,17 @@ import path from "node:path";
|
|
|
3
3
|
import type { AgentTool } from "@oh-my-pi/pi-agent-core";
|
|
4
4
|
import type { Theme } from "../../../modes/interactive/theme/theme.js";
|
|
5
5
|
import { resolveToCwd } from "../path-utils.js";
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
import {
|
|
7
|
+
ensureFileOpen,
|
|
8
|
+
getActiveClients,
|
|
9
|
+
getOrCreateClient,
|
|
10
|
+
type LspServerStatus,
|
|
11
|
+
refreshFile,
|
|
12
|
+
sendRequest,
|
|
13
|
+
setIdleTimeout,
|
|
14
|
+
} from "./client.js";
|
|
15
|
+
import { getServerForFile, getServersForFile, hasCapability, type LspConfig, loadConfig } from "./config.js";
|
|
16
|
+
import { applyTextEdits, applyWorkspaceEdit } from "./edits.js";
|
|
9
17
|
import { renderCall, renderResult } from "./render.js";
|
|
10
18
|
import * as rustAnalyzer from "./rust-analyzer.js";
|
|
11
19
|
import {
|
|
@@ -25,6 +33,7 @@ import {
|
|
|
25
33
|
lspSchema,
|
|
26
34
|
type ServerConfig,
|
|
27
35
|
type SymbolInformation,
|
|
36
|
+
type TextEdit,
|
|
28
37
|
type WorkspaceEdit,
|
|
29
38
|
} from "./types.js";
|
|
30
39
|
import {
|
|
@@ -41,8 +50,68 @@ import {
|
|
|
41
50
|
uriToFile,
|
|
42
51
|
} from "./utils.js";
|
|
43
52
|
|
|
53
|
+
export type { LspServerStatus } from "./client.js";
|
|
44
54
|
export type { LspToolDetails } from "./types.js";
|
|
45
55
|
|
|
56
|
+
/** Result from warming up LSP servers */
|
|
57
|
+
export interface LspWarmupResult {
|
|
58
|
+
servers: Array<{
|
|
59
|
+
name: string;
|
|
60
|
+
status: "ready" | "error";
|
|
61
|
+
fileTypes: string[];
|
|
62
|
+
error?: string;
|
|
63
|
+
}>;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Warm up LSP servers for a directory by connecting to all detected servers.
|
|
68
|
+
* This should be called at startup to avoid cold-start delays.
|
|
69
|
+
*
|
|
70
|
+
* @param cwd - Working directory to detect and start servers for
|
|
71
|
+
* @returns Status of each server that was started
|
|
72
|
+
*/
|
|
73
|
+
export async function warmupLspServers(cwd: string): Promise<LspWarmupResult> {
|
|
74
|
+
const config = loadConfig(cwd);
|
|
75
|
+
setIdleTimeout(config.idleTimeoutMs);
|
|
76
|
+
const servers: LspWarmupResult["servers"] = [];
|
|
77
|
+
|
|
78
|
+
// Start all detected servers in parallel
|
|
79
|
+
const results = await Promise.allSettled(
|
|
80
|
+
Object.entries(config.servers).map(async ([name, serverConfig]) => {
|
|
81
|
+
const client = await getOrCreateClient(serverConfig, cwd);
|
|
82
|
+
return { name, client, fileTypes: serverConfig.fileTypes };
|
|
83
|
+
}),
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
for (const result of results) {
|
|
87
|
+
if (result.status === "fulfilled") {
|
|
88
|
+
servers.push({
|
|
89
|
+
name: result.value.name,
|
|
90
|
+
status: "ready",
|
|
91
|
+
fileTypes: result.value.fileTypes,
|
|
92
|
+
});
|
|
93
|
+
} else {
|
|
94
|
+
// Extract server name from error if possible
|
|
95
|
+
const errorMsg = result.reason?.message ?? String(result.reason);
|
|
96
|
+
servers.push({
|
|
97
|
+
name: "unknown",
|
|
98
|
+
status: "error",
|
|
99
|
+
fileTypes: [],
|
|
100
|
+
error: errorMsg,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return { servers };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Get status of currently active LSP servers.
|
|
110
|
+
*/
|
|
111
|
+
export function getLspStatus(): LspServerStatus[] {
|
|
112
|
+
return getActiveClients();
|
|
113
|
+
}
|
|
114
|
+
|
|
46
115
|
// Cache config per cwd to avoid repeated file I/O
|
|
47
116
|
const configCache = new Map<string, LspConfig>();
|
|
48
117
|
|
|
@@ -50,6 +119,7 @@ function getConfig(cwd: string): LspConfig {
|
|
|
50
119
|
let config = configCache.get(cwd);
|
|
51
120
|
if (!config) {
|
|
52
121
|
config = loadConfig(cwd);
|
|
122
|
+
setIdleTimeout(config.idleTimeoutMs);
|
|
53
123
|
configCache.set(cwd, config);
|
|
54
124
|
}
|
|
55
125
|
return config;
|
|
@@ -139,6 +209,303 @@ async function waitForDiagnostics(client: LspClient, uri: string, timeoutMs = 30
|
|
|
139
209
|
return client.diagnostics.get(uri) ?? [];
|
|
140
210
|
}
|
|
141
211
|
|
|
212
|
+
/** Project type detection result */
|
|
213
|
+
interface ProjectType {
|
|
214
|
+
type: "rust" | "typescript" | "go" | "python" | "unknown";
|
|
215
|
+
command?: string[];
|
|
216
|
+
description: string;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** Detect project type from root markers */
|
|
220
|
+
function detectProjectType(cwd: string): ProjectType {
|
|
221
|
+
// Check for Rust (Cargo.toml)
|
|
222
|
+
if (fs.existsSync(path.join(cwd, "Cargo.toml"))) {
|
|
223
|
+
return { type: "rust", command: ["cargo", "check", "--message-format=short"], description: "Rust (cargo check)" };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Check for TypeScript (tsconfig.json)
|
|
227
|
+
if (fs.existsSync(path.join(cwd, "tsconfig.json"))) {
|
|
228
|
+
return { type: "typescript", command: ["npx", "tsc", "--noEmit"], description: "TypeScript (tsc --noEmit)" };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Check for Go (go.mod)
|
|
232
|
+
if (fs.existsSync(path.join(cwd, "go.mod"))) {
|
|
233
|
+
return { type: "go", command: ["go", "build", "./..."], description: "Go (go build)" };
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Check for Python (pyproject.toml or pyrightconfig.json)
|
|
237
|
+
if (fs.existsSync(path.join(cwd, "pyproject.toml")) || fs.existsSync(path.join(cwd, "pyrightconfig.json"))) {
|
|
238
|
+
return { type: "python", command: ["pyright"], description: "Python (pyright)" };
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return { type: "unknown", description: "Unknown project type" };
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** Run workspace diagnostics command and parse output */
|
|
245
|
+
async function runWorkspaceDiagnostics(
|
|
246
|
+
cwd: string,
|
|
247
|
+
config: LspConfig,
|
|
248
|
+
): Promise<{ output: string; projectType: ProjectType }> {
|
|
249
|
+
const projectType = detectProjectType(cwd);
|
|
250
|
+
|
|
251
|
+
// For Rust, use flycheck via rust-analyzer if available
|
|
252
|
+
if (projectType.type === "rust") {
|
|
253
|
+
const rustServer = getRustServer(config);
|
|
254
|
+
if (rustServer && hasCapability(rustServer[1], "flycheck")) {
|
|
255
|
+
const [_serverName, serverConfig] = rustServer;
|
|
256
|
+
try {
|
|
257
|
+
const client = await getOrCreateClient(serverConfig, cwd);
|
|
258
|
+
await rustAnalyzer.flycheck(client);
|
|
259
|
+
|
|
260
|
+
const collected: Array<{ filePath: string; diagnostic: Diagnostic }> = [];
|
|
261
|
+
for (const [diagUri, diags] of client.diagnostics.entries()) {
|
|
262
|
+
const relPath = path.relative(cwd, uriToFile(diagUri));
|
|
263
|
+
for (const diag of diags) {
|
|
264
|
+
collected.push({ filePath: relPath, diagnostic: diag });
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (collected.length === 0) {
|
|
269
|
+
return { output: "No issues found", projectType };
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const summary = formatDiagnosticsSummary(collected.map((d) => d.diagnostic));
|
|
273
|
+
const formatted = collected.slice(0, 50).map((d) => formatDiagnostic(d.diagnostic, d.filePath));
|
|
274
|
+
const more = collected.length > 50 ? `\n ... and ${collected.length - 50} more` : "";
|
|
275
|
+
return { output: `${summary}:\n${formatted.map((f) => ` ${f}`).join("\n")}${more}`, projectType };
|
|
276
|
+
} catch (e) {
|
|
277
|
+
// Fall through to shell command
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Fall back to shell command
|
|
283
|
+
if (!projectType.command) {
|
|
284
|
+
return {
|
|
285
|
+
output: `Cannot detect project type. Supported: Rust (Cargo.toml), TypeScript (tsconfig.json), Go (go.mod), Python (pyproject.toml)`,
|
|
286
|
+
projectType,
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
try {
|
|
291
|
+
const proc = Bun.spawn(projectType.command, {
|
|
292
|
+
cwd,
|
|
293
|
+
stdout: "pipe",
|
|
294
|
+
stderr: "pipe",
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
const [stdout, stderr] = await Promise.all([new Response(proc.stdout).text(), new Response(proc.stderr).text()]);
|
|
298
|
+
await proc.exited;
|
|
299
|
+
|
|
300
|
+
const combined = (stdout + stderr).trim();
|
|
301
|
+
if (!combined) {
|
|
302
|
+
return { output: "No issues found", projectType };
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Limit output length
|
|
306
|
+
const lines = combined.split("\n");
|
|
307
|
+
if (lines.length > 50) {
|
|
308
|
+
return { output: lines.slice(0, 50).join("\n") + `\n... and ${lines.length - 50} more lines`, projectType };
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return { output: combined, projectType };
|
|
312
|
+
} catch (e) {
|
|
313
|
+
return { output: `Failed to run ${projectType.command.join(" ")}: ${e}`, projectType };
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/** Result from getDiagnosticsForFile */
|
|
318
|
+
export interface FileDiagnosticsResult {
|
|
319
|
+
/** Whether an LSP server was available for the file type */
|
|
320
|
+
available: boolean;
|
|
321
|
+
/** Name of the LSP server used (if available) */
|
|
322
|
+
serverName?: string;
|
|
323
|
+
/** Formatted diagnostic messages */
|
|
324
|
+
diagnostics: string[];
|
|
325
|
+
/** Summary string (e.g., "2 error(s), 1 warning(s)") */
|
|
326
|
+
summary: string;
|
|
327
|
+
/** Whether there are any errors (severity 1) */
|
|
328
|
+
hasErrors: boolean;
|
|
329
|
+
/** Whether there are any warnings (severity 2) */
|
|
330
|
+
hasWarnings: boolean;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Get LSP diagnostics for a file after it has been written.
|
|
335
|
+
* Queries all applicable language servers (e.g., TypeScript + Biome) and merges results.
|
|
336
|
+
*
|
|
337
|
+
* @param absolutePath - Absolute path to the file
|
|
338
|
+
* @param cwd - Working directory for LSP config resolution
|
|
339
|
+
* @param timeoutMs - Timeout for waiting for diagnostics (default: 5000ms)
|
|
340
|
+
* @returns Diagnostic results or null if no LSP server available
|
|
341
|
+
*/
|
|
342
|
+
export async function getDiagnosticsForFile(
|
|
343
|
+
absolutePath: string,
|
|
344
|
+
cwd: string,
|
|
345
|
+
timeoutMs = 5000,
|
|
346
|
+
): Promise<FileDiagnosticsResult> {
|
|
347
|
+
const config = getConfig(cwd);
|
|
348
|
+
const servers = getServersForFile(config, absolutePath);
|
|
349
|
+
|
|
350
|
+
if (servers.length === 0) {
|
|
351
|
+
return {
|
|
352
|
+
available: false,
|
|
353
|
+
diagnostics: [],
|
|
354
|
+
summary: "",
|
|
355
|
+
hasErrors: false,
|
|
356
|
+
hasWarnings: false,
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const uri = fileToUri(absolutePath);
|
|
361
|
+
const relPath = path.relative(cwd, absolutePath);
|
|
362
|
+
const allDiagnostics: Diagnostic[] = [];
|
|
363
|
+
const serverNames: string[] = [];
|
|
364
|
+
|
|
365
|
+
// Query all applicable servers in parallel
|
|
366
|
+
const results = await Promise.allSettled(
|
|
367
|
+
servers.map(async ([serverName, serverConfig]) => {
|
|
368
|
+
const client = await getOrCreateClient(serverConfig, cwd);
|
|
369
|
+
await refreshFile(client, absolutePath);
|
|
370
|
+
const diagnostics = await waitForDiagnostics(client, uri, timeoutMs);
|
|
371
|
+
return { serverName, diagnostics };
|
|
372
|
+
}),
|
|
373
|
+
);
|
|
374
|
+
|
|
375
|
+
for (const result of results) {
|
|
376
|
+
if (result.status === "fulfilled") {
|
|
377
|
+
serverNames.push(result.value.serverName);
|
|
378
|
+
allDiagnostics.push(...result.value.diagnostics);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
if (serverNames.length === 0) {
|
|
383
|
+
// All servers failed
|
|
384
|
+
return {
|
|
385
|
+
available: false,
|
|
386
|
+
diagnostics: [],
|
|
387
|
+
summary: "",
|
|
388
|
+
hasErrors: false,
|
|
389
|
+
hasWarnings: false,
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
if (allDiagnostics.length === 0) {
|
|
394
|
+
return {
|
|
395
|
+
available: true,
|
|
396
|
+
serverName: serverNames.join(", "),
|
|
397
|
+
diagnostics: [],
|
|
398
|
+
summary: "No issues",
|
|
399
|
+
hasErrors: false,
|
|
400
|
+
hasWarnings: false,
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Deduplicate diagnostics by range + message (different servers might report similar issues)
|
|
405
|
+
const seen = new Set<string>();
|
|
406
|
+
const uniqueDiagnostics: Diagnostic[] = [];
|
|
407
|
+
for (const d of allDiagnostics) {
|
|
408
|
+
const key = `${d.range.start.line}:${d.range.start.character}:${d.range.end.line}:${d.range.end.character}:${d.message}`;
|
|
409
|
+
if (!seen.has(key)) {
|
|
410
|
+
seen.add(key);
|
|
411
|
+
uniqueDiagnostics.push(d);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const formatted = uniqueDiagnostics.map((d) => formatDiagnostic(d, relPath));
|
|
416
|
+
const summary = formatDiagnosticsSummary(uniqueDiagnostics);
|
|
417
|
+
const hasErrors = uniqueDiagnostics.some((d) => d.severity === 1);
|
|
418
|
+
const hasWarnings = uniqueDiagnostics.some((d) => d.severity === 2);
|
|
419
|
+
|
|
420
|
+
return {
|
|
421
|
+
available: true,
|
|
422
|
+
serverName: serverNames.join(", "),
|
|
423
|
+
diagnostics: formatted,
|
|
424
|
+
summary,
|
|
425
|
+
hasErrors,
|
|
426
|
+
hasWarnings,
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/** Result from formatFile */
|
|
431
|
+
export interface FileFormatResult {
|
|
432
|
+
/** Whether an LSP server with formatting support was available */
|
|
433
|
+
available: boolean;
|
|
434
|
+
/** Name of the LSP server used (if available) */
|
|
435
|
+
serverName?: string;
|
|
436
|
+
/** Whether formatting was applied */
|
|
437
|
+
formatted: boolean;
|
|
438
|
+
/** Error message if formatting failed */
|
|
439
|
+
error?: string;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/** Default formatting options for LSP */
|
|
443
|
+
const DEFAULT_FORMAT_OPTIONS = {
|
|
444
|
+
tabSize: 3,
|
|
445
|
+
insertSpaces: true,
|
|
446
|
+
trimTrailingWhitespace: true,
|
|
447
|
+
insertFinalNewline: true,
|
|
448
|
+
trimFinalNewlines: true,
|
|
449
|
+
};
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Format a file using LSP.
|
|
453
|
+
* Uses the first available server that supports formatting.
|
|
454
|
+
*
|
|
455
|
+
* @param absolutePath - Absolute path to the file
|
|
456
|
+
* @param cwd - Working directory for LSP config resolution
|
|
457
|
+
* @returns Format result indicating success/failure
|
|
458
|
+
*/
|
|
459
|
+
export async function formatFile(absolutePath: string, cwd: string): Promise<FileFormatResult> {
|
|
460
|
+
const config = getConfig(cwd);
|
|
461
|
+
const servers = getServersForFile(config, absolutePath);
|
|
462
|
+
|
|
463
|
+
if (servers.length === 0) {
|
|
464
|
+
return { available: false, formatted: false };
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const uri = fileToUri(absolutePath);
|
|
468
|
+
|
|
469
|
+
// Try each server until one successfully formats
|
|
470
|
+
for (const [serverName, serverConfig] of servers) {
|
|
471
|
+
try {
|
|
472
|
+
const client = await getOrCreateClient(serverConfig, cwd);
|
|
473
|
+
|
|
474
|
+
// Check if server supports formatting
|
|
475
|
+
const caps = client.serverCapabilities;
|
|
476
|
+
if (!caps?.documentFormattingProvider) {
|
|
477
|
+
continue;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// Ensure file is open and synced
|
|
481
|
+
await ensureFileOpen(client, absolutePath);
|
|
482
|
+
await refreshFile(client, absolutePath);
|
|
483
|
+
|
|
484
|
+
// Request formatting
|
|
485
|
+
const edits = (await sendRequest(client, "textDocument/formatting", {
|
|
486
|
+
textDocument: { uri },
|
|
487
|
+
options: DEFAULT_FORMAT_OPTIONS,
|
|
488
|
+
})) as TextEdit[] | null;
|
|
489
|
+
|
|
490
|
+
if (!edits || edits.length === 0) {
|
|
491
|
+
// No changes needed - file already formatted
|
|
492
|
+
return { available: true, serverName, formatted: false };
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// Apply the formatting edits
|
|
496
|
+
await applyTextEdits(absolutePath, edits);
|
|
497
|
+
|
|
498
|
+
// Notify LSP of the change so diagnostics update
|
|
499
|
+
await refreshFile(client, absolutePath);
|
|
500
|
+
|
|
501
|
+
return { available: true, serverName, formatted: true };
|
|
502
|
+
} catch {}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// No server could format
|
|
506
|
+
return { available: false, formatted: false };
|
|
507
|
+
}
|
|
508
|
+
|
|
142
509
|
export function createLspTool(cwd: string): AgentTool<typeof lspSchema, LspToolDetails, Theme> {
|
|
143
510
|
return {
|
|
144
511
|
name: "lsp",
|
|
@@ -147,6 +514,7 @@ export function createLspTool(cwd: string): AgentTool<typeof lspSchema, LspToolD
|
|
|
147
514
|
|
|
148
515
|
Standard operations:
|
|
149
516
|
- diagnostics: Get errors/warnings for a file
|
|
517
|
+
- workspace_diagnostics: Check entire project for errors (uses tsc, cargo check, go build, etc.)
|
|
150
518
|
- definition: Go to symbol definition
|
|
151
519
|
- references: Find all references to a symbol
|
|
152
520
|
- hover: Get type info and documentation
|
|
@@ -201,7 +569,21 @@ Rust-analyzer specific (require rust-analyzer):
|
|
|
201
569
|
};
|
|
202
570
|
}
|
|
203
571
|
|
|
204
|
-
//
|
|
572
|
+
// Workspace diagnostics - check entire project
|
|
573
|
+
if (action === "workspace_diagnostics") {
|
|
574
|
+
const result = await runWorkspaceDiagnostics(cwd, config);
|
|
575
|
+
return {
|
|
576
|
+
content: [
|
|
577
|
+
{
|
|
578
|
+
type: "text",
|
|
579
|
+
text: `Workspace diagnostics (${result.projectType.description}):\n${result.output}`,
|
|
580
|
+
},
|
|
581
|
+
],
|
|
582
|
+
details: { action, success: true },
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// Diagnostics can be batch or single-file - queries all applicable servers
|
|
205
587
|
if (action === "diagnostics") {
|
|
206
588
|
const targets = files?.length ? files : file ? [file] : null;
|
|
207
589
|
if (!targets) {
|
|
@@ -213,49 +595,67 @@ Rust-analyzer specific (require rust-analyzer):
|
|
|
213
595
|
|
|
214
596
|
const detailed = Boolean(files?.length);
|
|
215
597
|
const results: string[] = [];
|
|
216
|
-
|
|
598
|
+
const allServerNames = new Set<string>();
|
|
217
599
|
|
|
218
600
|
for (const target of targets) {
|
|
219
601
|
const resolved = resolveToCwd(target, cwd);
|
|
220
|
-
const
|
|
221
|
-
if (
|
|
602
|
+
const servers = getServersForFile(config, resolved);
|
|
603
|
+
if (servers.length === 0) {
|
|
222
604
|
results.push(`✗ ${target}: No language server found`);
|
|
223
605
|
continue;
|
|
224
606
|
}
|
|
225
607
|
|
|
226
|
-
const [serverName, serverConfig] = serverInfo;
|
|
227
|
-
lastServerName = serverName;
|
|
228
|
-
|
|
229
|
-
const client = await getOrCreateClient(serverConfig, cwd);
|
|
230
|
-
await refreshFile(client, resolved);
|
|
231
|
-
|
|
232
608
|
const uri = fileToUri(resolved);
|
|
233
|
-
const diagnostics = await waitForDiagnostics(client, uri);
|
|
234
609
|
const relPath = path.relative(cwd, resolved);
|
|
610
|
+
const allDiagnostics: Diagnostic[] = [];
|
|
611
|
+
|
|
612
|
+
// Query all applicable servers for this file
|
|
613
|
+
for (const [serverName, serverConfig] of servers) {
|
|
614
|
+
allServerNames.add(serverName);
|
|
615
|
+
try {
|
|
616
|
+
const client = await getOrCreateClient(serverConfig, cwd);
|
|
617
|
+
await refreshFile(client, resolved);
|
|
618
|
+
const diagnostics = await waitForDiagnostics(client, uri);
|
|
619
|
+
allDiagnostics.push(...diagnostics);
|
|
620
|
+
} catch {
|
|
621
|
+
// Server failed, continue with others
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// Deduplicate diagnostics
|
|
626
|
+
const seen = new Set<string>();
|
|
627
|
+
const uniqueDiagnostics: Diagnostic[] = [];
|
|
628
|
+
for (const d of allDiagnostics) {
|
|
629
|
+
const key = `${d.range.start.line}:${d.range.start.character}:${d.range.end.line}:${d.range.end.character}:${d.message}`;
|
|
630
|
+
if (!seen.has(key)) {
|
|
631
|
+
seen.add(key);
|
|
632
|
+
uniqueDiagnostics.push(d);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
235
635
|
|
|
236
636
|
if (!detailed && targets.length === 1) {
|
|
237
|
-
if (
|
|
637
|
+
if (uniqueDiagnostics.length === 0) {
|
|
238
638
|
return {
|
|
239
639
|
content: [{ type: "text", text: "No diagnostics" }],
|
|
240
|
-
details: { action, serverName, success: true },
|
|
640
|
+
details: { action, serverName: Array.from(allServerNames).join(", "), success: true },
|
|
241
641
|
};
|
|
242
642
|
}
|
|
243
643
|
|
|
244
|
-
const summary = formatDiagnosticsSummary(
|
|
245
|
-
const formatted =
|
|
644
|
+
const summary = formatDiagnosticsSummary(uniqueDiagnostics);
|
|
645
|
+
const formatted = uniqueDiagnostics.map((d) => formatDiagnostic(d, relPath));
|
|
246
646
|
const output = `${summary}:\n${formatted.map((f) => ` ${f}`).join("\n")}`;
|
|
247
647
|
return {
|
|
248
648
|
content: [{ type: "text", text: output }],
|
|
249
|
-
details: { action, serverName, success: true },
|
|
649
|
+
details: { action, serverName: Array.from(allServerNames).join(", "), success: true },
|
|
250
650
|
};
|
|
251
651
|
}
|
|
252
652
|
|
|
253
|
-
if (
|
|
653
|
+
if (uniqueDiagnostics.length === 0) {
|
|
254
654
|
results.push(`✓ ${relPath}: no issues`);
|
|
255
655
|
} else {
|
|
256
|
-
const summary = formatDiagnosticsSummary(
|
|
656
|
+
const summary = formatDiagnosticsSummary(uniqueDiagnostics);
|
|
257
657
|
results.push(`✗ ${relPath}: ${summary}`);
|
|
258
|
-
for (const diag of
|
|
658
|
+
for (const diag of uniqueDiagnostics) {
|
|
259
659
|
results.push(` ${formatDiagnostic(diag, relPath)}`);
|
|
260
660
|
}
|
|
261
661
|
}
|
|
@@ -263,7 +663,7 @@ Rust-analyzer specific (require rust-analyzer):
|
|
|
263
663
|
|
|
264
664
|
return {
|
|
265
665
|
content: [{ type: "text", text: results.join("\n") }],
|
|
266
|
-
details: { action, serverName:
|
|
666
|
+
details: { action, serverName: Array.from(allServerNames).join(", "), success: true },
|
|
267
667
|
};
|
|
268
668
|
}
|
|
269
669
|
|
|
@@ -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
|
// =============================================================================
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: explore
|
|
3
3
|
description: Fast read-only codebase scout that returns compressed context for handoff
|
|
4
4
|
tools: read, grep, glob, ls, bash
|
|
5
|
-
model:
|
|
5
|
+
model: pi/smol, haiku, flash, mini
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
You are a file search specialist and codebase scout. Quickly investigate a codebase and return structured findings that another agent can use without re-reading everything.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: reviewer
|
|
3
3
|
description: Expert code reviewer for PRs and implementation changes
|
|
4
4
|
tools: read, grep, glob, ls, bash
|
|
5
|
-
model: gpt-5.2-codex, gpt-5.2, codex, gpt
|
|
5
|
+
model: pi/slow, gpt-5.2-codex, gpt-5.2, codex, gpt
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
You are an expert code reviewer. Analyze code changes and provide thorough reviews.
|
|
@@ -6,9 +6,14 @@
|
|
|
6
6
|
* - Fuzzy match: "opus" → "claude-opus-4-5"
|
|
7
7
|
* - Comma fallback: "gpt, opus" → tries gpt first, then opus
|
|
8
8
|
* - "default" → undefined (use system default)
|
|
9
|
+
* - "pi/default" → configured default model from settings
|
|
10
|
+
* - "pi/smol" → configured smol model from settings
|
|
9
11
|
*/
|
|
10
12
|
|
|
11
13
|
import { spawnSync } from "node:child_process";
|
|
14
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
15
|
+
import { homedir } from "node:os";
|
|
16
|
+
import { join } from "node:path";
|
|
12
17
|
|
|
13
18
|
/** pi command: 'pi.cmd' on Windows, 'pi' elsewhere */
|
|
14
19
|
const PI_CMD = process.platform === "win32" ? "pi.cmd" : "pi";
|
|
@@ -74,6 +79,42 @@ export function clearModelCache(): void {
|
|
|
74
79
|
cacheExpiry = 0;
|
|
75
80
|
}
|
|
76
81
|
|
|
82
|
+
/**
|
|
83
|
+
* Load model roles from settings file.
|
|
84
|
+
*/
|
|
85
|
+
function loadModelRoles(): Record<string, string> {
|
|
86
|
+
try {
|
|
87
|
+
const settingsPath = join(homedir(), ".pi", "agent", "settings.json");
|
|
88
|
+
if (!existsSync(settingsPath)) return {};
|
|
89
|
+
const content = readFileSync(settingsPath, "utf-8");
|
|
90
|
+
const settings = JSON.parse(content);
|
|
91
|
+
return settings.modelRoles ?? {};
|
|
92
|
+
} catch {
|
|
93
|
+
return {};
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Resolve a pi/<role> alias to a model string.
|
|
99
|
+
* Looks up the role in settings.modelRoles and returns the configured model.
|
|
100
|
+
* Returns undefined if the role isn't configured.
|
|
101
|
+
*/
|
|
102
|
+
function resolvePiAlias(role: string, availableModels: string[]): string | undefined {
|
|
103
|
+
const roles = loadModelRoles();
|
|
104
|
+
|
|
105
|
+
// Look up role in settings (case-insensitive)
|
|
106
|
+
const configured = roles[role] || roles[role.toLowerCase()];
|
|
107
|
+
if (!configured) return undefined;
|
|
108
|
+
|
|
109
|
+
// configured is in "provider/modelId" format, extract just the modelId for matching
|
|
110
|
+
const slashIdx = configured.indexOf("/");
|
|
111
|
+
if (slashIdx <= 0) return undefined;
|
|
112
|
+
|
|
113
|
+
const modelId = configured.slice(slashIdx + 1);
|
|
114
|
+
// Find in available models
|
|
115
|
+
return availableModels.find((m) => m.toLowerCase() === modelId.toLowerCase());
|
|
116
|
+
}
|
|
117
|
+
|
|
77
118
|
/**
|
|
78
119
|
* Resolve a fuzzy model pattern to an actual model name.
|
|
79
120
|
*
|
|
@@ -97,16 +138,24 @@ export function resolveModelPattern(pattern: string | undefined, availableModels
|
|
|
97
138
|
// Split by comma, try each pattern in order
|
|
98
139
|
const patterns = pattern
|
|
99
140
|
.split(",")
|
|
100
|
-
.map((p) => p.trim()
|
|
141
|
+
.map((p) => p.trim())
|
|
101
142
|
.filter(Boolean);
|
|
102
143
|
|
|
103
144
|
for (const p of patterns) {
|
|
145
|
+
// Handle pi/<role> aliases - looks up role in settings.modelRoles
|
|
146
|
+
if (p.toLowerCase().startsWith("pi/")) {
|
|
147
|
+
const role = p.slice(3); // Remove "pi/" prefix
|
|
148
|
+
const resolved = resolvePiAlias(role, models);
|
|
149
|
+
if (resolved) return resolved;
|
|
150
|
+
continue; // Role not configured, try next pattern
|
|
151
|
+
}
|
|
152
|
+
|
|
104
153
|
// Try exact match first
|
|
105
|
-
const exactMatch = models.find((m) => m.toLowerCase() === p);
|
|
154
|
+
const exactMatch = models.find((m) => m.toLowerCase() === p.toLowerCase());
|
|
106
155
|
if (exactMatch) return exactMatch;
|
|
107
156
|
|
|
108
157
|
// Try fuzzy match (substring)
|
|
109
|
-
const fuzzyMatch = models.find((m) => m.toLowerCase().includes(p));
|
|
158
|
+
const fuzzyMatch = models.find((m) => m.toLowerCase().includes(p.toLowerCase()));
|
|
110
159
|
if (fuzzyMatch) return fuzzyMatch;
|
|
111
160
|
}
|
|
112
161
|
|