@oh-my-pi/pi-coding-agent 1.340.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 +42 -1
- package/package.json +1 -1
- package/src/cli/args.ts +8 -0
- package/src/core/agent-session.ts +32 -14
- package/src/core/model-resolver.ts +101 -0
- package/src/core/sdk.ts +50 -17
- package/src/core/session-manager.ts +117 -14
- package/src/core/settings-manager.ts +90 -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 +50 -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 +86 -9
|
@@ -51,12 +51,23 @@ export interface MCPSettings {
|
|
|
51
51
|
enableProjectConfig?: boolean; // default: true (load .mcp.json from project root)
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
export interface LspSettings {
|
|
55
|
+
formatOnWrite?: boolean; // default: true (format files using LSP after write tool writes code files)
|
|
56
|
+
diagnosticsOnWrite?: boolean; // default: true (return LSP diagnostics after write tool writes code files)
|
|
57
|
+
diagnosticsOnEdit?: boolean; // default: false (return LSP diagnostics after edit tool edits code files)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface EditSettings {
|
|
61
|
+
fuzzyMatch?: boolean; // default: true (accept high-confidence fuzzy matches for whitespace/indentation)
|
|
62
|
+
}
|
|
63
|
+
|
|
54
64
|
export interface Settings {
|
|
55
65
|
lastChangelogVersion?: string;
|
|
56
|
-
|
|
57
|
-
|
|
66
|
+
/** Model roles map: { default: "provider/modelId", small: "provider/modelId", ... } */
|
|
67
|
+
modelRoles?: Record<string, string>;
|
|
58
68
|
defaultThinkingLevel?: "off" | "minimal" | "low" | "medium" | "high" | "xhigh";
|
|
59
69
|
queueMode?: "all" | "one-at-a-time";
|
|
70
|
+
interruptMode?: "immediate" | "wait";
|
|
60
71
|
theme?: string;
|
|
61
72
|
compaction?: CompactionSettings;
|
|
62
73
|
branchSummary?: BranchSummarySettings;
|
|
@@ -72,6 +83,8 @@ export interface Settings {
|
|
|
72
83
|
exa?: ExaSettings;
|
|
73
84
|
bashInterceptor?: BashInterceptorSettings;
|
|
74
85
|
mcp?: MCPSettings;
|
|
86
|
+
lsp?: LspSettings;
|
|
87
|
+
edit?: EditSettings;
|
|
75
88
|
}
|
|
76
89
|
|
|
77
90
|
/** Deep merge settings: project/overrides take precedence, nested objects merge recursively */
|
|
@@ -200,28 +213,29 @@ export class SettingsManager {
|
|
|
200
213
|
this.save();
|
|
201
214
|
}
|
|
202
215
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
return this.settings.defaultModel;
|
|
216
|
+
/**
|
|
217
|
+
* Get model for a role. Returns "provider/modelId" string or undefined.
|
|
218
|
+
*/
|
|
219
|
+
getModelRole(role: string): string | undefined {
|
|
220
|
+
return this.settings.modelRoles?.[role];
|
|
209
221
|
}
|
|
210
222
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
223
|
+
/**
|
|
224
|
+
* Set model for a role. Model should be "provider/modelId" format.
|
|
225
|
+
*/
|
|
226
|
+
setModelRole(role: string, model: string): void {
|
|
227
|
+
if (!this.globalSettings.modelRoles) {
|
|
228
|
+
this.globalSettings.modelRoles = {};
|
|
229
|
+
}
|
|
230
|
+
this.globalSettings.modelRoles[role] = model;
|
|
218
231
|
this.save();
|
|
219
232
|
}
|
|
220
233
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
234
|
+
/**
|
|
235
|
+
* Get all model roles.
|
|
236
|
+
*/
|
|
237
|
+
getModelRoles(): Record<string, string> {
|
|
238
|
+
return { ...this.settings.modelRoles };
|
|
225
239
|
}
|
|
226
240
|
|
|
227
241
|
getQueueMode(): "all" | "one-at-a-time" {
|
|
@@ -233,6 +247,15 @@ export class SettingsManager {
|
|
|
233
247
|
this.save();
|
|
234
248
|
}
|
|
235
249
|
|
|
250
|
+
getInterruptMode(): "immediate" | "wait" {
|
|
251
|
+
return this.settings.interruptMode || "immediate";
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
setInterruptMode(mode: "immediate" | "wait"): void {
|
|
255
|
+
this.globalSettings.interruptMode = mode;
|
|
256
|
+
this.save();
|
|
257
|
+
}
|
|
258
|
+
|
|
236
259
|
getTheme(): string | undefined {
|
|
237
260
|
return this.settings.theme;
|
|
238
261
|
}
|
|
@@ -474,4 +497,52 @@ export class SettingsManager {
|
|
|
474
497
|
this.globalSettings.mcp.enableProjectConfig = enabled;
|
|
475
498
|
this.save();
|
|
476
499
|
}
|
|
500
|
+
|
|
501
|
+
getLspFormatOnWrite(): boolean {
|
|
502
|
+
return this.settings.lsp?.formatOnWrite ?? true;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
setLspFormatOnWrite(enabled: boolean): void {
|
|
506
|
+
if (!this.globalSettings.lsp) {
|
|
507
|
+
this.globalSettings.lsp = {};
|
|
508
|
+
}
|
|
509
|
+
this.globalSettings.lsp.formatOnWrite = enabled;
|
|
510
|
+
this.save();
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
getLspDiagnosticsOnWrite(): boolean {
|
|
514
|
+
return this.settings.lsp?.diagnosticsOnWrite ?? true;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
setLspDiagnosticsOnWrite(enabled: boolean): void {
|
|
518
|
+
if (!this.globalSettings.lsp) {
|
|
519
|
+
this.globalSettings.lsp = {};
|
|
520
|
+
}
|
|
521
|
+
this.globalSettings.lsp.diagnosticsOnWrite = enabled;
|
|
522
|
+
this.save();
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
getLspDiagnosticsOnEdit(): boolean {
|
|
526
|
+
return this.settings.lsp?.diagnosticsOnEdit ?? false;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
setLspDiagnosticsOnEdit(enabled: boolean): void {
|
|
530
|
+
if (!this.globalSettings.lsp) {
|
|
531
|
+
this.globalSettings.lsp = {};
|
|
532
|
+
}
|
|
533
|
+
this.globalSettings.lsp.diagnosticsOnEdit = enabled;
|
|
534
|
+
this.save();
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
getEditFuzzyMatch(): boolean {
|
|
538
|
+
return this.settings.edit?.fuzzyMatch ?? true;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
setEditFuzzyMatch(enabled: boolean): void {
|
|
542
|
+
if (!this.globalSettings.edit) {
|
|
543
|
+
this.globalSettings.edit = {};
|
|
544
|
+
}
|
|
545
|
+
this.globalSettings.edit.fuzzyMatch = enabled;
|
|
546
|
+
this.save();
|
|
547
|
+
}
|
|
477
548
|
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generate session titles using a smol, fast model.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { Model } from "@oh-my-pi/pi-ai";
|
|
6
|
+
import { completeSimple } from "@oh-my-pi/pi-ai";
|
|
7
|
+
import type { ModelRegistry } from "./model-registry.js";
|
|
8
|
+
import { findSmolModel } from "./model-resolver.js";
|
|
9
|
+
|
|
10
|
+
const TITLE_SYSTEM_PROMPT = `Generate a very short title (3-6 words) for a coding session based on the user's first message. The title should capture the main task or topic. Output ONLY the title, nothing else. No quotes, no punctuation at the end.
|
|
11
|
+
|
|
12
|
+
Examples:
|
|
13
|
+
- "Fix TypeScript compilation errors"
|
|
14
|
+
- "Add user authentication"
|
|
15
|
+
- "Refactor database queries"
|
|
16
|
+
- "Debug payment webhook"
|
|
17
|
+
- "Update React components"`;
|
|
18
|
+
|
|
19
|
+
const MAX_INPUT_CHARS = 2000;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Find the best available model for title generation.
|
|
23
|
+
* Uses the configured smol model if set, otherwise auto-discovers using priority chain.
|
|
24
|
+
*
|
|
25
|
+
* @param registry Model registry
|
|
26
|
+
* @param savedSmolModel Optional saved smol model from settings (provider/modelId format)
|
|
27
|
+
*/
|
|
28
|
+
export async function findTitleModel(registry: ModelRegistry, savedSmolModel?: string): Promise<Model<any> | null> {
|
|
29
|
+
const model = await findSmolModel(registry, savedSmolModel);
|
|
30
|
+
return model ?? null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Generate a title for a session based on the first user message.
|
|
35
|
+
*
|
|
36
|
+
* @param firstMessage The first user message
|
|
37
|
+
* @param registry Model registry
|
|
38
|
+
* @param savedSmolModel Optional saved smol model from settings (provider/modelId format)
|
|
39
|
+
*/
|
|
40
|
+
export async function generateSessionTitle(
|
|
41
|
+
firstMessage: string,
|
|
42
|
+
registry: ModelRegistry,
|
|
43
|
+
savedSmolModel?: string,
|
|
44
|
+
): Promise<string | null> {
|
|
45
|
+
const model = await findTitleModel(registry, savedSmolModel);
|
|
46
|
+
if (!model) return null;
|
|
47
|
+
|
|
48
|
+
const apiKey = await registry.getApiKey(model);
|
|
49
|
+
if (!apiKey) return null;
|
|
50
|
+
|
|
51
|
+
// Truncate message if too long
|
|
52
|
+
const truncatedMessage =
|
|
53
|
+
firstMessage.length > MAX_INPUT_CHARS ? `${firstMessage.slice(0, MAX_INPUT_CHARS)}...` : firstMessage;
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
const response = await completeSimple(
|
|
57
|
+
model,
|
|
58
|
+
{
|
|
59
|
+
systemPrompt: TITLE_SYSTEM_PROMPT,
|
|
60
|
+
messages: [{ role: "user", content: truncatedMessage, timestamp: Date.now() }],
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
apiKey,
|
|
64
|
+
maxTokens: 30,
|
|
65
|
+
},
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
// Extract title from response text content
|
|
69
|
+
let title = "";
|
|
70
|
+
for (const content of response.content) {
|
|
71
|
+
if (content.type === "text") {
|
|
72
|
+
title += content.text;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
title = title.trim();
|
|
76
|
+
|
|
77
|
+
if (!title || title.length > 60) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Clean up: remove quotes, trailing punctuation
|
|
82
|
+
return title.replace(/^["']|["']$/g, "").replace(/[.!?]$/, "");
|
|
83
|
+
} catch {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Set the terminal title using ANSI escape sequences.
|
|
90
|
+
*/
|
|
91
|
+
export function setTerminalTitle(title: string): void {
|
|
92
|
+
// OSC 2 sets the window title
|
|
93
|
+
process.stdout.write(`\x1b]2;${title}\x07`);
|
|
94
|
+
}
|
package/src/core/tools/bash.ts
CHANGED
|
@@ -66,8 +66,7 @@ Usage notes:
|
|
|
66
66
|
- If the commands are independent and can run in parallel, make multiple bash tool calls in a single message. For example, if you need to run "git status" and "git diff", send a single message with two bash tool calls in parallel.
|
|
67
67
|
- If the commands depend on each other and must run sequentially, use a single bash call with '&&' to chain them together (e.g., \`git add . && git commit -m "message" && git push\`). For instance, if one operation must complete before another starts (like mkdir before cp, Write before Bash for git operations, or git add before git commit), run these operations sequentially instead.
|
|
68
68
|
- Use ';' only when you need to run commands sequentially but don't care if earlier commands fail
|
|
69
|
-
- DO NOT use newlines to separate commands (newlines are ok in quoted strings)
|
|
70
|
-
- Try to maintain your current working directory throughout the session by using absolute paths and avoiding usage of \`cd\`. You may use \`cd\` if the User explicitly requests it.`,
|
|
69
|
+
- DO NOT use newlines to separate commands (newlines are ok in quoted strings)`,
|
|
71
70
|
parameters: bashSchema,
|
|
72
71
|
execute: async (
|
|
73
72
|
_toolCallId: string,
|
|
@@ -271,7 +271,7 @@ export function formatEditMatchError(
|
|
|
271
271
|
? options.fuzzyMatches && options.fuzzyMatches > 1
|
|
272
272
|
? `Found ${options.fuzzyMatches} high-confidence matches. Provide more context to make it unique.`
|
|
273
273
|
: `Closest match was below the ${thresholdPercent}% similarity threshold.`
|
|
274
|
-
: "
|
|
274
|
+
: "Fuzzy matching is disabled. Enable 'Edit fuzzy match' in settings to accept high-confidence matches.";
|
|
275
275
|
|
|
276
276
|
return [
|
|
277
277
|
options.allowFuzzy
|
|
@@ -409,7 +409,7 @@ export async function computeEditDiff(
|
|
|
409
409
|
oldText: string,
|
|
410
410
|
newText: string,
|
|
411
411
|
cwd: string,
|
|
412
|
-
fuzzy =
|
|
412
|
+
fuzzy = true,
|
|
413
413
|
): Promise<EditDiffResult | EditDiffError> {
|
|
414
414
|
const absolutePath = resolveToCwd(path, cwd);
|
|
415
415
|
|
package/src/core/tools/edit.ts
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
restoreLineEndings,
|
|
13
13
|
stripBom,
|
|
14
14
|
} from "./edit-diff.js";
|
|
15
|
+
import type { FileDiagnosticsResult } from "./lsp/index.js";
|
|
15
16
|
import { resolveToCwd } from "./path-utils.js";
|
|
16
17
|
|
|
17
18
|
const editSchema = Type.Object({
|
|
@@ -27,9 +28,21 @@ export interface EditToolDetails {
|
|
|
27
28
|
diff: string;
|
|
28
29
|
/** Line number of the first change in the new file (for editor navigation) */
|
|
29
30
|
firstChangedLine?: number;
|
|
31
|
+
/** Whether LSP diagnostics were retrieved */
|
|
32
|
+
hasDiagnostics?: boolean;
|
|
33
|
+
/** Diagnostic result (if available) */
|
|
34
|
+
diagnostics?: FileDiagnosticsResult;
|
|
30
35
|
}
|
|
31
36
|
|
|
32
|
-
export
|
|
37
|
+
export interface EditToolOptions {
|
|
38
|
+
/** Whether to accept high-confidence fuzzy matches for whitespace/indentation (default: true) */
|
|
39
|
+
fuzzyMatch?: boolean;
|
|
40
|
+
/** Callback to get LSP diagnostics after editing a file */
|
|
41
|
+
getDiagnostics?: (absolutePath: string) => Promise<FileDiagnosticsResult>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function createEditTool(cwd: string, options: EditToolOptions = {}): AgentTool<typeof editSchema> {
|
|
45
|
+
const allowFuzzy = options.fuzzyMatch ?? true;
|
|
33
46
|
return {
|
|
34
47
|
name: "edit",
|
|
35
48
|
label: "Edit",
|
|
@@ -108,7 +121,7 @@ Usage:
|
|
|
108
121
|
const normalizedNewText = normalizeToLF(newText);
|
|
109
122
|
|
|
110
123
|
const matchOutcome = findEditMatch(normalizedContent, normalizedOldText, {
|
|
111
|
-
allowFuzzy
|
|
124
|
+
allowFuzzy,
|
|
112
125
|
similarityThreshold: DEFAULT_FUZZY_THRESHOLD,
|
|
113
126
|
});
|
|
114
127
|
|
|
@@ -131,7 +144,7 @@ Usage:
|
|
|
131
144
|
reject(
|
|
132
145
|
new Error(
|
|
133
146
|
formatEditMatchError(path, normalizedOldText, matchOutcome.closest, {
|
|
134
|
-
allowFuzzy
|
|
147
|
+
allowFuzzy,
|
|
135
148
|
similarityThreshold: DEFAULT_FUZZY_THRESHOLD,
|
|
136
149
|
fuzzyMatches: matchOutcome.fuzzyMatches,
|
|
137
150
|
}),
|
|
@@ -179,14 +192,39 @@ Usage:
|
|
|
179
192
|
}
|
|
180
193
|
|
|
181
194
|
const diffResult = generateDiffString(normalizedContent, normalizedNewContent);
|
|
195
|
+
|
|
196
|
+
// Get LSP diagnostics if callback provided
|
|
197
|
+
let diagnosticsResult: FileDiagnosticsResult | undefined;
|
|
198
|
+
if (options.getDiagnostics) {
|
|
199
|
+
try {
|
|
200
|
+
diagnosticsResult = await options.getDiagnostics(absolutePath);
|
|
201
|
+
} catch {
|
|
202
|
+
// Ignore diagnostics errors - don't fail the edit
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Build result text
|
|
207
|
+
let resultText = `Successfully replaced text in ${path}.`;
|
|
208
|
+
|
|
209
|
+
// Append diagnostics if available and there are issues
|
|
210
|
+
if (diagnosticsResult?.available && diagnosticsResult.diagnostics.length > 0) {
|
|
211
|
+
resultText += `\n\nLSP Diagnostics (${diagnosticsResult.summary}):\n`;
|
|
212
|
+
resultText += diagnosticsResult.diagnostics.map((d) => ` ${d}`).join("\n");
|
|
213
|
+
}
|
|
214
|
+
|
|
182
215
|
resolve({
|
|
183
216
|
content: [
|
|
184
217
|
{
|
|
185
218
|
type: "text",
|
|
186
|
-
text:
|
|
219
|
+
text: resultText,
|
|
187
220
|
},
|
|
188
221
|
],
|
|
189
|
-
details: {
|
|
222
|
+
details: {
|
|
223
|
+
diff: diffResult.diff,
|
|
224
|
+
firstChangedLine: diffResult.firstChangedLine,
|
|
225
|
+
hasDiagnostics: diagnosticsResult?.available ?? false,
|
|
226
|
+
diagnostics: diagnosticsResult,
|
|
227
|
+
},
|
|
190
228
|
});
|
|
191
229
|
} catch (error: any) {
|
|
192
230
|
// Clean up abort handler
|
package/src/core/tools/grep.ts
CHANGED
|
@@ -68,10 +68,11 @@ export function createGrepTool(cwd: string): AgentTool<typeof grepSchema> {
|
|
|
68
68
|
|
|
69
69
|
Usage:
|
|
70
70
|
- ALWAYS use grep for search tasks. NEVER invoke \`grep\` or \`rg\` as a bash command. The grep tool has been optimized for correct permissions and access.
|
|
71
|
+
- Searches recursively by default - no need for -r flag
|
|
71
72
|
- Supports full regex syntax (e.g., "log.*Error", "function\\s+\\w+")
|
|
72
|
-
- Filter files with glob parameter (e.g., "*.
|
|
73
|
+
- Filter files with glob parameter (e.g., "*.ts", "**/*.spec.ts") or type parameter (e.g., "ts", "py", "rust") - equivalent to grep's --include
|
|
73
74
|
- Output modes: "content" shows matching lines, "files_with_matches" shows only file paths (default), "count" shows match counts
|
|
74
|
-
- Use
|
|
75
|
+
- Pagination: Use headLimit to limit results (like \`| head -N\`), offset to skip first N results
|
|
75
76
|
- Pattern syntax: Uses ripgrep (not grep) - literal braces need escaping (use \`interface\\{\\}\` to find \`interface{}\` in Go code)
|
|
76
77
|
- Multiline matching: By default patterns match within single lines only. For cross-line patterns like \`struct \\{[\\s\\S]*?field\`, use \`multiline: true\``,
|
|
77
78
|
parameters: grepSchema,
|
package/src/core/tools/index.ts
CHANGED
|
@@ -1,13 +1,25 @@
|
|
|
1
1
|
export { type AskToolDetails, askTool, createAskTool } from "./ask.js";
|
|
2
2
|
export { type BashToolDetails, bashTool, createBashTool } from "./bash.js";
|
|
3
|
-
export { createEditTool, editTool } from "./edit.js";
|
|
3
|
+
export { createEditTool, type EditToolOptions, editTool } from "./edit.js";
|
|
4
4
|
// Exa MCP tools (22 tools)
|
|
5
5
|
export { exaTools } from "./exa/index.js";
|
|
6
6
|
export type { ExaRenderDetails, ExaSearchResponse, ExaSearchResult } from "./exa/types.js";
|
|
7
7
|
export { createFindTool, type FindToolDetails, findTool } from "./find.js";
|
|
8
8
|
export { createGrepTool, type GrepToolDetails, grepTool } from "./grep.js";
|
|
9
9
|
export { createLsTool, type LsToolDetails, lsTool } from "./ls.js";
|
|
10
|
-
export {
|
|
10
|
+
export {
|
|
11
|
+
createLspTool,
|
|
12
|
+
type FileDiagnosticsResult,
|
|
13
|
+
type FileFormatResult,
|
|
14
|
+
formatFile,
|
|
15
|
+
getDiagnosticsForFile,
|
|
16
|
+
getLspStatus,
|
|
17
|
+
type LspServerStatus,
|
|
18
|
+
type LspToolDetails,
|
|
19
|
+
type LspWarmupResult,
|
|
20
|
+
lspTool,
|
|
21
|
+
warmupLspServers,
|
|
22
|
+
} from "./lsp/index.js";
|
|
11
23
|
export { createNotebookTool, type NotebookToolDetails, notebookTool } from "./notebook.js";
|
|
12
24
|
export { createReadTool, type ReadToolDetails, readTool } from "./read.js";
|
|
13
25
|
export { BUNDLED_AGENTS, createTaskTool, taskTool } from "./task/index.js";
|
|
@@ -31,7 +43,7 @@ export {
|
|
|
31
43
|
webSearchLinkedinTool,
|
|
32
44
|
webSearchTool,
|
|
33
45
|
} from "./web-search/index.js";
|
|
34
|
-
export { createWriteTool, writeTool } from "./write.js";
|
|
46
|
+
export { createWriteTool, type WriteToolDetails, type WriteToolOptions, writeTool } from "./write.js";
|
|
35
47
|
|
|
36
48
|
import type { AgentTool } from "@oh-my-pi/pi-agent-core";
|
|
37
49
|
import { askTool, createAskTool } from "./ask.js";
|
|
@@ -41,7 +53,7 @@ import { createEditTool, editTool } from "./edit.js";
|
|
|
41
53
|
import { createFindTool, findTool } from "./find.js";
|
|
42
54
|
import { createGrepTool, grepTool } from "./grep.js";
|
|
43
55
|
import { createLsTool, lsTool } from "./ls.js";
|
|
44
|
-
import { createLspTool, lspTool } from "./lsp/index.js";
|
|
56
|
+
import { createLspTool, formatFile, getDiagnosticsForFile, lspTool } from "./lsp/index.js";
|
|
45
57
|
import { createNotebookTool, notebookTool } from "./notebook.js";
|
|
46
58
|
import { createReadTool, readTool } from "./read.js";
|
|
47
59
|
import { createTaskTool, taskTool } from "./task/index.js";
|
|
@@ -57,16 +69,47 @@ export interface SessionContext {
|
|
|
57
69
|
getSessionFile: () => string | null;
|
|
58
70
|
}
|
|
59
71
|
|
|
72
|
+
/** Options for creating coding tools */
|
|
73
|
+
export interface CodingToolsOptions {
|
|
74
|
+
/** Whether to fetch LSP diagnostics after write tool writes files (default: true) */
|
|
75
|
+
lspDiagnosticsOnWrite?: boolean;
|
|
76
|
+
/** Whether to fetch LSP diagnostics after edit tool edits files (default: false) */
|
|
77
|
+
lspDiagnosticsOnEdit?: boolean;
|
|
78
|
+
/** Whether to format files using LSP after write tool writes (default: true) */
|
|
79
|
+
lspFormatOnWrite?: boolean;
|
|
80
|
+
/** Whether to accept high-confidence fuzzy matches in edit tool (default: true) */
|
|
81
|
+
editFuzzyMatch?: boolean;
|
|
82
|
+
}
|
|
83
|
+
|
|
60
84
|
// Factory function type
|
|
61
|
-
type ToolFactory = (cwd: string, sessionContext?: SessionContext) => Tool;
|
|
85
|
+
type ToolFactory = (cwd: string, sessionContext?: SessionContext, options?: CodingToolsOptions) => Tool;
|
|
62
86
|
|
|
63
87
|
// Tool definitions: static tools and their factory functions
|
|
64
88
|
const toolDefs: Record<string, { tool: Tool; create: ToolFactory }> = {
|
|
65
89
|
ask: { tool: askTool, create: createAskTool },
|
|
66
90
|
read: { tool: readTool, create: createReadTool },
|
|
67
91
|
bash: { tool: bashTool, create: createBashTool },
|
|
68
|
-
edit: {
|
|
69
|
-
|
|
92
|
+
edit: {
|
|
93
|
+
tool: editTool,
|
|
94
|
+
create: (cwd, _ctx, options) => {
|
|
95
|
+
const enableDiagnostics = options?.lspDiagnosticsOnEdit ?? false;
|
|
96
|
+
return createEditTool(cwd, {
|
|
97
|
+
fuzzyMatch: options?.editFuzzyMatch ?? true,
|
|
98
|
+
getDiagnostics: enableDiagnostics ? (absolutePath) => getDiagnosticsForFile(absolutePath, cwd) : undefined,
|
|
99
|
+
});
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
write: {
|
|
103
|
+
tool: writeTool,
|
|
104
|
+
create: (cwd, _ctx, options) => {
|
|
105
|
+
const enableFormat = options?.lspFormatOnWrite ?? true;
|
|
106
|
+
const enableDiagnostics = options?.lspDiagnosticsOnWrite ?? true;
|
|
107
|
+
return createWriteTool(cwd, {
|
|
108
|
+
formatOnWrite: enableFormat ? (absolutePath) => formatFile(absolutePath, cwd) : undefined,
|
|
109
|
+
getDiagnostics: enableDiagnostics ? (absolutePath) => getDiagnosticsForFile(absolutePath, cwd) : undefined,
|
|
110
|
+
});
|
|
111
|
+
},
|
|
112
|
+
},
|
|
70
113
|
grep: { tool: grepTool, create: createGrepTool },
|
|
71
114
|
find: { tool: findTool, create: createFindTool },
|
|
72
115
|
ls: { tool: lsTool, create: createLsTool },
|
|
@@ -116,10 +159,16 @@ export const allTools = Object.fromEntries(Object.entries(toolDefs).map(([name,
|
|
|
116
159
|
* @param cwd - Working directory for tools
|
|
117
160
|
* @param hasUI - Whether UI is available (includes ask tool if true)
|
|
118
161
|
* @param sessionContext - Optional session context for tools that need it
|
|
162
|
+
* @param options - Options for tool configuration
|
|
119
163
|
*/
|
|
120
|
-
export function createCodingTools(
|
|
164
|
+
export function createCodingTools(
|
|
165
|
+
cwd: string,
|
|
166
|
+
hasUI = false,
|
|
167
|
+
sessionContext?: SessionContext,
|
|
168
|
+
options?: CodingToolsOptions,
|
|
169
|
+
): Tool[] {
|
|
121
170
|
const names = hasUI ? [...baseCodingToolNames, ...uiToolNames] : baseCodingToolNames;
|
|
122
|
-
return names.map((name) => toolDefs[name].create(cwd, sessionContext));
|
|
171
|
+
return names.map((name) => toolDefs[name].create(cwd, sessionContext, options));
|
|
123
172
|
}
|
|
124
173
|
|
|
125
174
|
/**
|
|
@@ -127,20 +176,31 @@ export function createCodingTools(cwd: string, hasUI = false, sessionContext?: S
|
|
|
127
176
|
* @param cwd - Working directory for tools
|
|
128
177
|
* @param hasUI - Whether UI is available (includes ask tool if true)
|
|
129
178
|
* @param sessionContext - Optional session context for tools that need it
|
|
179
|
+
* @param options - Options for tool configuration
|
|
130
180
|
*/
|
|
131
|
-
export function createReadOnlyTools(
|
|
181
|
+
export function createReadOnlyTools(
|
|
182
|
+
cwd: string,
|
|
183
|
+
hasUI = false,
|
|
184
|
+
sessionContext?: SessionContext,
|
|
185
|
+
options?: CodingToolsOptions,
|
|
186
|
+
): Tool[] {
|
|
132
187
|
const names = hasUI ? [...baseReadOnlyToolNames, ...uiToolNames] : baseReadOnlyToolNames;
|
|
133
|
-
return names.map((name) => toolDefs[name].create(cwd, sessionContext));
|
|
188
|
+
return names.map((name) => toolDefs[name].create(cwd, sessionContext, options));
|
|
134
189
|
}
|
|
135
190
|
|
|
136
191
|
/**
|
|
137
192
|
* Create all tools configured for a specific working directory.
|
|
138
193
|
* @param cwd - Working directory for tools
|
|
139
194
|
* @param sessionContext - Optional session context for tools that need it
|
|
195
|
+
* @param options - Options for tool configuration
|
|
140
196
|
*/
|
|
141
|
-
export function createAllTools(
|
|
197
|
+
export function createAllTools(
|
|
198
|
+
cwd: string,
|
|
199
|
+
sessionContext?: SessionContext,
|
|
200
|
+
options?: CodingToolsOptions,
|
|
201
|
+
): Record<ToolName, Tool> {
|
|
142
202
|
return Object.fromEntries(
|
|
143
|
-
Object.entries(toolDefs).map(([name, def]) => [name, def.create(cwd, sessionContext)]),
|
|
203
|
+
Object.entries(toolDefs).map(([name, def]) => [name, def.create(cwd, sessionContext, options)]),
|
|
144
204
|
) as Record<ToolName, Tool>;
|
|
145
205
|
}
|
|
146
206
|
|
|
@@ -17,20 +17,32 @@ import { detectLanguageId, fileToUri } from "./utils.js";
|
|
|
17
17
|
|
|
18
18
|
const clients = new Map<string, LspClient>();
|
|
19
19
|
|
|
20
|
-
// Idle timeout
|
|
21
|
-
|
|
20
|
+
// Idle timeout configuration (disabled by default)
|
|
21
|
+
let idleTimeoutMs: number | null = null;
|
|
22
|
+
let idleCheckInterval: Timer | null = null;
|
|
22
23
|
const IDLE_CHECK_INTERVAL_MS = 60 * 1000;
|
|
23
24
|
|
|
24
|
-
|
|
25
|
-
|
|
25
|
+
/**
|
|
26
|
+
* Configure the idle timeout for LSP clients.
|
|
27
|
+
* @param ms - Timeout in milliseconds, or null/undefined to disable
|
|
28
|
+
*/
|
|
29
|
+
export function setIdleTimeout(ms: number | null | undefined): void {
|
|
30
|
+
idleTimeoutMs = ms ?? null;
|
|
31
|
+
|
|
32
|
+
if (idleTimeoutMs && idleTimeoutMs > 0) {
|
|
33
|
+
startIdleChecker();
|
|
34
|
+
} else {
|
|
35
|
+
stopIdleChecker();
|
|
36
|
+
}
|
|
37
|
+
}
|
|
26
38
|
|
|
27
39
|
function startIdleChecker(): void {
|
|
28
40
|
if (idleCheckInterval) return;
|
|
29
41
|
idleCheckInterval = setInterval(() => {
|
|
42
|
+
if (!idleTimeoutMs) return;
|
|
30
43
|
const now = Date.now();
|
|
31
44
|
for (const [key, client] of Array.from(clients.entries())) {
|
|
32
|
-
if (now - client.lastActivity >
|
|
33
|
-
console.log(`[LSP] Shutting down idle client: ${key}`);
|
|
45
|
+
if (now - client.lastActivity > idleTimeoutMs) {
|
|
34
46
|
shutdownClient(key);
|
|
35
47
|
}
|
|
36
48
|
}
|
|
@@ -98,6 +110,12 @@ const CLIENT_CAPABILITIES = {
|
|
|
98
110
|
properties: ["edit"],
|
|
99
111
|
},
|
|
100
112
|
},
|
|
113
|
+
formatting: {
|
|
114
|
+
dynamicRegistration: false,
|
|
115
|
+
},
|
|
116
|
+
rangeFormatting: {
|
|
117
|
+
dynamicRegistration: false,
|
|
118
|
+
},
|
|
101
119
|
publishDiagnostics: {
|
|
102
120
|
relatedInformation: true,
|
|
103
121
|
versionSupport: false,
|
|
@@ -357,7 +375,8 @@ export async function getOrCreateClient(config: ServerConfig, cwd: string): Prom
|
|
|
357
375
|
}
|
|
358
376
|
|
|
359
377
|
const args = config.args ?? [];
|
|
360
|
-
const
|
|
378
|
+
const command = config.resolvedCommand ?? config.command;
|
|
379
|
+
const proc = Bun.spawn([command, ...args], {
|
|
361
380
|
cwd,
|
|
362
381
|
stdin: "pipe",
|
|
363
382
|
stdout: "pipe",
|
|
@@ -379,16 +398,9 @@ export async function getOrCreateClient(config: ServerConfig, cwd: string): Prom
|
|
|
379
398
|
};
|
|
380
399
|
clients.set(key, client);
|
|
381
400
|
|
|
382
|
-
// Start idle checker if not already running
|
|
383
|
-
startIdleChecker();
|
|
384
|
-
|
|
385
401
|
// Register crash recovery - remove client on process exit
|
|
386
402
|
proc.exited.then(() => {
|
|
387
|
-
console.log(`[LSP] Process exited: ${key}`);
|
|
388
403
|
clients.delete(key);
|
|
389
|
-
if (clients.size === 0) {
|
|
390
|
-
stopIdleChecker();
|
|
391
|
-
}
|
|
392
404
|
});
|
|
393
405
|
|
|
394
406
|
// Start background message reader
|
|
@@ -497,10 +509,6 @@ export function shutdownClient(key: string): void {
|
|
|
497
509
|
// Kill process
|
|
498
510
|
client.process.kill();
|
|
499
511
|
clients.delete(key);
|
|
500
|
-
|
|
501
|
-
if (clients.size === 0) {
|
|
502
|
-
stopIdleChecker();
|
|
503
|
-
}
|
|
504
512
|
}
|
|
505
513
|
|
|
506
514
|
// =============================================================================
|
|
@@ -570,8 +578,6 @@ export async function sendNotification(client: LspClient, method: string, params
|
|
|
570
578
|
* Shutdown all LSP clients.
|
|
571
579
|
*/
|
|
572
580
|
export function shutdownAll(): void {
|
|
573
|
-
stopIdleChecker();
|
|
574
|
-
|
|
575
581
|
for (const client of Array.from(clients.values())) {
|
|
576
582
|
// Reject all pending requests
|
|
577
583
|
for (const pending of Array.from(client.pendingRequests.values())) {
|
|
@@ -587,6 +593,25 @@ export function shutdownAll(): void {
|
|
|
587
593
|
clients.clear();
|
|
588
594
|
}
|
|
589
595
|
|
|
596
|
+
/** Status of an LSP server */
|
|
597
|
+
export interface LspServerStatus {
|
|
598
|
+
name: string;
|
|
599
|
+
status: "connecting" | "ready" | "error";
|
|
600
|
+
fileTypes: string[];
|
|
601
|
+
error?: string;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/**
|
|
605
|
+
* Get status of all active LSP clients.
|
|
606
|
+
*/
|
|
607
|
+
export function getActiveClients(): LspServerStatus[] {
|
|
608
|
+
return Array.from(clients.values()).map((client) => ({
|
|
609
|
+
name: client.config.command,
|
|
610
|
+
status: "ready" as const,
|
|
611
|
+
fileTypes: client.config.fileTypes,
|
|
612
|
+
}));
|
|
613
|
+
}
|
|
614
|
+
|
|
590
615
|
// =============================================================================
|
|
591
616
|
// Process Cleanup
|
|
592
617
|
// =============================================================================
|