@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.
- package/CHANGELOG.md +115 -1
- package/README.md +1 -1
- package/examples/custom-tools/subagent/index.ts +1 -1
- package/package.json +5 -3
- package/src/cli/args.ts +13 -6
- package/src/cli/file-processor.ts +3 -3
- package/src/cli/list-models.ts +2 -2
- package/src/cli/plugin-cli.ts +1 -1
- package/src/cli/session-picker.ts +2 -2
- package/src/cli.ts +1 -1
- package/src/config.ts +3 -3
- package/src/core/agent-session.ts +189 -29
- package/src/core/bash-executor.ts +50 -10
- package/src/core/compaction/branch-summarization.ts +5 -5
- package/src/core/compaction/compaction.ts +3 -3
- package/src/core/compaction/index.ts +3 -3
- package/src/core/custom-commands/bundled/review/index.ts +156 -0
- package/src/core/custom-commands/index.ts +15 -0
- package/src/core/custom-commands/loader.ts +232 -0
- package/src/core/custom-commands/types.ts +112 -0
- package/src/core/custom-tools/index.ts +3 -3
- package/src/core/custom-tools/loader.ts +10 -8
- package/src/core/custom-tools/types.ts +11 -6
- package/src/core/custom-tools/wrapper.ts +2 -1
- package/src/core/exec.ts +22 -12
- package/src/core/export-html/index.ts +5 -5
- package/src/core/file-mentions.ts +54 -0
- package/src/core/hooks/index.ts +5 -5
- package/src/core/hooks/loader.ts +21 -16
- package/src/core/hooks/runner.ts +6 -6
- package/src/core/hooks/tool-wrapper.ts +2 -2
- package/src/core/hooks/types.ts +12 -15
- package/src/core/index.ts +6 -6
- package/src/core/logger.ts +112 -0
- package/src/core/mcp/client.ts +3 -3
- package/src/core/mcp/config.ts +1 -1
- package/src/core/mcp/index.ts +12 -12
- package/src/core/mcp/loader.ts +2 -2
- package/src/core/mcp/manager.ts +6 -6
- package/src/core/mcp/tool-bridge.ts +3 -3
- package/src/core/mcp/transports/http.ts +1 -1
- package/src/core/mcp/transports/index.ts +2 -2
- package/src/core/mcp/transports/stdio.ts +1 -1
- package/src/core/messages.ts +22 -0
- package/src/core/model-registry.ts +2 -2
- package/src/core/model-resolver.ts +103 -2
- package/src/core/plugins/doctor.ts +1 -1
- package/src/core/plugins/index.ts +6 -6
- package/src/core/plugins/installer.ts +4 -4
- package/src/core/plugins/loader.ts +4 -9
- package/src/core/plugins/manager.ts +5 -5
- package/src/core/plugins/paths.ts +3 -3
- package/src/core/sdk.ts +127 -52
- package/src/core/session-manager.ts +123 -20
- package/src/core/settings-manager.ts +106 -22
- package/src/core/skills.ts +5 -5
- package/src/core/slash-commands.ts +60 -45
- package/src/core/system-prompt.ts +6 -6
- package/src/core/title-generator.ts +94 -0
- package/src/core/tools/bash.ts +33 -157
- package/src/core/tools/context.ts +2 -2
- package/src/core/tools/edit-diff.ts +5 -5
- package/src/core/tools/edit.ts +60 -9
- package/src/core/tools/exa/company.ts +3 -3
- package/src/core/tools/exa/index.ts +16 -17
- package/src/core/tools/exa/linkedin.ts +3 -3
- package/src/core/tools/exa/mcp-client.ts +9 -9
- package/src/core/tools/exa/render.ts +5 -5
- package/src/core/tools/exa/researcher.ts +3 -3
- package/src/core/tools/exa/search.ts +6 -5
- package/src/core/tools/exa/types.ts +5 -6
- package/src/core/tools/exa/websets.ts +3 -3
- package/src/core/tools/find.ts +3 -3
- package/src/core/tools/grep.ts +6 -5
- package/src/core/tools/index.ts +114 -40
- package/src/core/tools/ls.ts +4 -4
- package/src/core/tools/lsp/client.ts +204 -108
- package/src/core/tools/lsp/config.ts +709 -35
- package/src/core/tools/lsp/edits.ts +2 -2
- package/src/core/tools/lsp/index.ts +432 -30
- package/src/core/tools/lsp/render.ts +2 -2
- package/src/core/tools/lsp/rust-analyzer.ts +3 -3
- package/src/core/tools/lsp/types.ts +5 -0
- package/src/core/tools/lsp/utils.ts +1 -1
- package/src/core/tools/notebook.ts +1 -1
- package/src/core/tools/output.ts +175 -0
- package/src/core/tools/read.ts +7 -7
- package/src/core/tools/renderers.ts +92 -13
- package/src/core/tools/review.ts +268 -0
- package/src/core/tools/task/agents.ts +1 -1
- package/src/core/tools/task/bundled-agents/explore.md +1 -1
- package/src/core/tools/task/bundled-agents/reviewer.md +53 -38
- package/src/core/tools/task/discovery.ts +2 -2
- package/src/core/tools/task/executor.ts +145 -28
- package/src/core/tools/task/index.ts +78 -30
- package/src/core/tools/task/model-resolver.ts +72 -13
- package/src/core/tools/task/parallel.ts +1 -1
- package/src/core/tools/task/render.ts +219 -30
- package/src/core/tools/task/subprocess-tool-registry.ts +89 -0
- package/src/core/tools/task/types.ts +36 -2
- package/src/core/tools/web-fetch.ts +5 -3
- package/src/core/tools/web-search/auth.ts +1 -1
- package/src/core/tools/web-search/index.ts +17 -15
- package/src/core/tools/web-search/providers/anthropic.ts +2 -2
- package/src/core/tools/web-search/providers/exa.ts +3 -5
- package/src/core/tools/web-search/providers/perplexity.ts +1 -1
- package/src/core/tools/web-search/render.ts +3 -3
- package/src/core/tools/write.ts +70 -7
- package/src/index.ts +33 -17
- package/src/main.ts +60 -34
- package/src/migrations.ts +3 -3
- package/src/modes/index.ts +5 -5
- package/src/modes/interactive/components/armin.ts +1 -1
- package/src/modes/interactive/components/assistant-message.ts +1 -1
- package/src/modes/interactive/components/bash-execution.ts +4 -4
- package/src/modes/interactive/components/bordered-loader.ts +2 -2
- package/src/modes/interactive/components/branch-summary-message.ts +2 -2
- package/src/modes/interactive/components/compaction-summary-message.ts +2 -2
- package/src/modes/interactive/components/diff.ts +1 -1
- package/src/modes/interactive/components/dynamic-border.ts +1 -1
- package/src/modes/interactive/components/footer.ts +5 -5
- package/src/modes/interactive/components/hook-editor.ts +2 -2
- package/src/modes/interactive/components/hook-input.ts +2 -2
- package/src/modes/interactive/components/hook-message.ts +3 -3
- package/src/modes/interactive/components/hook-selector.ts +2 -2
- package/src/modes/interactive/components/model-selector.ts +341 -41
- package/src/modes/interactive/components/oauth-selector.ts +3 -3
- package/src/modes/interactive/components/plugin-settings.ts +4 -4
- package/src/modes/interactive/components/queue-mode-selector.ts +2 -2
- package/src/modes/interactive/components/session-selector.ts +24 -11
- package/src/modes/interactive/components/settings-defs.ts +51 -3
- package/src/modes/interactive/components/settings-selector.ts +13 -16
- package/src/modes/interactive/components/show-images-selector.ts +2 -2
- package/src/modes/interactive/components/theme-selector.ts +2 -2
- package/src/modes/interactive/components/thinking-selector.ts +2 -2
- package/src/modes/interactive/components/tool-execution.ts +44 -8
- package/src/modes/interactive/components/tree-selector.ts +5 -5
- package/src/modes/interactive/components/user-message-selector.ts +2 -2
- package/src/modes/interactive/components/user-message.ts +1 -1
- package/src/modes/interactive/components/welcome.ts +42 -5
- package/src/modes/interactive/interactive-mode.ts +169 -48
- package/src/modes/interactive/theme/theme.ts +8 -7
- package/src/modes/print-mode.ts +4 -3
- package/src/modes/rpc/rpc-client.ts +4 -4
- package/src/modes/rpc/rpc-mode.ts +21 -11
- package/src/modes/rpc/rpc-types.ts +3 -3
- package/src/utils/changelog.ts +2 -2
- package/src/utils/clipboard.ts +1 -1
- package/src/utils/shell-snapshot.ts +218 -0
- package/src/utils/shell.ts +93 -13
- package/src/utils/tools-manager.ts +1 -1
- package/examples/custom-tools/subagent/agents/reviewer.md +0 -35
- 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
|
|
4
|
-
import { uriToFile } from "./utils
|
|
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
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
600
|
+
const allServerNames = new Set<string>();
|
|
217
601
|
|
|
218
602
|
for (const target of targets) {
|
|
219
603
|
const resolved = resolveToCwd(target, cwd);
|
|
220
|
-
const
|
|
221
|
-
if (
|
|
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 (
|
|
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(
|
|
245
|
-
const formatted =
|
|
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 (
|
|
655
|
+
if (uniqueDiagnostics.length === 0) {
|
|
254
656
|
results.push(`✓ ${relPath}: no issues`);
|
|
255
657
|
} else {
|
|
256
|
-
const summary = formatDiagnosticsSummary(
|
|
658
|
+
const summary = formatDiagnosticsSummary(uniqueDiagnostics);
|
|
257
659
|
results.push(`✗ ${relPath}: ${summary}`);
|
|
258
|
-
for (const diag of
|
|
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:
|
|
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
|
|
15
|
-
import type { LspParams, LspToolDetails } from "./types
|
|
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
|
|
2
|
-
import type { Diagnostic, ExpandMacroResult, LspClient, RelatedTest, Runnable, WorkspaceEdit } from "./types
|
|
3
|
-
import { fileToUri } from "./utils
|
|
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
|
// =============================================================================
|
|
@@ -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
|
|
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")], {
|