@oh-my-pi/pi-coding-agent 3.13.1337 → 3.15.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 +88 -0
- package/docs/theme.md +38 -5
- package/examples/sdk/11-sessions.ts +2 -2
- package/package.json +7 -4
- package/src/cli/file-processor.ts +51 -2
- package/src/cli/plugin-cli.ts +25 -19
- package/src/cli/update-cli.ts +4 -3
- package/src/core/agent-session.ts +31 -4
- package/src/core/compaction/branch-summarization.ts +4 -32
- package/src/core/compaction/compaction.ts +6 -84
- package/src/core/compaction/utils.ts +2 -3
- package/src/core/custom-tools/types.ts +2 -0
- package/src/core/export-html/index.ts +1 -1
- package/src/core/hooks/index.ts +1 -1
- package/src/core/hooks/tool-wrapper.ts +0 -1
- package/src/core/hooks/types.ts +2 -2
- package/src/core/plugins/doctor.ts +9 -1
- package/src/core/sdk.ts +2 -1
- package/src/core/session-manager.ts +552 -41
- package/src/core/settings-manager.ts +174 -0
- package/src/core/system-prompt.ts +9 -14
- package/src/core/title-generator.ts +2 -8
- package/src/core/tools/ask.ts +19 -37
- package/src/core/tools/bash.ts +2 -37
- package/src/core/tools/edit.ts +2 -9
- package/src/core/tools/exa/render.ts +52 -48
- package/src/core/tools/find.ts +10 -8
- package/src/core/tools/grep.ts +45 -17
- package/src/core/tools/ls.ts +22 -2
- package/src/core/tools/lsp/clients/biome-client.ts +207 -0
- package/src/core/tools/lsp/clients/index.ts +49 -0
- package/src/core/tools/lsp/clients/lsp-linter-client.ts +98 -0
- package/src/core/tools/lsp/config.ts +3 -0
- package/src/core/tools/lsp/index.ts +107 -55
- package/src/core/tools/lsp/render.ts +192 -79
- package/src/core/tools/lsp/types.ts +27 -0
- package/src/core/tools/lsp/utils.ts +62 -22
- package/src/core/tools/notebook.ts +9 -1
- package/src/core/tools/output.ts +37 -14
- package/src/core/tools/read.ts +349 -34
- package/src/core/tools/renderers.ts +290 -89
- package/src/core/tools/review.ts +12 -5
- package/src/core/tools/task/agents.ts +5 -5
- package/src/core/tools/task/commands.ts +3 -3
- package/src/core/tools/task/executor.ts +33 -1
- package/src/core/tools/task/index.ts +93 -6
- package/src/core/tools/task/render.ts +147 -66
- package/src/core/tools/task/types.ts +14 -9
- package/src/core/tools/web-fetch.ts +242 -103
- package/src/core/tools/web-search/index.ts +64 -20
- package/src/core/tools/web-search/providers/exa.ts +68 -172
- package/src/core/tools/web-search/render.ts +264 -74
- package/src/core/tools/write.ts +2 -8
- package/src/main.ts +10 -6
- package/src/modes/cleanup.ts +23 -0
- package/src/modes/index.ts +9 -4
- package/src/modes/interactive/components/bash-execution.ts +6 -3
- package/src/modes/interactive/components/branch-summary-message.ts +1 -1
- package/src/modes/interactive/components/compaction-summary-message.ts +1 -1
- package/src/modes/interactive/components/dynamic-border.ts +1 -1
- package/src/modes/interactive/components/extensions/extension-dashboard.ts +4 -5
- package/src/modes/interactive/components/extensions/extension-list.ts +18 -16
- package/src/modes/interactive/components/extensions/inspector-panel.ts +8 -8
- package/src/modes/interactive/components/hook-message.ts +2 -2
- package/src/modes/interactive/components/hook-selector.ts +1 -1
- package/src/modes/interactive/components/model-selector.ts +22 -9
- package/src/modes/interactive/components/oauth-selector.ts +20 -4
- package/src/modes/interactive/components/plugin-settings.ts +4 -2
- package/src/modes/interactive/components/session-selector.ts +9 -6
- package/src/modes/interactive/components/settings-defs.ts +285 -1
- package/src/modes/interactive/components/settings-selector.ts +176 -3
- package/src/modes/interactive/components/status-line/index.ts +4 -0
- package/src/modes/interactive/components/status-line/presets.ts +94 -0
- package/src/modes/interactive/components/status-line/segments.ts +350 -0
- package/src/modes/interactive/components/status-line/separators.ts +55 -0
- package/src/modes/interactive/components/status-line/types.ts +81 -0
- package/src/modes/interactive/components/status-line-segment-editor.ts +357 -0
- package/src/modes/interactive/components/status-line.ts +169 -233
- package/src/modes/interactive/components/tool-execution.ts +446 -211
- package/src/modes/interactive/components/tree-selector.ts +17 -6
- package/src/modes/interactive/components/ttsr-notification.ts +4 -4
- package/src/modes/interactive/components/welcome.ts +27 -19
- package/src/modes/interactive/interactive-mode.ts +98 -13
- package/src/modes/interactive/theme/dark.json +3 -2
- package/src/modes/interactive/theme/defaults/dark-arctic.json +111 -0
- package/src/modes/interactive/theme/defaults/dark-catppuccin.json +106 -0
- package/src/modes/interactive/theme/defaults/dark-cyberpunk.json +109 -0
- package/src/modes/interactive/theme/defaults/dark-dracula.json +105 -0
- package/src/modes/interactive/theme/defaults/dark-forest.json +103 -0
- package/src/modes/interactive/theme/defaults/dark-github.json +112 -0
- package/src/modes/interactive/theme/defaults/dark-gruvbox.json +119 -0
- package/src/modes/interactive/theme/defaults/dark-monochrome.json +101 -0
- package/src/modes/interactive/theme/defaults/dark-monokai.json +105 -0
- package/src/modes/interactive/theme/defaults/dark-nord.json +104 -0
- package/src/modes/interactive/theme/defaults/dark-ocean.json +108 -0
- package/src/modes/interactive/theme/defaults/dark-one.json +107 -0
- package/src/modes/interactive/theme/defaults/dark-retro.json +99 -0
- package/src/modes/interactive/theme/defaults/dark-rose-pine.json +95 -0
- package/src/modes/interactive/theme/defaults/dark-solarized.json +96 -0
- package/src/modes/interactive/theme/defaults/dark-sunset.json +106 -0
- package/src/modes/interactive/theme/defaults/dark-synthwave.json +102 -0
- package/src/modes/interactive/theme/defaults/dark-tokyo-night.json +108 -0
- package/src/modes/interactive/theme/defaults/index.ts +67 -0
- package/src/modes/interactive/theme/defaults/light-arctic.json +106 -0
- package/src/modes/interactive/theme/defaults/light-catppuccin.json +105 -0
- package/src/modes/interactive/theme/defaults/light-cyberpunk.json +103 -0
- package/src/modes/interactive/theme/defaults/light-forest.json +107 -0
- package/src/modes/interactive/theme/defaults/light-github.json +114 -0
- package/src/modes/interactive/theme/defaults/light-gruvbox.json +115 -0
- package/src/modes/interactive/theme/defaults/light-monochrome.json +100 -0
- package/src/modes/interactive/theme/defaults/light-ocean.json +106 -0
- package/src/modes/interactive/theme/defaults/light-one.json +105 -0
- package/src/modes/interactive/theme/defaults/light-retro.json +105 -0
- package/src/modes/interactive/theme/defaults/light-solarized.json +101 -0
- package/src/modes/interactive/theme/defaults/light-sunset.json +106 -0
- package/src/modes/interactive/theme/defaults/light-synthwave.json +105 -0
- package/src/modes/interactive/theme/defaults/light-tokyo-night.json +118 -0
- package/src/modes/interactive/theme/light.json +3 -2
- package/src/modes/interactive/theme/theme-schema.json +120 -4
- package/src/modes/interactive/theme/theme.ts +1228 -14
- package/src/prompts/branch-summary-preamble.md +3 -0
- package/src/prompts/branch-summary.md +28 -0
- package/src/prompts/compaction-summary.md +34 -0
- package/src/prompts/compaction-turn-prefix.md +16 -0
- package/src/prompts/compaction-update-summary.md +41 -0
- package/src/prompts/init.md +30 -0
- package/src/{core/tools/task/bundled-agents → prompts}/reviewer.md +6 -0
- package/src/prompts/summarization-system.md +3 -0
- package/src/prompts/system-prompt.md +27 -0
- package/src/{core/tools/task/bundled-agents → prompts}/task.md +2 -0
- package/src/prompts/title-system.md +8 -0
- package/src/prompts/tools/ask.md +24 -0
- package/src/prompts/tools/bash.md +23 -0
- package/src/prompts/tools/edit.md +9 -0
- package/src/prompts/tools/find.md +6 -0
- package/src/prompts/tools/grep.md +12 -0
- package/src/prompts/tools/lsp.md +14 -0
- package/src/prompts/tools/output.md +23 -0
- package/src/prompts/tools/read.md +25 -0
- package/src/prompts/tools/web-fetch.md +8 -0
- package/src/prompts/tools/web-search.md +10 -0
- package/src/prompts/tools/write.md +10 -0
- package/src/commands/init.md +0 -20
- /package/src/{core/tools/task/bundled-commands → prompts}/architect-plan.md +0 -0
- /package/src/{core/tools/task/bundled-agents → prompts}/browser.md +0 -0
- /package/src/{core/tools/task/bundled-agents → prompts}/explore.md +0 -0
- /package/src/{core/tools/task/bundled-commands → prompts}/implement-with-critic.md +0 -0
- /package/src/{core/tools/task/bundled-commands → prompts}/implement.md +0 -0
- /package/src/{core/tools/task/bundled-agents → prompts}/plan.md +0 -0
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Biome CLI-based linter client.
|
|
3
|
+
* Uses Biome's CLI with JSON output instead of LSP (which has stale diagnostics issues).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import type { Diagnostic, DiagnosticSeverity, LinterClient, ServerConfig } from "../types";
|
|
8
|
+
|
|
9
|
+
// =============================================================================
|
|
10
|
+
// Biome JSON Output Types
|
|
11
|
+
// =============================================================================
|
|
12
|
+
|
|
13
|
+
interface BiomeJsonOutput {
|
|
14
|
+
diagnostics: BiomeDiagnostic[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface BiomeDiagnostic {
|
|
18
|
+
category: string; // e.g., "lint/correctness/noUnusedVariables"
|
|
19
|
+
severity: "error" | "warning" | "info" | "hint";
|
|
20
|
+
description: string;
|
|
21
|
+
location?: {
|
|
22
|
+
path?: { file: string };
|
|
23
|
+
span?: [number, number]; // [startOffset, endOffset] in bytes
|
|
24
|
+
sourceCode?: string;
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// =============================================================================
|
|
29
|
+
// Helpers
|
|
30
|
+
// =============================================================================
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Convert byte offset to line:column using source code.
|
|
34
|
+
*/
|
|
35
|
+
function offsetToPosition(source: string, offset: number): { line: number; column: number } {
|
|
36
|
+
let line = 1;
|
|
37
|
+
let column = 1;
|
|
38
|
+
let byteIndex = 0;
|
|
39
|
+
|
|
40
|
+
for (const ch of source) {
|
|
41
|
+
const byteLen = Buffer.byteLength(ch);
|
|
42
|
+
if (byteIndex + byteLen > offset) {
|
|
43
|
+
break;
|
|
44
|
+
}
|
|
45
|
+
if (ch === "\n") {
|
|
46
|
+
line++;
|
|
47
|
+
column = 1;
|
|
48
|
+
} else {
|
|
49
|
+
column++;
|
|
50
|
+
}
|
|
51
|
+
byteIndex += byteLen;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return { line, column };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Parse Biome severity to LSP DiagnosticSeverity.
|
|
59
|
+
*/
|
|
60
|
+
function parseSeverity(severity: string): DiagnosticSeverity {
|
|
61
|
+
switch (severity) {
|
|
62
|
+
case "error":
|
|
63
|
+
return 1;
|
|
64
|
+
case "warning":
|
|
65
|
+
return 2;
|
|
66
|
+
case "info":
|
|
67
|
+
return 3;
|
|
68
|
+
case "hint":
|
|
69
|
+
return 4;
|
|
70
|
+
default:
|
|
71
|
+
return 2;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Run a Biome CLI command.
|
|
77
|
+
*/
|
|
78
|
+
async function runBiome(
|
|
79
|
+
args: string[],
|
|
80
|
+
cwd: string,
|
|
81
|
+
resolvedCommand?: string,
|
|
82
|
+
): Promise<{ stdout: string; stderr: string; success: boolean }> {
|
|
83
|
+
const command = resolvedCommand ?? "biome";
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
const proc = Bun.spawn([command, ...args], {
|
|
87
|
+
cwd,
|
|
88
|
+
stdout: "pipe",
|
|
89
|
+
stderr: "pipe",
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const [stdout, stderr] = await Promise.all([new Response(proc.stdout).text(), new Response(proc.stderr).text()]);
|
|
93
|
+
const exitCode = await proc.exited;
|
|
94
|
+
|
|
95
|
+
return { stdout, stderr, success: exitCode === 0 };
|
|
96
|
+
} catch (err) {
|
|
97
|
+
return { stdout: "", stderr: String(err), success: false };
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// =============================================================================
|
|
102
|
+
// Biome Client
|
|
103
|
+
// =============================================================================
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Biome CLI-based linter client.
|
|
107
|
+
* Parses Biome's --reporter=json output into LSP Diagnostic format.
|
|
108
|
+
*/
|
|
109
|
+
export class BiomeClient implements LinterClient {
|
|
110
|
+
private config: ServerConfig;
|
|
111
|
+
private cwd: string;
|
|
112
|
+
|
|
113
|
+
constructor(config: ServerConfig, cwd: string) {
|
|
114
|
+
this.config = config;
|
|
115
|
+
this.cwd = cwd;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async format(filePath: string, content: string): Promise<string> {
|
|
119
|
+
// Write content to file first
|
|
120
|
+
await Bun.write(filePath, content);
|
|
121
|
+
|
|
122
|
+
// Run biome format --write
|
|
123
|
+
const result = await runBiome(["format", "--write", filePath], this.cwd, this.config.resolvedCommand);
|
|
124
|
+
|
|
125
|
+
if (result.success) {
|
|
126
|
+
// Read back formatted content
|
|
127
|
+
return await Bun.file(filePath).text();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Format failed, return original
|
|
131
|
+
return content;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async lint(filePath: string): Promise<Diagnostic[]> {
|
|
135
|
+
// Run biome lint with JSON reporter
|
|
136
|
+
const result = await runBiome(["lint", "--reporter=json", filePath], this.cwd, this.config.resolvedCommand);
|
|
137
|
+
|
|
138
|
+
return this.parseJsonOutput(result.stdout, filePath);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Parse Biome's JSON output into LSP Diagnostics.
|
|
143
|
+
*/
|
|
144
|
+
private parseJsonOutput(jsonOutput: string, targetFile: string): Diagnostic[] {
|
|
145
|
+
const diagnostics: Diagnostic[] = [];
|
|
146
|
+
|
|
147
|
+
try {
|
|
148
|
+
const parsed: BiomeJsonOutput = JSON.parse(jsonOutput);
|
|
149
|
+
|
|
150
|
+
for (const diag of parsed.diagnostics) {
|
|
151
|
+
const location = diag.location;
|
|
152
|
+
if (!location?.path?.file) continue;
|
|
153
|
+
|
|
154
|
+
// Resolve file path
|
|
155
|
+
const diagFile = path.isAbsolute(location.path.file)
|
|
156
|
+
? location.path.file
|
|
157
|
+
: path.join(this.cwd, location.path.file);
|
|
158
|
+
|
|
159
|
+
// Only include diagnostics for the target file
|
|
160
|
+
if (path.resolve(diagFile) !== path.resolve(targetFile)) {
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Convert byte offset to line:column
|
|
165
|
+
let startLine = 1;
|
|
166
|
+
let startColumn = 1;
|
|
167
|
+
let endLine = 1;
|
|
168
|
+
let endColumn = 1;
|
|
169
|
+
|
|
170
|
+
if (location.span && location.sourceCode) {
|
|
171
|
+
const startPos = offsetToPosition(location.sourceCode, location.span[0]);
|
|
172
|
+
const endPos = offsetToPosition(location.sourceCode, location.span[1]);
|
|
173
|
+
startLine = startPos.line;
|
|
174
|
+
startColumn = startPos.column;
|
|
175
|
+
endLine = endPos.line;
|
|
176
|
+
endColumn = endPos.column;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
diagnostics.push({
|
|
180
|
+
range: {
|
|
181
|
+
start: { line: startLine - 1, character: startColumn - 1 },
|
|
182
|
+
end: { line: endLine - 1, character: endColumn - 1 },
|
|
183
|
+
},
|
|
184
|
+
severity: parseSeverity(diag.severity),
|
|
185
|
+
message: diag.description,
|
|
186
|
+
source: "biome",
|
|
187
|
+
code: diag.category,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
} catch {
|
|
191
|
+
// JSON parse failed, return empty
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return diagnostics;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
dispose(): void {
|
|
198
|
+
// Nothing to dispose for CLI client
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Factory function to create a Biome client.
|
|
204
|
+
*/
|
|
205
|
+
export function createBiomeClient(config: ServerConfig, cwd: string): LinterClient {
|
|
206
|
+
return new BiomeClient(config, cwd);
|
|
207
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Linter client implementations.
|
|
3
|
+
*
|
|
4
|
+
* The LinterClient interface provides a common API for formatters and linters.
|
|
5
|
+
* Different implementations can use LSP protocol, CLI tools, or other mechanisms.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export { BiomeClient, createBiomeClient } from "./biome-client";
|
|
9
|
+
export { createLspLinterClient, LspLinterClient } from "./lsp-linter-client";
|
|
10
|
+
|
|
11
|
+
import type { LinterClient, ServerConfig } from "../types";
|
|
12
|
+
import { createLspLinterClient } from "./lsp-linter-client";
|
|
13
|
+
|
|
14
|
+
// Cache of linter clients by server name + cwd
|
|
15
|
+
const clientCache = new Map<string, LinterClient>();
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Get or create a linter client for a server configuration.
|
|
19
|
+
* Uses the server's custom factory if provided, otherwise falls back to LSP.
|
|
20
|
+
*/
|
|
21
|
+
export function getLinterClient(serverName: string, config: ServerConfig, cwd: string): LinterClient {
|
|
22
|
+
const key = `${serverName}:${cwd}`;
|
|
23
|
+
|
|
24
|
+
let client = clientCache.get(key);
|
|
25
|
+
if (client) {
|
|
26
|
+
return client;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Use custom factory if provided
|
|
30
|
+
if (config.createClient) {
|
|
31
|
+
client = config.createClient(config, cwd);
|
|
32
|
+
} else {
|
|
33
|
+
// Default to LSP
|
|
34
|
+
client = createLspLinterClient(config, cwd);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
clientCache.set(key, client);
|
|
38
|
+
return client;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Clear all cached linter clients.
|
|
43
|
+
*/
|
|
44
|
+
export function clearLinterClientCache(): void {
|
|
45
|
+
for (const client of clientCache.values()) {
|
|
46
|
+
client.dispose?.();
|
|
47
|
+
}
|
|
48
|
+
clientCache.clear();
|
|
49
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LSP-based linter client.
|
|
3
|
+
* Uses the Language Server Protocol for formatting and diagnostics.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { getOrCreateClient, notifySaved, sendRequest, syncContent } from "../client";
|
|
7
|
+
import { applyTextEditsToString } from "../edits";
|
|
8
|
+
import type { Diagnostic, LinterClient, LspClient, ServerConfig, TextEdit } from "../types";
|
|
9
|
+
import { fileToUri } from "../utils";
|
|
10
|
+
|
|
11
|
+
/** Default formatting options for LSP */
|
|
12
|
+
const DEFAULT_FORMAT_OPTIONS = {
|
|
13
|
+
tabSize: 3,
|
|
14
|
+
insertSpaces: true,
|
|
15
|
+
trimTrailingWhitespace: true,
|
|
16
|
+
insertFinalNewline: true,
|
|
17
|
+
trimFinalNewlines: true,
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* LSP-based linter client implementation.
|
|
22
|
+
* Wraps the existing LSP client infrastructure.
|
|
23
|
+
*/
|
|
24
|
+
export class LspLinterClient implements LinterClient {
|
|
25
|
+
private config: ServerConfig;
|
|
26
|
+
private cwd: string;
|
|
27
|
+
private client: LspClient | null = null;
|
|
28
|
+
|
|
29
|
+
constructor(config: ServerConfig, cwd: string) {
|
|
30
|
+
this.config = config;
|
|
31
|
+
this.cwd = cwd;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
private async getClient(): Promise<LspClient> {
|
|
35
|
+
if (!this.client) {
|
|
36
|
+
this.client = await getOrCreateClient(this.config, this.cwd);
|
|
37
|
+
}
|
|
38
|
+
return this.client;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async format(filePath: string, content: string): Promise<string> {
|
|
42
|
+
const client = await this.getClient();
|
|
43
|
+
const uri = fileToUri(filePath);
|
|
44
|
+
|
|
45
|
+
// Sync content to LSP
|
|
46
|
+
await syncContent(client, filePath, content);
|
|
47
|
+
|
|
48
|
+
// Check if server supports formatting
|
|
49
|
+
const caps = client.serverCapabilities;
|
|
50
|
+
if (!caps?.documentFormattingProvider) {
|
|
51
|
+
return content;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Request formatting
|
|
55
|
+
const edits = (await sendRequest(client, "textDocument/formatting", {
|
|
56
|
+
textDocument: { uri },
|
|
57
|
+
options: DEFAULT_FORMAT_OPTIONS,
|
|
58
|
+
})) as TextEdit[] | null;
|
|
59
|
+
|
|
60
|
+
if (!edits || edits.length === 0) {
|
|
61
|
+
return content;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return applyTextEditsToString(content, edits);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async lint(filePath: string): Promise<Diagnostic[]> {
|
|
68
|
+
const client = await this.getClient();
|
|
69
|
+
const uri = fileToUri(filePath);
|
|
70
|
+
|
|
71
|
+
// Notify that file was saved to trigger diagnostics
|
|
72
|
+
await notifySaved(client, filePath);
|
|
73
|
+
|
|
74
|
+
// Wait for diagnostics with timeout
|
|
75
|
+
const timeoutMs = 3000;
|
|
76
|
+
const start = Date.now();
|
|
77
|
+
while (Date.now() - start < timeoutMs) {
|
|
78
|
+
const diagnostics = client.diagnostics.get(uri);
|
|
79
|
+
if (diagnostics !== undefined) {
|
|
80
|
+
return diagnostics;
|
|
81
|
+
}
|
|
82
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return client.diagnostics.get(uri) ?? [];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
dispose(): void {
|
|
89
|
+
// Client lifecycle is managed globally, nothing to dispose here
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Factory function to create an LSP linter client.
|
|
95
|
+
*/
|
|
96
|
+
export function createLspLinterClient(config: ServerConfig, cwd: string): LinterClient {
|
|
97
|
+
return new LspLinterClient(config, cwd);
|
|
98
|
+
}
|
|
@@ -2,6 +2,7 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { extname, join } from "node:path";
|
|
4
4
|
import { getConfigDirPaths } from "../../../config.js";
|
|
5
|
+
import { createBiomeClient } from "./clients/biome-client";
|
|
5
6
|
import type { ServerConfig } from "./types";
|
|
6
7
|
|
|
7
8
|
export interface LspConfig {
|
|
@@ -110,6 +111,8 @@ export const SERVERS: Record<string, ServerConfig> = {
|
|
|
110
111
|
fileTypes: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".json", ".jsonc"],
|
|
111
112
|
rootMarkers: ["biome.json", "biome.jsonc"],
|
|
112
113
|
isLinter: true,
|
|
114
|
+
// Use CLI instead of LSP - Biome's LSP has known stale diagnostics issues
|
|
115
|
+
createClient: createBiomeClient,
|
|
113
116
|
},
|
|
114
117
|
|
|
115
118
|
eslint: {
|
|
@@ -2,7 +2,8 @@ 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
4
|
import type { BunFile } from "bun";
|
|
5
|
-
import type
|
|
5
|
+
import { type Theme, theme } from "../../../modes/interactive/theme/theme";
|
|
6
|
+
import lspDescription from "../../../prompts/tools/lsp.md" with { type: "text" };
|
|
6
7
|
import { logger } from "../../logger";
|
|
7
8
|
import { once, untilAborted } from "../../utils";
|
|
8
9
|
import { resolveToCwd } from "../path-utils";
|
|
@@ -17,7 +18,8 @@ import {
|
|
|
17
18
|
setIdleTimeout,
|
|
18
19
|
syncContent,
|
|
19
20
|
} from "./client";
|
|
20
|
-
import {
|
|
21
|
+
import { getLinterClient } from "./clients";
|
|
22
|
+
import { getServersForFile, hasCapability, type LspConfig, loadConfig } from "./config";
|
|
21
23
|
import { applyTextEditsToString, applyWorkspaceEdit } from "./edits";
|
|
22
24
|
import { renderCall, renderResult } from "./render";
|
|
23
25
|
import * as rustAnalyzer from "./rust-analyzer";
|
|
@@ -79,10 +81,11 @@ export async function warmupLspServers(cwd: string): Promise<LspWarmupResult> {
|
|
|
79
81
|
const config = loadConfig(cwd);
|
|
80
82
|
setIdleTimeout(config.idleTimeoutMs);
|
|
81
83
|
const servers: LspWarmupResult["servers"] = [];
|
|
84
|
+
const lspServers = getLspServers(config);
|
|
82
85
|
|
|
83
86
|
// Start all detected servers in parallel
|
|
84
87
|
const results = await Promise.allSettled(
|
|
85
|
-
|
|
88
|
+
lspServers.map(async ([name, serverConfig]) => {
|
|
86
89
|
const client = await getOrCreateClient(serverConfig, cwd);
|
|
87
90
|
return { name, client, fileTypes: serverConfig.fileTypes };
|
|
88
91
|
}),
|
|
@@ -134,6 +137,9 @@ async function syncFileContent(
|
|
|
134
137
|
): Promise<void> {
|
|
135
138
|
await Promise.allSettled(
|
|
136
139
|
servers.map(async ([_serverName, serverConfig]) => {
|
|
140
|
+
if (serverConfig.createClient) {
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
137
143
|
const client = await getOrCreateClient(serverConfig, cwd);
|
|
138
144
|
await syncContent(client, absolutePath, content);
|
|
139
145
|
}),
|
|
@@ -155,6 +161,9 @@ async function notifyFileSaved(
|
|
|
155
161
|
): Promise<void> {
|
|
156
162
|
await Promise.allSettled(
|
|
157
163
|
servers.map(async ([_serverName, serverConfig]) => {
|
|
164
|
+
if (serverConfig.createClient) {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
158
167
|
const client = await getOrCreateClient(serverConfig, cwd);
|
|
159
168
|
await notifySaved(client, absolutePath);
|
|
160
169
|
}),
|
|
@@ -174,6 +183,41 @@ function getConfig(cwd: string): LspConfig {
|
|
|
174
183
|
return config;
|
|
175
184
|
}
|
|
176
185
|
|
|
186
|
+
function isCustomLinter(serverConfig: ServerConfig): boolean {
|
|
187
|
+
return Boolean(serverConfig.createClient);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function splitServers(servers: Array<[string, ServerConfig]>): {
|
|
191
|
+
lspServers: Array<[string, ServerConfig]>;
|
|
192
|
+
customLinterServers: Array<[string, ServerConfig]>;
|
|
193
|
+
} {
|
|
194
|
+
const lspServers: Array<[string, ServerConfig]> = [];
|
|
195
|
+
const customLinterServers: Array<[string, ServerConfig]> = [];
|
|
196
|
+
for (const entry of servers) {
|
|
197
|
+
if (isCustomLinter(entry[1])) {
|
|
198
|
+
customLinterServers.push(entry);
|
|
199
|
+
} else {
|
|
200
|
+
lspServers.push(entry);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return { lspServers, customLinterServers };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function getLspServers(config: LspConfig): Array<[string, ServerConfig]> {
|
|
207
|
+
return (Object.entries(config.servers) as Array<[string, ServerConfig]>).filter(
|
|
208
|
+
([, serverConfig]) => !isCustomLinter(serverConfig),
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function getLspServersForFile(config: LspConfig, filePath: string): Array<[string, ServerConfig]> {
|
|
213
|
+
return getServersForFile(config, filePath).filter(([, serverConfig]) => !isCustomLinter(serverConfig));
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function getLspServerForFile(config: LspConfig, filePath: string): [string, ServerConfig] | null {
|
|
217
|
+
const servers = getLspServersForFile(config, filePath);
|
|
218
|
+
return servers.length > 0 ? servers[0] : null;
|
|
219
|
+
}
|
|
220
|
+
|
|
177
221
|
const FILE_SEARCH_MAX_DEPTH = 5;
|
|
178
222
|
const IGNORED_DIRS = new Set(["node_modules", "target", "dist", "build", ".git"]);
|
|
179
223
|
|
|
@@ -214,7 +258,7 @@ function findFileForServer(cwd: string, serverConfig: ServerConfig): string | nu
|
|
|
214
258
|
}
|
|
215
259
|
|
|
216
260
|
function getRustServer(config: LspConfig): [string, ServerConfig] | null {
|
|
217
|
-
const entries =
|
|
261
|
+
const entries = getLspServers(config);
|
|
218
262
|
const byName = entries.find(([name, server]) => name === "rust-analyzer" || server.command === "rust-analyzer");
|
|
219
263
|
if (byName) return byName;
|
|
220
264
|
|
|
@@ -234,7 +278,7 @@ function getRustServer(config: LspConfig): [string, ServerConfig] | null {
|
|
|
234
278
|
}
|
|
235
279
|
|
|
236
280
|
function getServerForWorkspaceAction(config: LspConfig, action: string): [string, ServerConfig] | null {
|
|
237
|
-
const entries =
|
|
281
|
+
const entries = getLspServers(config);
|
|
238
282
|
if (entries.length === 0) return null;
|
|
239
283
|
|
|
240
284
|
if (action === "workspace_symbols") {
|
|
@@ -379,8 +423,7 @@ export interface FileDiagnosticsResult {
|
|
|
379
423
|
}
|
|
380
424
|
|
|
381
425
|
/**
|
|
382
|
-
* Get
|
|
383
|
-
* Assumes content was synced and didSave was sent - just waits for diagnostics.
|
|
426
|
+
* Get diagnostics for a file using LSP or custom linter client.
|
|
384
427
|
*
|
|
385
428
|
* @param absolutePath - Absolute path to the file
|
|
386
429
|
* @param cwd - Working directory for LSP config resolution
|
|
@@ -404,6 +447,14 @@ async function getDiagnosticsForFile(
|
|
|
404
447
|
// Wait for diagnostics from all servers in parallel
|
|
405
448
|
const results = await Promise.allSettled(
|
|
406
449
|
servers.map(async ([serverName, serverConfig]) => {
|
|
450
|
+
// Use custom linter client if configured
|
|
451
|
+
if (serverConfig.createClient) {
|
|
452
|
+
const linterClient = getLinterClient(serverName, serverConfig, cwd);
|
|
453
|
+
const diagnostics = await linterClient.lint(absolutePath);
|
|
454
|
+
return { serverName, diagnostics };
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// Default: use LSP
|
|
407
458
|
const client = await getOrCreateClient(serverConfig, cwd);
|
|
408
459
|
// Content already synced + didSave sent, just wait for diagnostics
|
|
409
460
|
const diagnostics = await waitForDiagnostics(client, uri);
|
|
@@ -469,9 +520,7 @@ const DEFAULT_FORMAT_OPTIONS = {
|
|
|
469
520
|
};
|
|
470
521
|
|
|
471
522
|
/**
|
|
472
|
-
* Format content
|
|
473
|
-
* Assumes content was already synced to all servers via syncFileContent.
|
|
474
|
-
* Requests formatting from first capable server, applies edits in-memory.
|
|
523
|
+
* Format content using LSP or custom linter client.
|
|
475
524
|
*
|
|
476
525
|
* @param absolutePath - Absolute path (for URI)
|
|
477
526
|
* @param content - Content to format
|
|
@@ -491,8 +540,15 @@ async function formatContent(
|
|
|
491
540
|
|
|
492
541
|
const uri = fileToUri(absolutePath);
|
|
493
542
|
|
|
494
|
-
for (const [
|
|
543
|
+
for (const [serverName, serverConfig] of servers) {
|
|
495
544
|
try {
|
|
545
|
+
// Use custom linter client if configured
|
|
546
|
+
if (serverConfig.createClient) {
|
|
547
|
+
const linterClient = getLinterClient(serverName, serverConfig, cwd);
|
|
548
|
+
return await linterClient.format(absolutePath, content);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// Default: use LSP
|
|
496
552
|
const client = await getOrCreateClient(serverConfig, cwd);
|
|
497
553
|
|
|
498
554
|
const caps = client.serverCapabilities;
|
|
@@ -561,36 +617,48 @@ export function createLspWritethrough(cwd: string, options?: WritethroughOptions
|
|
|
561
617
|
if (servers.length === 0) {
|
|
562
618
|
return writethroughNoop(dst, content, signal, file);
|
|
563
619
|
}
|
|
620
|
+
const { lspServers, customLinterServers } = splitServers(servers);
|
|
564
621
|
|
|
565
622
|
let finalContent = content;
|
|
566
|
-
const
|
|
623
|
+
const writeContent = async (value: string) => (file ? file.write(value) : Bun.write(dst, value));
|
|
624
|
+
const getWritePromise = once(() => writeContent(finalContent));
|
|
625
|
+
const useCustomFormatter = enableFormat && customLinterServers.length > 0;
|
|
567
626
|
|
|
568
627
|
let formatter: FileFormatResult | undefined;
|
|
569
628
|
let diagnostics: FileDiagnosticsResult | undefined;
|
|
570
629
|
try {
|
|
571
630
|
signal ??= AbortSignal.timeout(10_000);
|
|
572
631
|
await untilAborted(signal, async () => {
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
if (enableFormat) {
|
|
578
|
-
finalContent = await formatContent(dst, content, cwd, servers);
|
|
632
|
+
if (useCustomFormatter) {
|
|
633
|
+
// Custom linters (e.g. Biome CLI) require on-disk input.
|
|
634
|
+
await writeContent(content);
|
|
635
|
+
finalContent = await formatContent(dst, content, cwd, customLinterServers);
|
|
579
636
|
formatter = finalContent !== content ? FileFormatResult.FORMATTED : FileFormatResult.UNCHANGED;
|
|
580
|
-
|
|
637
|
+
await writeContent(finalContent);
|
|
638
|
+
await syncFileContent(dst, finalContent, cwd, lspServers);
|
|
639
|
+
} else {
|
|
640
|
+
// 1. Sync original content to LSP servers
|
|
641
|
+
await syncFileContent(dst, content, cwd, lspServers);
|
|
642
|
+
|
|
643
|
+
// 2. Format in-memory via LSP
|
|
644
|
+
if (enableFormat) {
|
|
645
|
+
finalContent = await formatContent(dst, content, cwd, lspServers);
|
|
646
|
+
formatter = finalContent !== content ? FileFormatResult.FORMATTED : FileFormatResult.UNCHANGED;
|
|
647
|
+
}
|
|
581
648
|
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
649
|
+
// 3. If formatted, sync formatted content to LSP servers
|
|
650
|
+
if (finalContent !== content) {
|
|
651
|
+
await syncFileContent(dst, finalContent, cwd, lspServers);
|
|
652
|
+
}
|
|
586
653
|
|
|
587
|
-
|
|
588
|
-
|
|
654
|
+
// 4. Write to disk
|
|
655
|
+
await getWritePromise();
|
|
656
|
+
}
|
|
589
657
|
|
|
590
|
-
// 5. Notify saved to
|
|
591
|
-
await notifyFileSaved(dst, cwd,
|
|
658
|
+
// 5. Notify saved to LSP servers
|
|
659
|
+
await notifyFileSaved(dst, cwd, lspServers);
|
|
592
660
|
|
|
593
|
-
// 6. Get diagnostics from
|
|
661
|
+
// 6. Get diagnostics from all servers
|
|
594
662
|
if (enableDiagnostics) {
|
|
595
663
|
diagnostics = await getDiagnosticsForFile(dst, cwd, servers);
|
|
596
664
|
}
|
|
@@ -618,29 +686,7 @@ export function createLspTool(cwd: string): AgentTool<typeof lspSchema, LspToolD
|
|
|
618
686
|
return {
|
|
619
687
|
name: "lsp",
|
|
620
688
|
label: "LSP",
|
|
621
|
-
description:
|
|
622
|
-
|
|
623
|
-
Standard operations:
|
|
624
|
-
- diagnostics: Get errors/warnings for a file
|
|
625
|
-
- workspace_diagnostics: Check entire project for errors (uses tsc, cargo check, go build, etc.)
|
|
626
|
-
- definition: Go to symbol definition
|
|
627
|
-
- references: Find all references to a symbol
|
|
628
|
-
- hover: Get type info and documentation
|
|
629
|
-
- symbols: List symbols in a file (functions, classes, etc.)
|
|
630
|
-
- workspace_symbols: Search for symbols across the project
|
|
631
|
-
- rename: Rename a symbol across the codebase
|
|
632
|
-
- actions: List and apply code actions (quick fixes, refactors)
|
|
633
|
-
- incoming_calls: Find all callers of a function
|
|
634
|
-
- outgoing_calls: Find all functions called by a function
|
|
635
|
-
- status: Show active language servers
|
|
636
|
-
|
|
637
|
-
Rust-analyzer specific (require rust-analyzer):
|
|
638
|
-
- flycheck: Run clippy/cargo check
|
|
639
|
-
- expand_macro: Show macro expansion at cursor
|
|
640
|
-
- ssr: Structural search-replace
|
|
641
|
-
- runnables: Find runnable tests/binaries
|
|
642
|
-
- related_tests: Find tests for a function
|
|
643
|
-
- reload_workspace: Reload Cargo.toml changes`,
|
|
689
|
+
description: lspDescription,
|
|
644
690
|
parameters: lspSchema,
|
|
645
691
|
renderCall,
|
|
646
692
|
renderResult,
|
|
@@ -709,7 +755,7 @@ Rust-analyzer specific (require rust-analyzer):
|
|
|
709
755
|
const resolved = resolveToCwd(target, cwd);
|
|
710
756
|
const servers = getServersForFile(config, resolved);
|
|
711
757
|
if (servers.length === 0) {
|
|
712
|
-
results.push(
|
|
758
|
+
results.push(`${theme.status.error} ${target}: No language server found`);
|
|
713
759
|
continue;
|
|
714
760
|
}
|
|
715
761
|
|
|
@@ -721,6 +767,12 @@ Rust-analyzer specific (require rust-analyzer):
|
|
|
721
767
|
for (const [serverName, serverConfig] of servers) {
|
|
722
768
|
allServerNames.add(serverName);
|
|
723
769
|
try {
|
|
770
|
+
if (serverConfig.createClient) {
|
|
771
|
+
const linterClient = getLinterClient(serverName, serverConfig, cwd);
|
|
772
|
+
const diagnostics = await linterClient.lint(resolved);
|
|
773
|
+
allDiagnostics.push(...diagnostics);
|
|
774
|
+
continue;
|
|
775
|
+
}
|
|
724
776
|
const client = await getOrCreateClient(serverConfig, cwd);
|
|
725
777
|
await refreshFile(client, resolved);
|
|
726
778
|
const diagnostics = await waitForDiagnostics(client, uri);
|
|
@@ -759,10 +811,10 @@ Rust-analyzer specific (require rust-analyzer):
|
|
|
759
811
|
}
|
|
760
812
|
|
|
761
813
|
if (uniqueDiagnostics.length === 0) {
|
|
762
|
-
results.push(
|
|
814
|
+
results.push(`${theme.status.success} ${relPath}: no issues`);
|
|
763
815
|
} else {
|
|
764
816
|
const summary = formatDiagnosticsSummary(uniqueDiagnostics);
|
|
765
|
-
results.push(
|
|
817
|
+
results.push(`${theme.status.error} ${relPath}: ${summary}`);
|
|
766
818
|
for (const diag of uniqueDiagnostics) {
|
|
767
819
|
results.push(` ${formatDiagnostic(diag, relPath)}`);
|
|
768
820
|
}
|
|
@@ -792,7 +844,7 @@ Rust-analyzer specific (require rust-analyzer):
|
|
|
792
844
|
|
|
793
845
|
const resolvedFile = file ? resolveToCwd(file, cwd) : null;
|
|
794
846
|
const serverInfo = resolvedFile
|
|
795
|
-
?
|
|
847
|
+
? getLspServerForFile(config, resolvedFile)
|
|
796
848
|
: getServerForWorkspaceAction(config, action);
|
|
797
849
|
|
|
798
850
|
if (!serverInfo) {
|