@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.
Files changed (151) hide show
  1. package/CHANGELOG.md +73 -0
  2. package/README.md +1 -1
  3. package/examples/custom-tools/subagent/index.ts +1 -1
  4. package/package.json +5 -3
  5. package/src/cli/args.ts +5 -6
  6. package/src/cli/file-processor.ts +3 -3
  7. package/src/cli/list-models.ts +2 -2
  8. package/src/cli/plugin-cli.ts +1 -1
  9. package/src/cli/session-picker.ts +2 -2
  10. package/src/cli.ts +1 -1
  11. package/src/config.ts +3 -3
  12. package/src/core/agent-session.ts +157 -15
  13. package/src/core/bash-executor.ts +50 -10
  14. package/src/core/compaction/branch-summarization.ts +5 -5
  15. package/src/core/compaction/compaction.ts +3 -3
  16. package/src/core/compaction/index.ts +3 -3
  17. package/src/core/custom-commands/bundled/review/index.ts +156 -0
  18. package/src/core/custom-commands/index.ts +15 -0
  19. package/src/core/custom-commands/loader.ts +232 -0
  20. package/src/core/custom-commands/types.ts +112 -0
  21. package/src/core/custom-tools/index.ts +3 -3
  22. package/src/core/custom-tools/loader.ts +10 -8
  23. package/src/core/custom-tools/types.ts +11 -6
  24. package/src/core/custom-tools/wrapper.ts +2 -1
  25. package/src/core/exec.ts +22 -12
  26. package/src/core/export-html/index.ts +5 -5
  27. package/src/core/file-mentions.ts +54 -0
  28. package/src/core/hooks/index.ts +5 -5
  29. package/src/core/hooks/loader.ts +21 -16
  30. package/src/core/hooks/runner.ts +6 -6
  31. package/src/core/hooks/tool-wrapper.ts +2 -2
  32. package/src/core/hooks/types.ts +12 -15
  33. package/src/core/index.ts +6 -6
  34. package/src/core/logger.ts +112 -0
  35. package/src/core/mcp/client.ts +3 -3
  36. package/src/core/mcp/config.ts +1 -1
  37. package/src/core/mcp/index.ts +12 -12
  38. package/src/core/mcp/loader.ts +2 -2
  39. package/src/core/mcp/manager.ts +6 -6
  40. package/src/core/mcp/tool-bridge.ts +3 -3
  41. package/src/core/mcp/transports/http.ts +1 -1
  42. package/src/core/mcp/transports/index.ts +2 -2
  43. package/src/core/mcp/transports/stdio.ts +1 -1
  44. package/src/core/messages.ts +22 -0
  45. package/src/core/model-registry.ts +2 -2
  46. package/src/core/model-resolver.ts +2 -2
  47. package/src/core/plugins/doctor.ts +1 -1
  48. package/src/core/plugins/index.ts +6 -6
  49. package/src/core/plugins/installer.ts +4 -4
  50. package/src/core/plugins/loader.ts +4 -9
  51. package/src/core/plugins/manager.ts +5 -5
  52. package/src/core/plugins/paths.ts +3 -3
  53. package/src/core/sdk.ts +77 -35
  54. package/src/core/session-manager.ts +6 -6
  55. package/src/core/settings-manager.ts +16 -3
  56. package/src/core/skills.ts +5 -5
  57. package/src/core/slash-commands.ts +60 -45
  58. package/src/core/system-prompt.ts +6 -6
  59. package/src/core/title-generator.ts +2 -2
  60. package/src/core/tools/bash.ts +32 -155
  61. package/src/core/tools/context.ts +2 -2
  62. package/src/core/tools/edit-diff.ts +3 -3
  63. package/src/core/tools/edit.ts +18 -5
  64. package/src/core/tools/exa/company.ts +3 -3
  65. package/src/core/tools/exa/index.ts +16 -17
  66. package/src/core/tools/exa/linkedin.ts +3 -3
  67. package/src/core/tools/exa/mcp-client.ts +9 -9
  68. package/src/core/tools/exa/render.ts +5 -5
  69. package/src/core/tools/exa/researcher.ts +3 -3
  70. package/src/core/tools/exa/search.ts +6 -5
  71. package/src/core/tools/exa/types.ts +5 -6
  72. package/src/core/tools/exa/websets.ts +3 -3
  73. package/src/core/tools/find.ts +3 -3
  74. package/src/core/tools/grep.ts +3 -3
  75. package/src/core/tools/index.ts +48 -34
  76. package/src/core/tools/ls.ts +4 -4
  77. package/src/core/tools/lsp/client.ts +161 -90
  78. package/src/core/tools/lsp/config.ts +1 -1
  79. package/src/core/tools/lsp/edits.ts +2 -2
  80. package/src/core/tools/lsp/index.ts +15 -13
  81. package/src/core/tools/lsp/render.ts +2 -2
  82. package/src/core/tools/lsp/rust-analyzer.ts +3 -3
  83. package/src/core/tools/lsp/utils.ts +1 -1
  84. package/src/core/tools/notebook.ts +1 -1
  85. package/src/core/tools/output.ts +175 -0
  86. package/src/core/tools/read.ts +7 -7
  87. package/src/core/tools/renderers.ts +92 -13
  88. package/src/core/tools/review.ts +268 -0
  89. package/src/core/tools/task/agents.ts +1 -1
  90. package/src/core/tools/task/bundled-agents/reviewer.md +52 -37
  91. package/src/core/tools/task/discovery.ts +2 -2
  92. package/src/core/tools/task/executor.ts +145 -28
  93. package/src/core/tools/task/index.ts +78 -30
  94. package/src/core/tools/task/model-resolver.ts +30 -20
  95. package/src/core/tools/task/parallel.ts +1 -1
  96. package/src/core/tools/task/render.ts +219 -30
  97. package/src/core/tools/task/subprocess-tool-registry.ts +89 -0
  98. package/src/core/tools/task/types.ts +36 -2
  99. package/src/core/tools/web-fetch.ts +5 -3
  100. package/src/core/tools/web-search/auth.ts +1 -1
  101. package/src/core/tools/web-search/index.ts +17 -15
  102. package/src/core/tools/web-search/providers/anthropic.ts +2 -2
  103. package/src/core/tools/web-search/providers/exa.ts +3 -5
  104. package/src/core/tools/web-search/providers/perplexity.ts +1 -1
  105. package/src/core/tools/web-search/render.ts +3 -3
  106. package/src/core/tools/write.ts +4 -4
  107. package/src/index.ts +29 -18
  108. package/src/main.ts +37 -32
  109. package/src/migrations.ts +3 -3
  110. package/src/modes/index.ts +5 -5
  111. package/src/modes/interactive/components/armin.ts +1 -1
  112. package/src/modes/interactive/components/assistant-message.ts +1 -1
  113. package/src/modes/interactive/components/bash-execution.ts +4 -4
  114. package/src/modes/interactive/components/bordered-loader.ts +2 -2
  115. package/src/modes/interactive/components/branch-summary-message.ts +2 -2
  116. package/src/modes/interactive/components/compaction-summary-message.ts +2 -2
  117. package/src/modes/interactive/components/diff.ts +1 -1
  118. package/src/modes/interactive/components/dynamic-border.ts +1 -1
  119. package/src/modes/interactive/components/footer.ts +5 -5
  120. package/src/modes/interactive/components/hook-editor.ts +2 -2
  121. package/src/modes/interactive/components/hook-input.ts +2 -2
  122. package/src/modes/interactive/components/hook-message.ts +3 -3
  123. package/src/modes/interactive/components/hook-selector.ts +2 -2
  124. package/src/modes/interactive/components/model-selector.ts +281 -59
  125. package/src/modes/interactive/components/oauth-selector.ts +3 -3
  126. package/src/modes/interactive/components/plugin-settings.ts +4 -4
  127. package/src/modes/interactive/components/queue-mode-selector.ts +2 -2
  128. package/src/modes/interactive/components/session-selector.ts +4 -4
  129. package/src/modes/interactive/components/settings-defs.ts +1 -1
  130. package/src/modes/interactive/components/settings-selector.ts +5 -5
  131. package/src/modes/interactive/components/show-images-selector.ts +2 -2
  132. package/src/modes/interactive/components/theme-selector.ts +2 -2
  133. package/src/modes/interactive/components/thinking-selector.ts +2 -2
  134. package/src/modes/interactive/components/tool-execution.ts +26 -8
  135. package/src/modes/interactive/components/tree-selector.ts +3 -3
  136. package/src/modes/interactive/components/user-message-selector.ts +2 -2
  137. package/src/modes/interactive/components/user-message.ts +1 -1
  138. package/src/modes/interactive/components/welcome.ts +2 -2
  139. package/src/modes/interactive/interactive-mode.ts +85 -41
  140. package/src/modes/interactive/theme/theme.ts +8 -7
  141. package/src/modes/print-mode.ts +4 -3
  142. package/src/modes/rpc/rpc-client.ts +4 -4
  143. package/src/modes/rpc/rpc-mode.ts +21 -11
  144. package/src/modes/rpc/rpc-types.ts +3 -3
  145. package/src/utils/changelog.ts +2 -2
  146. package/src/utils/clipboard.ts +1 -1
  147. package/src/utils/shell-snapshot.ts +218 -0
  148. package/src/utils/shell.ts +93 -13
  149. package/src/utils/tools-manager.ts +1 -1
  150. package/examples/custom-tools/subagent/agents/reviewer.md +0 -35
  151. package/src/core/tools/exa/logger.ts +0 -56
@@ -1,12 +1,12 @@
1
- export { type AskToolDetails, askTool, createAskTool } from "./ask.js";
2
- export { type BashToolDetails, bashTool, createBashTool } from "./bash.js";
3
- export { createEditTool, type EditToolOptions, editTool } from "./edit.js";
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.js";
6
- export type { ExaRenderDetails, ExaSearchResponse, ExaSearchResult } from "./exa/types.js";
7
- export { createFindTool, type FindToolDetails, findTool } from "./find.js";
8
- export { createGrepTool, type GrepToolDetails, grepTool } from "./grep.js";
9
- export { createLsTool, type LsToolDetails, lsTool } from "./ls.js";
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.js";
23
- export { createNotebookTool, type NotebookToolDetails, notebookTool } from "./notebook.js";
24
- export { createReadTool, type ReadToolDetails, readTool } from "./read.js";
25
- export { BUNDLED_AGENTS, createTaskTool, taskTool } from "./task/index.js";
26
- export type { TruncationResult } from "./truncate.js";
27
- export { createWebFetchTool, type WebFetchToolDetails, webFetchCustomTool, webFetchTool } from "./web-fetch.js";
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.js";
46
- export { createWriteTool, type WriteToolDetails, type WriteToolOptions, writeTool } from "./write.js";
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.js";
50
- import { bashTool, createBashTool } from "./bash.js";
51
- import { checkBashInterception, checkSimpleLsInterception } from "./bash-interceptor.js";
52
- import { createEditTool, editTool } from "./edit.js";
53
- import { createFindTool, findTool } from "./find.js";
54
- import { createGrepTool, grepTool } from "./grep.js";
55
- import { createLsTool, lsTool } from "./ls.js";
56
- import { createLspTool, formatFile, getDiagnosticsForFile, lspTool } from "./lsp/index.js";
57
- import { createNotebookTool, notebookTool } from "./notebook.js";
58
- import { createReadTool, readTool } from "./read.js";
59
- import { createTaskTool, taskTool } from "./task/index.js";
60
- import { createWebFetchTool, webFetchTool } from "./web-fetch.js";
61
- import { createWebSearchTool, webSearchTool } from "./web-search/index.js";
62
- import { createWriteTool, writeTool } from "./write.js";
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
- task: { tool: taskTool, create: (cwd, ctx) => createTaskTool(cwd, ctx) },
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
- return names.map((name) => toolDefs[name].create(cwd, sessionContext, options));
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
- return names.map((name) => toolDefs[name].create(cwd, sessionContext, options));
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, options)]),
217
+ Object.entries(toolDefs).map(([name, def]) => [name, def.create(cwd, sessionContext, optionsWithTools)]),
204
218
  ) as Record<ToolName, Tool>;
205
219
  }
206
220
 
@@ -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 { existsSync, readdirSync, statSync } from "fs";
4
- import nodePath from "path";
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.js";
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.js";
12
- import { detectLanguageId, fileToUri } from "./utils.js";
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
- client.messageBuffer = concatBuffers(client.messageBuffer, value);
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
- let parsed = parseMessage(client.messageBuffer);
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
- client.messageBuffer = remaining;
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(client.messageBuffer);
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 (clients.has(key)) {
372
- const client = clients.get(key)!;
373
- client.lastActivity = Date.now();
374
- return client;
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
- const args = config.args ?? [];
378
- const command = config.resolvedCommand ?? config.command;
379
- const proc = Bun.spawn([command, ...args], {
380
- cwd,
381
- stdin: "pipe",
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
- const client: LspClient = {
387
- name: key,
388
- cwd,
389
- process: proc,
390
- config,
391
- requestId: 0,
392
- diagnostics: new Map(),
393
- openFiles: new Map(),
394
- pendingRequests: new Map(),
395
- messageBuffer: new Uint8Array(0),
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
- // Register crash recovery - remove client on process exit
402
- proc.exited.then(() => {
403
- clients.delete(key);
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
- // Start background message reader
407
- startMessageReader(client);
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
- try {
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
- client.serverCapabilities = initResult.capabilities as LspClient["serverCapabilities"];
445
+ // Send initialized notification
446
+ await sendNotification(client, "initialized", {});
425
447
 
426
- // Send initialized notification
427
- await sendNotification(client, "initialized", {});
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
- return client;
430
- } catch (err) {
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
- const content = fs.readFileSync(filePath, "utf-8");
449
- const languageId = detectLanguageId(filePath);
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
- await sendNotification(client, "textDocument/didOpen", {
452
- textDocument: {
453
- uri,
454
- languageId,
455
- version: 1,
456
- text: content,
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
- client.openFiles.set(uri, { version: 1, languageId });
461
- client.lastActivity = Date.now();
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 info = client.openFiles.get(uri);
521
+ const lockKey = `${client.name}:${uri}`;
471
522
 
472
- if (!info) {
473
- await ensureFileOpen(client, filePath);
474
- return;
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
- const content = fs.readFileSync(filePath, "utf-8");
478
- info.version++;
529
+ // Lock and refresh file
530
+ const refreshPromise = (async () => {
531
+ const info = client.openFiles.get(uri);
479
532
 
480
- await sendNotification(client, "textDocument/didChange", {
481
- textDocument: { uri, version: info.version },
482
- contentChanges: [{ text: content }],
483
- });
533
+ if (!info) {
534
+ await ensureFileOpen(client, filePath);
535
+ return;
536
+ }
484
537
 
485
- await sendNotification(client, "textDocument/didSave", {
486
- textDocument: { uri },
487
- text: content,
488
- });
538
+ const content = fs.readFileSync(filePath, "utf-8");
539
+ const version = ++info.version;
489
540
 
490
- client.lastActivity = Date.now();
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.js";
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.js";
4
- import { uriToFile } from "./utils.js";
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.js";
5
- import { resolveToCwd } from "../path-utils.js";
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.js";
15
- import { getServerForFile, getServersForFile, hasCapability, type LspConfig, loadConfig } from "./config.js";
16
- import { applyTextEdits, applyWorkspaceEdit } from "./edits.js";
17
- import { renderCall, renderResult } from "./render.js";
18
- import * as rustAnalyzer from "./rust-analyzer.js";
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.js";
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.js";
52
+ } from "./utils";
52
53
 
53
- export type { LspServerStatus } from "./client.js";
54
- export type { LspToolDetails } from "./types.js";
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 (e) {
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") + `\n... and ${lines.length - 50} more lines`, projectType };
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.js";
15
- import type { LspParams, LspToolDetails } from "./types.js";
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.js";
2
- import type { Diagnostic, ExpandMacroResult, LspClient, RelatedTest, Runnable, WorkspaceEdit } from "./types.js";
3
- import { fileToUri } from "./utils.js";
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.
@@ -8,7 +8,7 @@ import type {
8
8
  SymbolKind,
9
9
  TextEdit,
10
10
  WorkspaceEdit,
11
- } from "./types.js";
11
+ } from "./types";
12
12
 
13
13
  // =============================================================================
14
14
  // Language Detection
@@ -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.js";
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")], {