@oh-my-pi/pi-coding-agent 1.341.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 +73 -0
- package/README.md +1 -1
- package/examples/custom-tools/subagent/index.ts +1 -1
- package/package.json +5 -3
- package/src/cli/args.ts +5 -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 +157 -15
- 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 +2 -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 +77 -35
- package/src/core/session-manager.ts +6 -6
- package/src/core/settings-manager.ts +16 -3
- 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 +2 -2
- package/src/core/tools/bash.ts +32 -155
- package/src/core/tools/context.ts +2 -2
- package/src/core/tools/edit-diff.ts +3 -3
- package/src/core/tools/edit.ts +18 -5
- 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 +3 -3
- package/src/core/tools/index.ts +48 -34
- package/src/core/tools/ls.ts +4 -4
- package/src/core/tools/lsp/client.ts +161 -90
- package/src/core/tools/lsp/config.ts +1 -1
- package/src/core/tools/lsp/edits.ts +2 -2
- package/src/core/tools/lsp/index.ts +15 -13
- package/src/core/tools/lsp/render.ts +2 -2
- package/src/core/tools/lsp/rust-analyzer.ts +3 -3
- 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/reviewer.md +52 -37
- 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 +30 -20
- 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 +4 -4
- package/src/index.ts +29 -18
- package/src/main.ts +37 -32
- 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 +281 -59
- 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 +4 -4
- package/src/modes/interactive/components/settings-defs.ts +1 -1
- package/src/modes/interactive/components/settings-selector.ts +5 -5
- 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 +26 -8
- package/src/modes/interactive/components/tree-selector.ts +3 -3
- 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 +2 -2
- package/src/modes/interactive/interactive-mode.ts +85 -41
- 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
package/src/core/tools/index.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
export { type AskToolDetails, askTool, createAskTool } from "./ask
|
|
2
|
-
export { type BashToolDetails, bashTool, createBashTool } from "./bash
|
|
3
|
-
export { createEditTool, type EditToolOptions, editTool } from "./edit
|
|
1
|
+
export { type AskToolDetails, askTool, createAskTool } from "./ask";
|
|
2
|
+
export { type BashToolDetails, bashTool, createBashTool } from "./bash";
|
|
3
|
+
export { createEditTool, type EditToolOptions, editTool } from "./edit";
|
|
4
4
|
// Exa MCP tools (22 tools)
|
|
5
|
-
export { exaTools } from "./exa/index
|
|
6
|
-
export type { ExaRenderDetails, ExaSearchResponse, ExaSearchResult } from "./exa/types
|
|
7
|
-
export { createFindTool, type FindToolDetails, findTool } from "./find
|
|
8
|
-
export { createGrepTool, type GrepToolDetails, grepTool } from "./grep
|
|
9
|
-
export { createLsTool, type LsToolDetails, lsTool } from "./ls
|
|
5
|
+
export { exaTools } from "./exa/index";
|
|
6
|
+
export type { ExaRenderDetails, ExaSearchResponse, ExaSearchResult } from "./exa/types";
|
|
7
|
+
export { createFindTool, type FindToolDetails, findTool } from "./find";
|
|
8
|
+
export { createGrepTool, type GrepToolDetails, grepTool } from "./grep";
|
|
9
|
+
export { createLsTool, type LsToolDetails, lsTool } from "./ls";
|
|
10
10
|
export {
|
|
11
11
|
createLspTool,
|
|
12
12
|
type FileDiagnosticsResult,
|
|
@@ -19,12 +19,14 @@ export {
|
|
|
19
19
|
type LspWarmupResult,
|
|
20
20
|
lspTool,
|
|
21
21
|
warmupLspServers,
|
|
22
|
-
} from "./lsp/index
|
|
23
|
-
export { createNotebookTool, type NotebookToolDetails, notebookTool } from "./notebook
|
|
24
|
-
export {
|
|
25
|
-
export {
|
|
26
|
-
export
|
|
27
|
-
export {
|
|
22
|
+
} from "./lsp/index";
|
|
23
|
+
export { createNotebookTool, type NotebookToolDetails, notebookTool } from "./notebook";
|
|
24
|
+
export { createOutputTool, type OutputToolDetails, outputTool } from "./output";
|
|
25
|
+
export { createReadTool, type ReadToolDetails, readTool } from "./read";
|
|
26
|
+
export { createReportFindingTool, createSubmitReviewTool, reportFindingTool, submitReviewTool } from "./review";
|
|
27
|
+
export { BUNDLED_AGENTS, createTaskTool, taskTool } from "./task/index";
|
|
28
|
+
export type { TruncationResult } from "./truncate";
|
|
29
|
+
export { createWebFetchTool, type WebFetchToolDetails, webFetchCustomTool, webFetchTool } from "./web-fetch";
|
|
28
30
|
export {
|
|
29
31
|
companyWebSearchTools,
|
|
30
32
|
createWebSearchTool,
|
|
@@ -42,24 +44,26 @@ export {
|
|
|
42
44
|
webSearchDeepTool,
|
|
43
45
|
webSearchLinkedinTool,
|
|
44
46
|
webSearchTool,
|
|
45
|
-
} from "./web-search/index
|
|
46
|
-
export { createWriteTool, type WriteToolDetails, type WriteToolOptions, writeTool } from "./write
|
|
47
|
+
} from "./web-search/index";
|
|
48
|
+
export { createWriteTool, type WriteToolDetails, type WriteToolOptions, writeTool } from "./write";
|
|
47
49
|
|
|
48
50
|
import type { AgentTool } from "@oh-my-pi/pi-agent-core";
|
|
49
|
-
import { askTool, createAskTool } from "./ask
|
|
50
|
-
import { bashTool, createBashTool } from "./bash
|
|
51
|
-
import { checkBashInterception, checkSimpleLsInterception } from "./bash-interceptor
|
|
52
|
-
import { createEditTool, editTool } from "./edit
|
|
53
|
-
import { createFindTool, findTool } from "./find
|
|
54
|
-
import { createGrepTool, grepTool } from "./grep
|
|
55
|
-
import { createLsTool, lsTool } from "./ls
|
|
56
|
-
import { createLspTool, formatFile, getDiagnosticsForFile, lspTool } from "./lsp/index
|
|
57
|
-
import { createNotebookTool, notebookTool } from "./notebook
|
|
58
|
-
import {
|
|
59
|
-
import {
|
|
60
|
-
import {
|
|
61
|
-
import {
|
|
62
|
-
import {
|
|
51
|
+
import { askTool, createAskTool } from "./ask";
|
|
52
|
+
import { bashTool, createBashTool } from "./bash";
|
|
53
|
+
import { checkBashInterception, checkSimpleLsInterception } from "./bash-interceptor";
|
|
54
|
+
import { createEditTool, editTool } from "./edit";
|
|
55
|
+
import { createFindTool, findTool } from "./find";
|
|
56
|
+
import { createGrepTool, grepTool } from "./grep";
|
|
57
|
+
import { createLsTool, lsTool } from "./ls";
|
|
58
|
+
import { createLspTool, formatFile, getDiagnosticsForFile, lspTool } from "./lsp/index";
|
|
59
|
+
import { createNotebookTool, notebookTool } from "./notebook";
|
|
60
|
+
import { createOutputTool, outputTool } from "./output";
|
|
61
|
+
import { createReadTool, readTool } from "./read";
|
|
62
|
+
import { createReportFindingTool, createSubmitReviewTool, reportFindingTool, submitReviewTool } from "./review";
|
|
63
|
+
import { createTaskTool, taskTool } from "./task/index";
|
|
64
|
+
import { createWebFetchTool, webFetchTool } from "./web-fetch";
|
|
65
|
+
import { createWebSearchTool, webSearchTool } from "./web-search/index";
|
|
66
|
+
import { createWriteTool, writeTool } from "./write";
|
|
63
67
|
|
|
64
68
|
/** Tool type (AgentTool from pi-ai) */
|
|
65
69
|
export type Tool = AgentTool<any, any, any>;
|
|
@@ -79,6 +83,8 @@ export interface CodingToolsOptions {
|
|
|
79
83
|
lspFormatOnWrite?: boolean;
|
|
80
84
|
/** Whether to accept high-confidence fuzzy matches in edit tool (default: true) */
|
|
81
85
|
editFuzzyMatch?: boolean;
|
|
86
|
+
/** Set of tool names available to the agent (for cross-tool awareness) */
|
|
87
|
+
availableTools?: Set<string>;
|
|
82
88
|
}
|
|
83
89
|
|
|
84
90
|
// Factory function type
|
|
@@ -115,9 +121,12 @@ const toolDefs: Record<string, { tool: Tool; create: ToolFactory }> = {
|
|
|
115
121
|
ls: { tool: lsTool, create: createLsTool },
|
|
116
122
|
lsp: { tool: lspTool, create: createLspTool },
|
|
117
123
|
notebook: { tool: notebookTool, create: createNotebookTool },
|
|
118
|
-
|
|
124
|
+
output: { tool: outputTool, create: (cwd, ctx) => createOutputTool(cwd, ctx) },
|
|
125
|
+
task: { tool: taskTool, create: (cwd, ctx, opts) => createTaskTool(cwd, ctx, opts) },
|
|
119
126
|
web_fetch: { tool: webFetchTool, create: createWebFetchTool },
|
|
120
127
|
web_search: { tool: webSearchTool, create: createWebSearchTool },
|
|
128
|
+
report_finding: { tool: reportFindingTool, create: createReportFindingTool },
|
|
129
|
+
submit_review: { tool: submitReviewTool, create: createSubmitReviewTool },
|
|
121
130
|
};
|
|
122
131
|
|
|
123
132
|
export type ToolName = keyof typeof toolDefs;
|
|
@@ -136,6 +145,7 @@ const baseCodingToolNames: ToolName[] = [
|
|
|
136
145
|
"ls",
|
|
137
146
|
"lsp",
|
|
138
147
|
"notebook",
|
|
148
|
+
"output",
|
|
139
149
|
"task",
|
|
140
150
|
"web_fetch",
|
|
141
151
|
"web_search",
|
|
@@ -168,7 +178,8 @@ export function createCodingTools(
|
|
|
168
178
|
options?: CodingToolsOptions,
|
|
169
179
|
): Tool[] {
|
|
170
180
|
const names = hasUI ? [...baseCodingToolNames, ...uiToolNames] : baseCodingToolNames;
|
|
171
|
-
|
|
181
|
+
const optionsWithTools = { ...options, availableTools: new Set(names) };
|
|
182
|
+
return names.map((name) => toolDefs[name].create(cwd, sessionContext, optionsWithTools));
|
|
172
183
|
}
|
|
173
184
|
|
|
174
185
|
/**
|
|
@@ -185,7 +196,8 @@ export function createReadOnlyTools(
|
|
|
185
196
|
options?: CodingToolsOptions,
|
|
186
197
|
): Tool[] {
|
|
187
198
|
const names = hasUI ? [...baseReadOnlyToolNames, ...uiToolNames] : baseReadOnlyToolNames;
|
|
188
|
-
|
|
199
|
+
const optionsWithTools = { ...options, availableTools: new Set(names) };
|
|
200
|
+
return names.map((name) => toolDefs[name].create(cwd, sessionContext, optionsWithTools));
|
|
189
201
|
}
|
|
190
202
|
|
|
191
203
|
/**
|
|
@@ -199,8 +211,10 @@ export function createAllTools(
|
|
|
199
211
|
sessionContext?: SessionContext,
|
|
200
212
|
options?: CodingToolsOptions,
|
|
201
213
|
): Record<ToolName, Tool> {
|
|
214
|
+
const names = Object.keys(toolDefs);
|
|
215
|
+
const optionsWithTools = { ...options, availableTools: new Set(names) };
|
|
202
216
|
return Object.fromEntries(
|
|
203
|
-
Object.entries(toolDefs).map(([name, def]) => [name, def.create(cwd, sessionContext,
|
|
217
|
+
Object.entries(toolDefs).map(([name, def]) => [name, def.create(cwd, sessionContext, optionsWithTools)]),
|
|
204
218
|
) as Record<ToolName, Tool>;
|
|
205
219
|
}
|
|
206
220
|
|
package/src/core/tools/ls.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
+
import { existsSync, readdirSync, statSync } from "node:fs";
|
|
2
|
+
import nodePath from "node:path";
|
|
1
3
|
import type { AgentTool } from "@oh-my-pi/pi-agent-core";
|
|
2
4
|
import { Type } from "@sinclair/typebox";
|
|
3
|
-
import {
|
|
4
|
-
import
|
|
5
|
-
import { resolveToCwd } from "./path-utils.js";
|
|
6
|
-
import { DEFAULT_MAX_BYTES, formatSize, type TruncationResult, truncateHead } from "./truncate.js";
|
|
5
|
+
import { resolveToCwd } from "./path-utils";
|
|
6
|
+
import { DEFAULT_MAX_BYTES, formatSize, type TruncationResult, truncateHead } from "./truncate";
|
|
7
7
|
|
|
8
8
|
const lsSchema = Type.Object({
|
|
9
9
|
path: Type.Optional(Type.String({ description: "Directory to list (default: current directory)" })),
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
|
-
import { applyWorkspaceEdit } from "./edits
|
|
2
|
+
import { applyWorkspaceEdit } from "./edits";
|
|
3
3
|
import type {
|
|
4
4
|
Diagnostic,
|
|
5
5
|
LspClient,
|
|
@@ -8,14 +8,16 @@ import type {
|
|
|
8
8
|
LspJsonRpcResponse,
|
|
9
9
|
ServerConfig,
|
|
10
10
|
WorkspaceEdit,
|
|
11
|
-
} from "./types
|
|
12
|
-
import { detectLanguageId, fileToUri } from "./utils
|
|
11
|
+
} from "./types";
|
|
12
|
+
import { detectLanguageId, fileToUri } from "./utils";
|
|
13
13
|
|
|
14
14
|
// =============================================================================
|
|
15
15
|
// Client State
|
|
16
16
|
// =============================================================================
|
|
17
17
|
|
|
18
18
|
const clients = new Map<string, LspClient>();
|
|
19
|
+
const clientLocks = new Map<string, Promise<LspClient>>();
|
|
20
|
+
const fileOperationLocks = new Map<string, Promise<void>>();
|
|
19
21
|
|
|
20
22
|
// Idle timeout configuration (disabled by default)
|
|
21
23
|
let idleTimeoutMs: number | null = null;
|
|
@@ -233,13 +235,17 @@ async function startMessageReader(client: LspClient): Promise<void> {
|
|
|
233
235
|
const { done, value } = await reader.read();
|
|
234
236
|
if (done) break;
|
|
235
237
|
|
|
236
|
-
|
|
238
|
+
// Atomically update buffer before processing
|
|
239
|
+
const currentBuffer = concatBuffers(client.messageBuffer, value);
|
|
240
|
+
client.messageBuffer = currentBuffer;
|
|
237
241
|
|
|
238
242
|
// Process all complete messages in buffer
|
|
239
|
-
|
|
243
|
+
// Use local variable to avoid race with concurrent buffer updates
|
|
244
|
+
let workingBuffer = currentBuffer;
|
|
245
|
+
let parsed = parseMessage(workingBuffer);
|
|
240
246
|
while (parsed) {
|
|
241
247
|
const { message, remaining } = parsed;
|
|
242
|
-
|
|
248
|
+
workingBuffer = remaining;
|
|
243
249
|
|
|
244
250
|
// Route message
|
|
245
251
|
if ("id" in message && message.id !== undefined) {
|
|
@@ -263,8 +269,11 @@ async function startMessageReader(client: LspClient): Promise<void> {
|
|
|
263
269
|
}
|
|
264
270
|
}
|
|
265
271
|
|
|
266
|
-
parsed = parseMessage(
|
|
272
|
+
parsed = parseMessage(workingBuffer);
|
|
267
273
|
}
|
|
274
|
+
|
|
275
|
+
// Atomically commit processed buffer
|
|
276
|
+
client.messageBuffer = workingBuffer;
|
|
268
277
|
}
|
|
269
278
|
} catch (err) {
|
|
270
279
|
// Connection closed or error - reject all pending requests
|
|
@@ -368,71 +377,88 @@ async function sendResponse(
|
|
|
368
377
|
export async function getOrCreateClient(config: ServerConfig, cwd: string): Promise<LspClient> {
|
|
369
378
|
const key = `${config.command}:${cwd}`;
|
|
370
379
|
|
|
371
|
-
if
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
380
|
+
// Check if client already exists
|
|
381
|
+
const existingClient = clients.get(key);
|
|
382
|
+
if (existingClient) {
|
|
383
|
+
existingClient.lastActivity = Date.now();
|
|
384
|
+
return existingClient;
|
|
375
385
|
}
|
|
376
386
|
|
|
377
|
-
|
|
378
|
-
const
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
stdout: "pipe",
|
|
383
|
-
stderr: "pipe",
|
|
384
|
-
});
|
|
387
|
+
// Check if another coroutine is already creating this client
|
|
388
|
+
const existingLock = clientLocks.get(key);
|
|
389
|
+
if (existingLock) {
|
|
390
|
+
return existingLock;
|
|
391
|
+
}
|
|
385
392
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
isReading: false,
|
|
397
|
-
lastActivity: Date.now(),
|
|
398
|
-
};
|
|
399
|
-
clients.set(key, client);
|
|
393
|
+
// Create new client with lock
|
|
394
|
+
const clientPromise = (async () => {
|
|
395
|
+
const args = config.args ?? [];
|
|
396
|
+
const command = config.resolvedCommand ?? config.command;
|
|
397
|
+
const proc = Bun.spawn([command, ...args], {
|
|
398
|
+
cwd,
|
|
399
|
+
stdin: "pipe",
|
|
400
|
+
stdout: "pipe",
|
|
401
|
+
stderr: "pipe",
|
|
402
|
+
});
|
|
400
403
|
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
404
|
+
const client: LspClient = {
|
|
405
|
+
name: key,
|
|
406
|
+
cwd,
|
|
407
|
+
process: proc,
|
|
408
|
+
config,
|
|
409
|
+
requestId: 0,
|
|
410
|
+
diagnostics: new Map(),
|
|
411
|
+
openFiles: new Map(),
|
|
412
|
+
pendingRequests: new Map(),
|
|
413
|
+
messageBuffer: new Uint8Array(0),
|
|
414
|
+
isReading: false,
|
|
415
|
+
lastActivity: Date.now(),
|
|
416
|
+
};
|
|
417
|
+
clients.set(key, client);
|
|
418
|
+
|
|
419
|
+
// Register crash recovery - remove client on process exit
|
|
420
|
+
proc.exited.then(() => {
|
|
421
|
+
clients.delete(key);
|
|
422
|
+
clientLocks.delete(key);
|
|
423
|
+
});
|
|
405
424
|
|
|
406
|
-
|
|
407
|
-
|
|
425
|
+
// Start background message reader
|
|
426
|
+
startMessageReader(client);
|
|
427
|
+
|
|
428
|
+
try {
|
|
429
|
+
// Send initialize request
|
|
430
|
+
const initResult = (await sendRequest(client, "initialize", {
|
|
431
|
+
processId: process.pid,
|
|
432
|
+
rootUri: fileToUri(cwd),
|
|
433
|
+
rootPath: cwd,
|
|
434
|
+
capabilities: CLIENT_CAPABILITIES,
|
|
435
|
+
initializationOptions: config.initOptions ?? {},
|
|
436
|
+
workspaceFolders: [{ uri: fileToUri(cwd), name: cwd.split("/").pop() ?? "workspace" }],
|
|
437
|
+
})) as { capabilities?: unknown };
|
|
438
|
+
|
|
439
|
+
if (!initResult) {
|
|
440
|
+
throw new Error("Failed to initialize LSP: no response");
|
|
441
|
+
}
|
|
408
442
|
|
|
409
|
-
|
|
410
|
-
// Send initialize request
|
|
411
|
-
const initResult = (await sendRequest(client, "initialize", {
|
|
412
|
-
processId: process.pid,
|
|
413
|
-
rootUri: fileToUri(cwd),
|
|
414
|
-
rootPath: cwd,
|
|
415
|
-
capabilities: CLIENT_CAPABILITIES,
|
|
416
|
-
initializationOptions: config.initOptions ?? {},
|
|
417
|
-
workspaceFolders: [{ uri: fileToUri(cwd), name: cwd.split("/").pop() ?? "workspace" }],
|
|
418
|
-
})) as { capabilities?: unknown };
|
|
419
|
-
|
|
420
|
-
if (!initResult) {
|
|
421
|
-
throw new Error("Failed to initialize LSP: no response");
|
|
422
|
-
}
|
|
443
|
+
client.serverCapabilities = initResult.capabilities as LspClient["serverCapabilities"];
|
|
423
444
|
|
|
424
|
-
|
|
445
|
+
// Send initialized notification
|
|
446
|
+
await sendNotification(client, "initialized", {});
|
|
425
447
|
|
|
426
|
-
|
|
427
|
-
|
|
448
|
+
return client;
|
|
449
|
+
} catch (err) {
|
|
450
|
+
// Clean up on initialization failure
|
|
451
|
+
clients.delete(key);
|
|
452
|
+
clientLocks.delete(key);
|
|
453
|
+
proc.kill();
|
|
454
|
+
throw err;
|
|
455
|
+
} finally {
|
|
456
|
+
clientLocks.delete(key);
|
|
457
|
+
}
|
|
458
|
+
})();
|
|
428
459
|
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
// Clean up on initialization failure
|
|
432
|
-
clients.delete(key);
|
|
433
|
-
proc.kill();
|
|
434
|
-
throw err;
|
|
435
|
-
}
|
|
460
|
+
clientLocks.set(key, clientPromise);
|
|
461
|
+
return clientPromise;
|
|
436
462
|
}
|
|
437
463
|
|
|
438
464
|
/**
|
|
@@ -441,24 +467,49 @@ export async function getOrCreateClient(config: ServerConfig, cwd: string): Prom
|
|
|
441
467
|
*/
|
|
442
468
|
export async function ensureFileOpen(client: LspClient, filePath: string): Promise<void> {
|
|
443
469
|
const uri = fileToUri(filePath);
|
|
470
|
+
const lockKey = `${client.name}:${uri}`;
|
|
471
|
+
|
|
472
|
+
// Check if file is already open
|
|
444
473
|
if (client.openFiles.has(uri)) {
|
|
445
474
|
return;
|
|
446
475
|
}
|
|
447
476
|
|
|
448
|
-
|
|
449
|
-
const
|
|
477
|
+
// Check if another operation is already opening this file
|
|
478
|
+
const existingLock = fileOperationLocks.get(lockKey);
|
|
479
|
+
if (existingLock) {
|
|
480
|
+
await existingLock;
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
450
483
|
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
},
|
|
458
|
-
});
|
|
484
|
+
// Lock and open file
|
|
485
|
+
const openPromise = (async () => {
|
|
486
|
+
// Double-check after acquiring lock
|
|
487
|
+
if (client.openFiles.has(uri)) {
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
459
490
|
|
|
460
|
-
|
|
461
|
-
|
|
491
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
492
|
+
const languageId = detectLanguageId(filePath);
|
|
493
|
+
|
|
494
|
+
await sendNotification(client, "textDocument/didOpen", {
|
|
495
|
+
textDocument: {
|
|
496
|
+
uri,
|
|
497
|
+
languageId,
|
|
498
|
+
version: 1,
|
|
499
|
+
text: content,
|
|
500
|
+
},
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
client.openFiles.set(uri, { version: 1, languageId });
|
|
504
|
+
client.lastActivity = Date.now();
|
|
505
|
+
})();
|
|
506
|
+
|
|
507
|
+
fileOperationLocks.set(lockKey, openPromise);
|
|
508
|
+
try {
|
|
509
|
+
await openPromise;
|
|
510
|
+
} finally {
|
|
511
|
+
fileOperationLocks.delete(lockKey);
|
|
512
|
+
}
|
|
462
513
|
}
|
|
463
514
|
|
|
464
515
|
/**
|
|
@@ -467,27 +518,45 @@ export async function ensureFileOpen(client: LspClient, filePath: string): Promi
|
|
|
467
518
|
*/
|
|
468
519
|
export async function refreshFile(client: LspClient, filePath: string): Promise<void> {
|
|
469
520
|
const uri = fileToUri(filePath);
|
|
470
|
-
const
|
|
521
|
+
const lockKey = `${client.name}:${uri}`;
|
|
471
522
|
|
|
472
|
-
if
|
|
473
|
-
|
|
474
|
-
|
|
523
|
+
// Check if another operation is in progress
|
|
524
|
+
const existingLock = fileOperationLocks.get(lockKey);
|
|
525
|
+
if (existingLock) {
|
|
526
|
+
await existingLock;
|
|
475
527
|
}
|
|
476
528
|
|
|
477
|
-
|
|
478
|
-
|
|
529
|
+
// Lock and refresh file
|
|
530
|
+
const refreshPromise = (async () => {
|
|
531
|
+
const info = client.openFiles.get(uri);
|
|
479
532
|
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
533
|
+
if (!info) {
|
|
534
|
+
await ensureFileOpen(client, filePath);
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
484
537
|
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
text: content,
|
|
488
|
-
});
|
|
538
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
539
|
+
const version = ++info.version;
|
|
489
540
|
|
|
490
|
-
|
|
541
|
+
await sendNotification(client, "textDocument/didChange", {
|
|
542
|
+
textDocument: { uri, version },
|
|
543
|
+
contentChanges: [{ text: content }],
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
await sendNotification(client, "textDocument/didSave", {
|
|
547
|
+
textDocument: { uri },
|
|
548
|
+
text: content,
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
client.lastActivity = Date.now();
|
|
552
|
+
})();
|
|
553
|
+
|
|
554
|
+
fileOperationLocks.set(lockKey, refreshPromise);
|
|
555
|
+
try {
|
|
556
|
+
await refreshPromise;
|
|
557
|
+
} finally {
|
|
558
|
+
fileOperationLocks.delete(lockKey);
|
|
559
|
+
}
|
|
491
560
|
}
|
|
492
561
|
|
|
493
562
|
/**
|
|
@@ -519,7 +588,9 @@ export function shutdownClient(key: string): void {
|
|
|
519
588
|
* Send an LSP request and wait for response.
|
|
520
589
|
*/
|
|
521
590
|
export async function sendRequest(client: LspClient, method: string, params: unknown): Promise<unknown> {
|
|
591
|
+
// Atomically increment and capture request ID
|
|
522
592
|
const id = ++client.requestId;
|
|
593
|
+
|
|
523
594
|
const request: LspJsonRpcRequest = {
|
|
524
595
|
jsonrpc: "2.0",
|
|
525
596
|
id,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { extname, join } from "node:path";
|
|
4
|
-
import type { ServerConfig } from "./types
|
|
4
|
+
import type { ServerConfig } from "./types";
|
|
5
5
|
|
|
6
6
|
export interface LspConfig {
|
|
7
7
|
servers: Record<string, ServerConfig>;
|
|
@@ -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,8 +1,9 @@
|
|
|
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 {
|
|
4
|
+
import type { Theme } from "../../../modes/interactive/theme/theme";
|
|
5
|
+
import { logger } from "../../logger";
|
|
6
|
+
import { resolveToCwd } from "../path-utils";
|
|
6
7
|
import {
|
|
7
8
|
ensureFileOpen,
|
|
8
9
|
getActiveClients,
|
|
@@ -11,11 +12,11 @@ import {
|
|
|
11
12
|
refreshFile,
|
|
12
13
|
sendRequest,
|
|
13
14
|
setIdleTimeout,
|
|
14
|
-
} from "./client
|
|
15
|
-
import { getServerForFile, getServersForFile, hasCapability, type LspConfig, loadConfig } from "./config
|
|
16
|
-
import { applyTextEdits, applyWorkspaceEdit } from "./edits
|
|
17
|
-
import { renderCall, renderResult } from "./render
|
|
18
|
-
import * as rustAnalyzer from "./rust-analyzer
|
|
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";
|
|
19
20
|
import {
|
|
20
21
|
type CallHierarchyIncomingCall,
|
|
21
22
|
type CallHierarchyItem,
|
|
@@ -35,7 +36,7 @@ import {
|
|
|
35
36
|
type SymbolInformation,
|
|
36
37
|
type TextEdit,
|
|
37
38
|
type WorkspaceEdit,
|
|
38
|
-
} from "./types
|
|
39
|
+
} from "./types";
|
|
39
40
|
import {
|
|
40
41
|
extractHoverText,
|
|
41
42
|
fileToUri,
|
|
@@ -48,10 +49,10 @@ import {
|
|
|
48
49
|
sleep,
|
|
49
50
|
symbolKindToIcon,
|
|
50
51
|
uriToFile,
|
|
51
|
-
} from "./utils
|
|
52
|
+
} from "./utils";
|
|
52
53
|
|
|
53
|
-
export type { LspServerStatus } from "./client
|
|
54
|
-
export type { LspToolDetails } from "./types
|
|
54
|
+
export type { LspServerStatus } from "./client";
|
|
55
|
+
export type { LspToolDetails } from "./types";
|
|
55
56
|
|
|
56
57
|
/** Result from warming up LSP servers */
|
|
57
58
|
export interface LspWarmupResult {
|
|
@@ -273,7 +274,8 @@ async function runWorkspaceDiagnostics(
|
|
|
273
274
|
const formatted = collected.slice(0, 50).map((d) => formatDiagnostic(d.diagnostic, d.filePath));
|
|
274
275
|
const more = collected.length > 50 ? `\n ... and ${collected.length - 50} more` : "";
|
|
275
276
|
return { output: `${summary}:\n${formatted.map((f) => ` ${f}`).join("\n")}${more}`, projectType };
|
|
276
|
-
} catch (
|
|
277
|
+
} catch (err) {
|
|
278
|
+
logger.debug("LSP diagnostics failed, falling back to shell", { error: String(err) });
|
|
277
279
|
// Fall through to shell command
|
|
278
280
|
}
|
|
279
281
|
}
|
|
@@ -305,7 +307,7 @@ async function runWorkspaceDiagnostics(
|
|
|
305
307
|
// Limit output length
|
|
306
308
|
const lines = combined.split("\n");
|
|
307
309
|
if (lines.length > 50) {
|
|
308
|
-
return { output: lines.slice(0, 50).join("\n")
|
|
310
|
+
return { output: `${lines.slice(0, 50).join("\n")}\n... and ${lines.length - 50} more lines`, projectType };
|
|
309
311
|
}
|
|
310
312
|
|
|
311
313
|
return { output: combined, projectType };
|
|
@@ -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.
|
|
@@ -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")], {
|