@oh-my-pi/pi-coding-agent 13.18.0 → 14.0.2

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 (235) hide show
  1. package/CHANGELOG.md +316 -1
  2. package/package.json +86 -24
  3. package/scripts/format-prompts.ts +2 -2
  4. package/src/autoresearch/apply-contract-to-state.ts +24 -0
  5. package/src/autoresearch/contract.ts +0 -44
  6. package/src/autoresearch/dashboard.ts +1 -2
  7. package/src/autoresearch/git.ts +116 -30
  8. package/src/autoresearch/helpers.ts +49 -0
  9. package/src/autoresearch/index.ts +28 -187
  10. package/src/autoresearch/prompt.md +26 -9
  11. package/src/autoresearch/state.ts +0 -6
  12. package/src/autoresearch/tools/init-experiment.ts +202 -117
  13. package/src/autoresearch/tools/log-experiment.ts +123 -178
  14. package/src/autoresearch/tools/run-experiment.ts +48 -10
  15. package/src/autoresearch/types.ts +2 -2
  16. package/src/capability/index.ts +4 -2
  17. package/src/cli/file-processor.ts +3 -3
  18. package/src/cli/grep-cli.ts +8 -8
  19. package/src/cli/grievances-cli.ts +78 -0
  20. package/src/cli/read-cli.ts +67 -0
  21. package/src/cli/setup-cli.ts +4 -4
  22. package/src/cli/update-cli.ts +3 -3
  23. package/src/cli.ts +2 -0
  24. package/src/commands/grep.ts +6 -1
  25. package/src/commands/grievances.ts +20 -0
  26. package/src/commands/read.ts +33 -0
  27. package/src/commit/agentic/agent.ts +5 -8
  28. package/src/commit/agentic/index.ts +22 -26
  29. package/src/commit/agentic/tools/analyze-file.ts +3 -3
  30. package/src/commit/agentic/tools/git-file-diff.ts +3 -6
  31. package/src/commit/agentic/tools/git-hunk.ts +3 -3
  32. package/src/commit/agentic/tools/git-overview.ts +6 -9
  33. package/src/commit/agentic/tools/index.ts +6 -8
  34. package/src/commit/agentic/tools/propose-commit.ts +4 -7
  35. package/src/commit/agentic/tools/recent-commits.ts +3 -3
  36. package/src/commit/agentic/tools/split-commit.ts +4 -4
  37. package/src/commit/agentic/validation.ts +1 -1
  38. package/src/commit/analysis/conventional.ts +4 -4
  39. package/src/commit/analysis/summary.ts +3 -3
  40. package/src/commit/changelog/generate.ts +4 -4
  41. package/src/commit/changelog/index.ts +5 -9
  42. package/src/commit/map-reduce/map-phase.ts +4 -4
  43. package/src/commit/map-reduce/reduce-phase.ts +4 -4
  44. package/src/commit/pipeline.ts +13 -16
  45. package/src/config/keybindings.ts +7 -6
  46. package/src/config/prompt-templates.ts +44 -226
  47. package/src/config/resolve-config-value.ts +4 -2
  48. package/src/config/settings-schema.ts +98 -2
  49. package/src/config/settings.ts +25 -26
  50. package/src/dap/client.ts +674 -0
  51. package/src/dap/config.ts +150 -0
  52. package/src/dap/defaults.json +211 -0
  53. package/src/dap/index.ts +4 -0
  54. package/src/dap/session.ts +1255 -0
  55. package/src/dap/types.ts +600 -0
  56. package/src/debug/log-viewer.ts +3 -2
  57. package/src/discovery/builtin.ts +1 -2
  58. package/src/discovery/codex.ts +2 -2
  59. package/src/discovery/github.ts +2 -1
  60. package/src/discovery/helpers.ts +2 -2
  61. package/src/discovery/opencode.ts +2 -2
  62. package/src/edit/diff.ts +818 -0
  63. package/src/edit/index.ts +309 -0
  64. package/src/edit/line-hash.ts +67 -0
  65. package/src/edit/modes/chunk.ts +454 -0
  66. package/src/{patch → edit/modes}/hashline.ts +741 -361
  67. package/src/{patch/applicator.ts → edit/modes/patch.ts} +420 -117
  68. package/src/{patch/fuzzy.ts → edit/modes/replace.ts} +519 -197
  69. package/src/{patch → edit}/normalize.ts +97 -76
  70. package/src/{patch/shared.ts → edit/renderer.ts} +181 -108
  71. package/src/exec/bash-executor.ts +4 -2
  72. package/src/exec/idle-timeout-watchdog.ts +126 -0
  73. package/src/exec/non-interactive-env.ts +5 -0
  74. package/src/extensibility/custom-commands/bundled/ci-green/index.ts +6 -18
  75. package/src/extensibility/custom-commands/bundled/review/index.ts +45 -43
  76. package/src/extensibility/custom-commands/loader.ts +1 -2
  77. package/src/extensibility/custom-tools/loader.ts +34 -11
  78. package/src/extensibility/custom-tools/types.ts +1 -1
  79. package/src/extensibility/extensions/loader.ts +9 -4
  80. package/src/extensibility/extensions/runner.ts +24 -1
  81. package/src/extensibility/extensions/types.ts +4 -2
  82. package/src/extensibility/hooks/loader.ts +5 -6
  83. package/src/extensibility/hooks/types.ts +2 -2
  84. package/src/extensibility/plugins/doctor.ts +2 -1
  85. package/src/extensibility/plugins/marketplace/fetcher.ts +2 -57
  86. package/src/extensibility/plugins/marketplace/source-resolver.ts +4 -4
  87. package/src/extensibility/slash-commands.ts +3 -7
  88. package/src/index.ts +3 -1
  89. package/src/internal-urls/docs-index.generated.ts +11 -11
  90. package/src/ipy/executor.ts +58 -17
  91. package/src/ipy/gateway-coordinator.ts +6 -4
  92. package/src/ipy/kernel.ts +45 -22
  93. package/src/ipy/runtime.ts +2 -2
  94. package/src/lsp/client.ts +7 -4
  95. package/src/lsp/clients/lsp-linter-client.ts +4 -4
  96. package/src/lsp/config.ts +2 -2
  97. package/src/lsp/defaults.json +688 -154
  98. package/src/lsp/index.ts +234 -45
  99. package/src/lsp/lspmux.ts +2 -2
  100. package/src/lsp/startup-events.ts +13 -0
  101. package/src/lsp/types.ts +12 -1
  102. package/src/lsp/utils.ts +8 -1
  103. package/src/main.ts +125 -47
  104. package/src/memories/index.ts +4 -5
  105. package/src/modes/acp/acp-agent.ts +563 -163
  106. package/src/modes/acp/acp-event-mapper.ts +9 -1
  107. package/src/modes/acp/acp-mode.ts +4 -2
  108. package/src/modes/components/agent-dashboard.ts +3 -4
  109. package/src/modes/components/diff.ts +6 -7
  110. package/src/modes/components/footer.ts +9 -29
  111. package/src/modes/components/hook-editor.ts +3 -3
  112. package/src/modes/components/hook-selector.ts +6 -1
  113. package/src/modes/components/read-tool-group.ts +6 -12
  114. package/src/modes/components/session-observer-overlay.ts +472 -0
  115. package/src/modes/components/settings-defs.ts +24 -0
  116. package/src/modes/components/status-line.ts +15 -61
  117. package/src/modes/components/tool-execution.ts +1 -1
  118. package/src/modes/components/welcome.ts +1 -1
  119. package/src/modes/controllers/btw-controller.ts +2 -2
  120. package/src/modes/controllers/command-controller.ts +4 -2
  121. package/src/modes/controllers/event-controller.ts +59 -2
  122. package/src/modes/controllers/extension-ui-controller.ts +1 -0
  123. package/src/modes/controllers/input-controller.ts +15 -8
  124. package/src/modes/controllers/selector-controller.ts +26 -0
  125. package/src/modes/index.ts +20 -2
  126. package/src/modes/interactive-mode.ts +278 -69
  127. package/src/modes/rpc/host-tools.ts +186 -0
  128. package/src/modes/rpc/rpc-client.ts +178 -13
  129. package/src/modes/rpc/rpc-mode.ts +73 -3
  130. package/src/modes/rpc/rpc-types.ts +53 -1
  131. package/src/modes/session-observer-registry.ts +146 -0
  132. package/src/modes/shared.ts +0 -42
  133. package/src/modes/theme/theme.ts +80 -8
  134. package/src/modes/types.ts +4 -2
  135. package/src/modes/utils/keybinding-matchers.ts +9 -0
  136. package/src/prompts/system/custom-system-prompt.md +5 -0
  137. package/src/prompts/system/system-prompt.md +8 -1
  138. package/src/prompts/tools/chunk-edit.md +219 -0
  139. package/src/prompts/tools/debug.md +43 -0
  140. package/src/prompts/tools/grep.md +3 -0
  141. package/src/prompts/tools/lsp.md +5 -5
  142. package/src/prompts/tools/read-chunk.md +17 -0
  143. package/src/prompts/tools/read.md +19 -5
  144. package/src/sdk.ts +216 -165
  145. package/src/secrets/index.ts +1 -1
  146. package/src/secrets/obfuscator.ts +25 -17
  147. package/src/session/agent-session.ts +381 -286
  148. package/src/session/agent-storage.ts +12 -12
  149. package/src/session/compaction/branch-summarization.ts +3 -3
  150. package/src/session/compaction/compaction.ts +5 -6
  151. package/src/session/compaction/utils.ts +3 -3
  152. package/src/session/history-storage.ts +62 -19
  153. package/src/session/messages.ts +3 -3
  154. package/src/session/session-dump-format.ts +203 -0
  155. package/src/session/session-manager.ts +15 -5
  156. package/src/session/session-storage.ts +4 -2
  157. package/src/session/streaming-output.ts +1 -1
  158. package/src/session/tool-choice-queue.ts +213 -0
  159. package/src/slash-commands/builtin-registry.ts +56 -8
  160. package/src/ssh/connection-manager.ts +2 -2
  161. package/src/ssh/sshfs-mount.ts +5 -5
  162. package/src/stt/downloader.ts +4 -4
  163. package/src/stt/recorder.ts +4 -4
  164. package/src/stt/transcriber.ts +2 -2
  165. package/src/system-prompt.ts +25 -13
  166. package/src/task/agents.ts +5 -6
  167. package/src/task/commands.ts +2 -5
  168. package/src/task/executor.ts +32 -4
  169. package/src/task/index.ts +91 -82
  170. package/src/task/template.ts +2 -2
  171. package/src/task/types.ts +25 -0
  172. package/src/task/worktree.ts +131 -149
  173. package/src/tools/ask.ts +2 -3
  174. package/src/tools/ast-edit.ts +7 -7
  175. package/src/tools/ast-grep.ts +7 -7
  176. package/src/tools/auto-generated-guard.ts +36 -41
  177. package/src/tools/await-tool.ts +2 -2
  178. package/src/tools/bash.ts +5 -23
  179. package/src/tools/browser.ts +4 -5
  180. package/src/tools/calculator.ts +2 -3
  181. package/src/tools/cancel-job.ts +2 -2
  182. package/src/tools/checkpoint.ts +3 -3
  183. package/src/tools/debug.ts +1007 -0
  184. package/src/tools/exit-plan-mode.ts +3 -3
  185. package/src/tools/fetch.ts +67 -3
  186. package/src/tools/find.ts +4 -5
  187. package/src/tools/fs-cache-invalidation.ts +5 -0
  188. package/src/tools/gemini-image.ts +13 -5
  189. package/src/tools/gh.ts +130 -308
  190. package/src/tools/grep.ts +57 -9
  191. package/src/tools/index.ts +44 -22
  192. package/src/tools/inspect-image.ts +4 -4
  193. package/src/tools/output-meta.ts +1 -1
  194. package/src/tools/python.ts +19 -6
  195. package/src/tools/read.ts +211 -146
  196. package/src/tools/render-mermaid.ts +2 -3
  197. package/src/tools/render-utils.ts +20 -6
  198. package/src/tools/renderers.ts +3 -1
  199. package/src/tools/report-tool-issue.ts +80 -0
  200. package/src/tools/resolve.ts +70 -39
  201. package/src/tools/search-tool-bm25.ts +2 -2
  202. package/src/tools/ssh.ts +2 -2
  203. package/src/tools/todo-write.ts +2 -2
  204. package/src/tools/tool-timeouts.ts +1 -0
  205. package/src/tools/write.ts +5 -6
  206. package/src/tui/tree-list.ts +3 -1
  207. package/src/utils/clipboard.ts +80 -0
  208. package/src/utils/commit-message-generator.ts +2 -3
  209. package/src/utils/edit-mode.ts +49 -0
  210. package/src/utils/external-editor.ts +11 -5
  211. package/src/utils/file-display-mode.ts +6 -5
  212. package/src/utils/file-mentions.ts +8 -7
  213. package/src/utils/git.ts +1400 -0
  214. package/src/utils/image-loading.ts +98 -0
  215. package/src/utils/title-generator.ts +2 -3
  216. package/src/utils/tools-manager.ts +6 -6
  217. package/src/web/scrapers/choosealicense.ts +1 -1
  218. package/src/web/search/index.ts +3 -3
  219. package/src/web/search/render.ts +6 -4
  220. package/src/autoresearch/command-initialize.md +0 -34
  221. package/src/commit/git/errors.ts +0 -9
  222. package/src/commit/git/index.ts +0 -210
  223. package/src/commit/git/operations.ts +0 -54
  224. package/src/patch/diff.ts +0 -433
  225. package/src/patch/index.ts +0 -888
  226. package/src/patch/parser.ts +0 -532
  227. package/src/patch/types.ts +0 -292
  228. package/src/prompts/agents/oracle.md +0 -77
  229. package/src/tools/gh-cli.ts +0 -125
  230. package/src/tools/pending-action.ts +0 -49
  231. package/src/utils/child-process.ts +0 -88
  232. package/src/utils/frontmatter.ts +0 -117
  233. package/src/utils/image-input.ts +0 -274
  234. package/src/utils/mime.ts +0 -53
  235. package/src/utils/prompt-format.ts +0 -170
package/src/lsp/index.ts CHANGED
@@ -1,9 +1,8 @@
1
1
  import * as fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
4
- import { logger, once, untilAborted } from "@oh-my-pi/pi-utils";
4
+ import { logger, once, prompt, untilAborted } from "@oh-my-pi/pi-utils";
5
5
  import type { BunFile } from "bun";
6
- import { renderPromptTemplate } from "../config/prompt-templates";
7
6
  import { type Theme, theme } from "../modes/theme/theme";
8
7
  import lspDescription from "../prompts/tools/lsp.md" with { type: "text" };
9
8
  import type { ToolSession } from "../tools";
@@ -40,6 +39,7 @@ import {
40
39
  type LspParams,
41
40
  type LspToolDetails,
42
41
  lspSchema,
42
+ type PublishedDiagnostics,
43
43
  type ServerConfig,
44
44
  type SymbolInformation,
45
45
  type TextEdit,
@@ -71,14 +71,16 @@ import {
71
71
  export type { LspServerStatus } from "./client";
72
72
  export type { LspToolDetails } from "./types";
73
73
 
74
+ export interface LspStartupServerInfo {
75
+ name: string;
76
+ status: "connecting" | "ready" | "error";
77
+ fileTypes: string[];
78
+ error?: string;
79
+ }
80
+
74
81
  /** Result from warming up LSP servers */
75
82
  export interface LspWarmupResult {
76
- servers: Array<{
77
- name: string;
78
- status: "ready" | "error";
79
- fileTypes: string[];
80
- error?: string;
81
- }>;
83
+ servers: Array<LspStartupServerInfo & { status: "ready" | "error" }>;
82
84
  }
83
85
 
84
86
  /** Options for warming up LSP servers */
@@ -87,6 +89,15 @@ export interface LspWarmupOptions {
87
89
  onConnecting?: (serverNames: string[]) => void;
88
90
  }
89
91
 
92
+ export function discoverStartupLspServers(cwd: string): LspStartupServerInfo[] {
93
+ const config = loadConfig(cwd);
94
+ return getLspServers(config).map(([name, serverConfig]) => ({
95
+ name,
96
+ status: "connecting",
97
+ fileTypes: serverConfig.fileTypes,
98
+ }));
99
+ }
100
+
90
101
  /**
91
102
  * Warm up LSP servers for a directory by connecting to all detected servers.
92
103
  * This should be called at startup to avoid cold-start delays.
@@ -312,22 +323,59 @@ async function reloadServer(client: LspClient, serverName: string, signal?: Abor
312
323
  return output;
313
324
  }
314
325
 
326
+ interface WaitForDiagnosticsOptions {
327
+ timeoutMs?: number;
328
+ signal?: AbortSignal;
329
+ minVersion?: number;
330
+ expectedDocumentVersion?: number;
331
+ allowUnversioned?: boolean;
332
+ }
333
+
334
+ function getAcceptedDiagnostics(
335
+ publishedDiagnostics: PublishedDiagnostics | undefined,
336
+ expectedDocumentVersion?: number,
337
+ allowUnversioned = true,
338
+ ): Diagnostic[] | undefined {
339
+ if (!publishedDiagnostics) {
340
+ return undefined;
341
+ }
342
+ if (expectedDocumentVersion === undefined) {
343
+ return publishedDiagnostics.diagnostics;
344
+ }
345
+ if (publishedDiagnostics.version === expectedDocumentVersion) {
346
+ return publishedDiagnostics.diagnostics;
347
+ }
348
+ if (allowUnversioned && publishedDiagnostics.version == null) {
349
+ return publishedDiagnostics.diagnostics;
350
+ }
351
+ return undefined;
352
+ }
353
+
315
354
  async function waitForDiagnostics(
316
355
  client: LspClient,
317
356
  uri: string,
318
- timeoutMs = 3000,
319
- signal?: AbortSignal,
320
- minVersion?: number,
357
+ options: WaitForDiagnosticsOptions = {},
321
358
  ): Promise<Diagnostic[]> {
359
+ const { timeoutMs = 3000, signal, minVersion, expectedDocumentVersion, allowUnversioned = true } = options;
322
360
  const start = Date.now();
323
361
  while (Date.now() - start < timeoutMs) {
324
362
  throwIfAborted(signal);
325
- const diagnostics = client.diagnostics.get(uri);
326
363
  const versionOk = minVersion === undefined || client.diagnosticsVersion > minVersion;
327
- if (diagnostics !== undefined && versionOk) return diagnostics;
364
+ const diagnostics = getAcceptedDiagnostics(
365
+ client.diagnostics.get(uri),
366
+ expectedDocumentVersion,
367
+ allowUnversioned,
368
+ );
369
+ if (diagnostics !== undefined && versionOk) {
370
+ return diagnostics;
371
+ }
328
372
  await Bun.sleep(100);
329
373
  }
330
- return client.diagnostics.get(uri) ?? [];
374
+ const versionOk = minVersion === undefined || client.diagnosticsVersion > minVersion;
375
+ if (!versionOk) {
376
+ return [];
377
+ }
378
+ return getAcceptedDiagnostics(client.diagnostics.get(uri), expectedDocumentVersion, allowUnversioned) ?? [];
331
379
  }
332
380
 
333
381
  /** Project type detection result */
@@ -426,8 +474,14 @@ export interface FileDiagnosticsResult {
426
474
  formatter?: FileFormatResult;
427
475
  }
428
476
 
429
- /** Captured diagnostic versions per server (before sync) */
430
- type DiagnosticVersions = Map<string, number>;
477
+ type ServerVersionMap = Map<string, number>;
478
+
479
+ interface GetDiagnosticsForFileOptions {
480
+ signal?: AbortSignal;
481
+ minVersions?: ServerVersionMap;
482
+ expectedDocumentVersions?: ServerVersionMap;
483
+ allowUnversionedLspDiagnostics?: boolean;
484
+ }
431
485
 
432
486
  /**
433
487
  * Capture current diagnostic versions for all LSP servers.
@@ -436,7 +490,7 @@ type DiagnosticVersions = Map<string, number>;
436
490
  async function captureDiagnosticVersions(
437
491
  cwd: string,
438
492
  servers: Array<[string, ServerConfig]>,
439
- ): Promise<DiagnosticVersions> {
493
+ ): Promise<ServerVersionMap> {
440
494
  const versions = new Map<string, number>();
441
495
  await Promise.allSettled(
442
496
  servers.map(async ([serverName, serverConfig]) => {
@@ -448,6 +502,25 @@ async function captureDiagnosticVersions(
448
502
  return versions;
449
503
  }
450
504
 
505
+ async function captureOpenFileVersions(
506
+ absolutePath: string,
507
+ cwd: string,
508
+ servers: Array<[string, ServerConfig]>,
509
+ ): Promise<ServerVersionMap> {
510
+ const uri = fileToUri(absolutePath);
511
+ const versions = new Map<string, number>();
512
+ await Promise.allSettled(
513
+ servers.map(async ([serverName, serverConfig]) => {
514
+ const client = await getOrCreateClient(serverConfig, cwd);
515
+ const version = client.openFiles.get(uri)?.version;
516
+ if (version !== undefined) {
517
+ versions.set(serverName, version);
518
+ }
519
+ }),
520
+ );
521
+ return versions;
522
+ }
523
+
451
524
  /**
452
525
  * Get diagnostics for a file using LSP or custom linter client.
453
526
  *
@@ -461,9 +534,9 @@ async function getDiagnosticsForFile(
461
534
  absolutePath: string,
462
535
  cwd: string,
463
536
  servers: Array<[string, ServerConfig]>,
464
- signal?: AbortSignal,
465
- minVersions?: DiagnosticVersions,
537
+ options: GetDiagnosticsForFileOptions = {},
466
538
  ): Promise<FileDiagnosticsResult | undefined> {
539
+ const { signal, minVersions, expectedDocumentVersions, allowUnversionedLspDiagnostics = true } = options;
467
540
  if (servers.length === 0) {
468
541
  return undefined;
469
542
  }
@@ -489,7 +562,14 @@ async function getDiagnosticsForFile(
489
562
  throwIfAborted(signal);
490
563
  // Content already synced + didSave sent, wait for fresh diagnostics
491
564
  const minVersion = minVersions?.get(serverName);
492
- const diagnostics = await waitForDiagnostics(client, uri, 3000, signal, minVersion);
565
+ const expectedDocumentVersion = expectedDocumentVersions?.get(serverName);
566
+ const diagnostics = await waitForDiagnostics(client, uri, {
567
+ timeoutMs: 3000,
568
+ signal,
569
+ minVersion,
570
+ expectedDocumentVersion,
571
+ allowUnversioned: allowUnversionedLspDiagnostics,
572
+ });
493
573
  return { serverName, diagnostics };
494
574
  }),
495
575
  );
@@ -622,8 +702,25 @@ export interface WritethroughOptions {
622
702
  enableFormat?: boolean;
623
703
  /** Whether to get LSP diagnostics after writing */
624
704
  enableDiagnostics?: boolean;
705
+ /** Called when diagnostics arrive after the main timeout. */
706
+ onDeferredDiagnostics?: (diagnostics: FileDiagnosticsResult) => void;
707
+ /** Signal to cancel a pending deferred diagnostics fetch. */
708
+ deferredSignal?: AbortSignal;
625
709
  }
626
710
 
711
+ /** Internal resolved form of {@link WritethroughOptions} that the writethrough machinery operates on. */
712
+ type ResolvedWritethroughOptions = {
713
+ enableFormat: boolean;
714
+ enableDiagnostics: boolean;
715
+ };
716
+
717
+ /** Per-file deferred LSP diagnostics wiring for {@link WritethroughCallback}. */
718
+ export type WritethroughDeferredHandle = {
719
+ onDeferredDiagnostics: (diagnostics: FileDiagnosticsResult) => void;
720
+ signal: AbortSignal;
721
+ finalize: (diagnostics: FileDiagnosticsResult | undefined) => void;
722
+ };
723
+
627
724
  /** Callback type for the LSP writethrough */
628
725
  export type WritethroughCallback = (
629
726
  dst: string,
@@ -631,6 +728,7 @@ export type WritethroughCallback = (
631
728
  signal?: AbortSignal,
632
729
  file?: BunFile,
633
730
  batch?: LspWritethroughBatchRequest,
731
+ getDeferred?: (dst: string) => WritethroughDeferredHandle | undefined,
634
732
  ) => Promise<FileDiagnosticsResult | undefined>;
635
733
 
636
734
  /** No-op writethrough callback */
@@ -639,6 +737,8 @@ export async function writethroughNoop(
639
737
  content: string,
640
738
  _signal?: AbortSignal,
641
739
  file?: BunFile,
740
+ _batch?: LspWritethroughBatchRequest,
741
+ _getDeferred?: (dst: string) => WritethroughDeferredHandle | undefined,
642
742
  ): Promise<FileDiagnosticsResult | undefined> {
643
743
  if (file) {
644
744
  await file.write(content);
@@ -661,12 +761,12 @@ interface LspWritethroughBatchRequest {
661
761
 
662
762
  interface LspWritethroughBatchState {
663
763
  entries: Map<string, PendingWritethrough>;
664
- options: Required<WritethroughOptions>;
764
+ options: ResolvedWritethroughOptions;
665
765
  }
666
766
 
667
767
  const writethroughBatches = new Map<string, LspWritethroughBatchState>();
668
768
 
669
- function getOrCreateWritethroughBatch(id: string, options: Required<WritethroughOptions>): LspWritethroughBatchState {
769
+ function getOrCreateWritethroughBatch(id: string, options: ResolvedWritethroughOptions): LspWritethroughBatchState {
670
770
  const existing = writethroughBatches.get(id);
671
771
  if (existing) {
672
772
  existing.options.enableFormat ||= options.enableFormat;
@@ -717,7 +817,7 @@ function summarizeDiagnosticMessages(messages: string[]): { summary: string; err
717
817
 
718
818
  function mergeDiagnostics(
719
819
  results: Array<FileDiagnosticsResult | undefined>,
720
- options: Required<WritethroughOptions>,
820
+ options: ResolvedWritethroughOptions,
721
821
  ): FileDiagnosticsResult | undefined {
722
822
  const messages: string[] = [];
723
823
  const servers = new Set<string>();
@@ -771,13 +871,41 @@ function mergeDiagnostics(
771
871
  };
772
872
  }
773
873
 
874
+ async function scheduleDeferredDiagnosticsFetch(args: {
875
+ dst: string;
876
+ cwd: string;
877
+ servers: Array<[string, ServerConfig]>;
878
+ minVersions: ServerVersionMap | undefined;
879
+ expectedDocumentVersions: ServerVersionMap | undefined;
880
+ signal: AbortSignal;
881
+ callback: (diagnostics: FileDiagnosticsResult) => void;
882
+ }): Promise<void> {
883
+ try {
884
+ const deferredTimeout = AbortSignal.timeout(25_000);
885
+ const combined = AbortSignal.any([args.signal, deferredTimeout]);
886
+ const diagnostics = await getDiagnosticsForFile(args.dst, args.cwd, args.servers, {
887
+ signal: combined,
888
+ minVersions: args.minVersions,
889
+ expectedDocumentVersions: args.expectedDocumentVersions,
890
+ });
891
+ if (args.signal.aborted || diagnostics === undefined) return;
892
+ args.callback(diagnostics);
893
+ } catch {
894
+ // Cancelled or LSP gave up; silently discard.
895
+ }
896
+ }
897
+
774
898
  async function runLspWritethrough(
775
899
  dst: string,
776
900
  content: string,
777
901
  cwd: string,
778
- options: Required<WritethroughOptions>,
902
+ options: ResolvedWritethroughOptions,
779
903
  signal?: AbortSignal,
780
904
  file?: BunFile,
905
+ deferred?: {
906
+ onDeferredDiagnostics: (diagnostics: FileDiagnosticsResult) => void;
907
+ signal: AbortSignal;
908
+ },
781
909
  ): Promise<FileDiagnosticsResult | undefined> {
782
910
  const { enableFormat, enableDiagnostics } = options;
783
911
  const config = getConfig(cwd);
@@ -794,12 +922,13 @@ async function runLspWritethrough(
794
922
 
795
923
  // Capture diagnostic versions BEFORE syncing to detect stale diagnostics
796
924
  const minVersions = enableDiagnostics ? await captureDiagnosticVersions(cwd, servers) : undefined;
925
+ let expectedDocumentVersions: ServerVersionMap | undefined;
797
926
 
798
927
  let formatter: FileFormatResult | undefined;
799
928
  let diagnostics: FileDiagnosticsResult | undefined;
800
929
  let timedOut = false;
801
930
  try {
802
- const timeoutSignal = AbortSignal.timeout(10_000);
931
+ const timeoutSignal = AbortSignal.timeout(5_000);
803
932
  timeoutSignal.addEventListener(
804
933
  "abort",
805
934
  () => {
@@ -835,18 +964,39 @@ async function runLspWritethrough(
835
964
  await getWritePromise();
836
965
  }
837
966
 
967
+ if (enableDiagnostics) {
968
+ expectedDocumentVersions = await captureOpenFileVersions(dst, cwd, lspServers);
969
+ }
970
+
838
971
  // 5. Notify saved to LSP servers
839
972
  await notifyFileSaved(dst, cwd, lspServers, operationSignal);
840
973
 
841
974
  // 6. Get diagnostics from all servers (wait for fresh results)
842
975
  if (enableDiagnostics) {
843
- diagnostics = await getDiagnosticsForFile(dst, cwd, servers, operationSignal, minVersions);
976
+ diagnostics = await getDiagnosticsForFile(dst, cwd, servers, {
977
+ signal: operationSignal,
978
+ minVersions,
979
+ expectedDocumentVersions,
980
+ allowUnversionedLspDiagnostics: false,
981
+ });
844
982
  }
845
983
  });
846
984
  } catch {
847
985
  if (timedOut) {
848
986
  formatter = undefined;
849
987
  diagnostics = undefined;
988
+ // Schedule background diagnostic fetch if caller wants deferred results
989
+ if (deferred && !deferred.signal.aborted && enableDiagnostics) {
990
+ void scheduleDeferredDiagnosticsFetch({
991
+ dst,
992
+ cwd,
993
+ servers,
994
+ minVersions,
995
+ expectedDocumentVersions,
996
+ signal: deferred.signal,
997
+ callback: deferred.onDeferredDiagnostics,
998
+ });
999
+ }
850
1000
  }
851
1001
  await getWritePromise();
852
1002
  }
@@ -867,22 +1017,32 @@ async function runLspWritethrough(
867
1017
  async function flushWritethroughBatch(
868
1018
  batch: PendingWritethrough[],
869
1019
  cwd: string,
870
- options: Required<WritethroughOptions>,
1020
+ options: ResolvedWritethroughOptions,
871
1021
  signal?: AbortSignal,
1022
+ getDeferred?: (dst: string) => WritethroughDeferredHandle | undefined,
872
1023
  ): Promise<FileDiagnosticsResult | undefined> {
873
1024
  if (batch.length === 0) {
874
1025
  return undefined;
875
1026
  }
876
1027
  const results: Array<FileDiagnosticsResult | undefined> = [];
877
1028
  for (const entry of batch) {
878
- results.push(await runLspWritethrough(entry.dst, entry.content, cwd, options, signal, entry.file));
1029
+ const bundle = getDeferred?.(entry.dst);
1030
+ const deferredInner =
1031
+ bundle &&
1032
+ ({
1033
+ onDeferredDiagnostics: bundle.onDeferredDiagnostics,
1034
+ signal: bundle.signal,
1035
+ } as const);
1036
+ const diag = await runLspWritethrough(entry.dst, entry.content, cwd, options, signal, entry.file, deferredInner);
1037
+ bundle?.finalize(diag);
1038
+ results.push(diag);
879
1039
  }
880
1040
  return mergeDiagnostics(results, options);
881
1041
  }
882
1042
 
883
1043
  /** Create a writethrough callback for LSP aware write operations */
884
1044
  export function createLspWritethrough(cwd: string, options?: WritethroughOptions): WritethroughCallback {
885
- const resolvedOptions: Required<WritethroughOptions> = {
1045
+ const resolvedOptions: ResolvedWritethroughOptions = {
886
1046
  enableFormat: options?.enableFormat ?? false,
887
1047
  enableDiagnostics: options?.enableDiagnostics ?? false,
888
1048
  };
@@ -895,9 +1055,19 @@ export function createLspWritethrough(cwd: string, options?: WritethroughOptions
895
1055
  signal?: AbortSignal,
896
1056
  file?: BunFile,
897
1057
  batch?: LspWritethroughBatchRequest,
1058
+ getDeferred?: (dst: string) => WritethroughDeferredHandle | undefined,
898
1059
  ) => {
899
1060
  if (!batch) {
900
- return runLspWritethrough(dst, content, cwd, resolvedOptions, signal, file);
1061
+ const bundle = getDeferred?.(dst);
1062
+ const deferredInner =
1063
+ bundle &&
1064
+ ({
1065
+ onDeferredDiagnostics: bundle.onDeferredDiagnostics,
1066
+ signal: bundle.signal,
1067
+ } as const);
1068
+ const diagnostics = await runLspWritethrough(dst, content, cwd, resolvedOptions, signal, file, deferredInner);
1069
+ bundle?.finalize(diagnostics);
1070
+ return diagnostics;
901
1071
  }
902
1072
 
903
1073
  const state = getOrCreateWritethroughBatch(batch.id, resolvedOptions);
@@ -909,7 +1079,7 @@ export function createLspWritethrough(cwd: string, options?: WritethroughOptions
909
1079
  }
910
1080
 
911
1081
  writethroughBatches.delete(batch.id);
912
- return flushWritethroughBatch(Array.from(state.entries.values()), cwd, state.options, signal);
1082
+ return flushWritethroughBatch(Array.from(state.entries.values()), cwd, state.options, signal, getDeferred);
913
1083
  };
914
1084
  }
915
1085
 
@@ -928,7 +1098,7 @@ export class LspTool implements AgentTool<typeof lspSchema, LspToolDetails, Them
928
1098
  readonly inline = true;
929
1099
 
930
1100
  constructor(private readonly session: ToolSession) {
931
- this.description = renderPromptTemplate(lspDescription);
1101
+ this.description = prompt.render(lspDescription);
932
1102
  }
933
1103
 
934
1104
  static createIf(session: ToolSession): LspTool | null {
@@ -974,8 +1144,8 @@ export class LspTool implements AgentTool<typeof lspSchema, LspToolDetails, Them
974
1144
 
975
1145
  // Diagnostics can be batch or single-file - queries all applicable servers
976
1146
  if (action === "diagnostics") {
977
- if (!file) {
978
- // No file specified - run workspace diagnostics
1147
+ if (file === "*") {
1148
+ // `*` => run workspace diagnostics across all configured servers
979
1149
  const result = await runWorkspaceDiagnostics(this.session.cwd, signal);
980
1150
  return {
981
1151
  content: [
@@ -988,6 +1158,18 @@ export class LspTool implements AgentTool<typeof lspSchema, LspToolDetails, Them
988
1158
  };
989
1159
  }
990
1160
 
1161
+ if (!file) {
1162
+ return {
1163
+ content: [
1164
+ {
1165
+ type: "text",
1166
+ text: "Error: file parameter required. Use `*` for workspace-wide diagnostics or a path/glob for specific files.",
1167
+ },
1168
+ ],
1169
+ details: { action, success: false, request: params },
1170
+ };
1171
+ }
1172
+
991
1173
  let targets: string[];
992
1174
  let truncatedGlobTargets = false;
993
1175
  if (hasGlobPattern(file)) {
@@ -1044,13 +1226,13 @@ export class LspTool implements AgentTool<typeof lspSchema, LspToolDetails, Them
1044
1226
  const client = await getOrCreateClient(serverConfig, this.session.cwd);
1045
1227
  const minVersion = client.diagnosticsVersion;
1046
1228
  await refreshFile(client, resolved, signal);
1047
- const diagnostics = await waitForDiagnostics(
1048
- client,
1049
- uri,
1050
- diagnosticsWaitTimeoutMs,
1229
+ const expectedDocumentVersion = client.openFiles.get(uri)?.version;
1230
+ const diagnostics = await waitForDiagnostics(client, uri, {
1231
+ timeoutMs: diagnosticsWaitTimeoutMs,
1051
1232
  signal,
1052
1233
  minVersion,
1053
- );
1234
+ expectedDocumentVersion,
1235
+ });
1054
1236
  allDiagnostics.push(...diagnostics);
1055
1237
  } catch (err) {
1056
1238
  if (err instanceof ToolAbortError || signal?.aborted) {
@@ -1106,17 +1288,24 @@ export class LspTool implements AgentTool<typeof lspSchema, LspToolDetails, Them
1106
1288
  };
1107
1289
  }
1108
1290
 
1109
- const requiresFile = !file && action !== "symbols" && action !== "reload";
1291
+ // `*` means workspace scope for symbols/reload; other actions need a concrete file.
1292
+ const isWorkspace = file === "*";
1293
+ const requiresFile = !file && action !== "reload";
1110
1294
 
1111
1295
  if (requiresFile) {
1112
1296
  return {
1113
- content: [{ type: "text", text: "Error: file parameter required for this action" }],
1297
+ content: [
1298
+ {
1299
+ type: "text",
1300
+ text: "Error: file parameter required. Use `*` for workspace scope where supported.",
1301
+ },
1302
+ ],
1114
1303
  details: { action, success: false },
1115
1304
  };
1116
1305
  }
1117
1306
 
1118
- const resolvedFile = file ? resolveToCwd(file, this.session.cwd) : null;
1119
- if (action === "symbols" && !resolvedFile) {
1307
+ const resolvedFile = file && !isWorkspace ? resolveToCwd(file, this.session.cwd) : null;
1308
+ if (action === "symbols" && (isWorkspace || !resolvedFile)) {
1120
1309
  const normalizedQuery = query?.trim();
1121
1310
  if (!normalizedQuery) {
1122
1311
  return {
@@ -1188,7 +1377,7 @@ export class LspTool implements AgentTool<typeof lspSchema, LspToolDetails, Them
1188
1377
  };
1189
1378
  }
1190
1379
 
1191
- if (action === "reload" && !resolvedFile) {
1380
+ if (action === "reload" && (isWorkspace || !resolvedFile)) {
1192
1381
  const servers = getLspServers(config);
1193
1382
  if (servers.length === 0) {
1194
1383
  return {
@@ -1372,7 +1561,7 @@ export class LspTool implements AgentTool<typeof lspSchema, LspToolDetails, Them
1372
1561
  }
1373
1562
 
1374
1563
  case "code_actions": {
1375
- const diagnostics = client.diagnostics.get(uri) ?? [];
1564
+ const diagnostics = client.diagnostics.get(uri)?.diagnostics ?? [];
1376
1565
  const context: CodeActionContext = {
1377
1566
  diagnostics,
1378
1567
  only: !apply && query ? [query] : undefined,
package/src/lsp/lspmux.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as os from "node:os";
2
2
  import * as path from "node:path";
3
- import { $env, logger } from "@oh-my-pi/pi-utils";
3
+ import { $env, $which, logger } from "@oh-my-pi/pi-utils";
4
4
  import { TOML } from "bun";
5
5
 
6
6
  /**
@@ -141,7 +141,7 @@ export async function detectLspmux(): Promise<LspmuxState> {
141
141
  return cachedState;
142
142
  }
143
143
 
144
- const binaryPath = Bun.which("lspmux");
144
+ const binaryPath = $which("lspmux");
145
145
  if (!binaryPath) {
146
146
  cachedState = { available: false, running: false, binaryPath: null, config: null };
147
147
  cacheTimestamp = now;
@@ -0,0 +1,13 @@
1
+ import type { LspStartupServerInfo } from "./index";
2
+
3
+ export const LSP_STARTUP_EVENT_CHANNEL = "lsp:startup";
4
+
5
+ export type LspStartupEvent =
6
+ | {
7
+ type: "completed";
8
+ servers: Array<LspStartupServerInfo & { status: "ready" | "error" }>;
9
+ }
10
+ | {
11
+ type: "failed";
12
+ error: string;
13
+ };
package/src/lsp/types.ts CHANGED
@@ -93,6 +93,17 @@ export interface Diagnostic {
93
93
  data?: unknown;
94
94
  }
95
95
 
96
+ export interface PublishedDiagnostics {
97
+ diagnostics: Diagnostic[];
98
+ version: number | null;
99
+ }
100
+
101
+ export interface PublishDiagnosticsParams {
102
+ uri: string;
103
+ diagnostics: Diagnostic[];
104
+ version?: number | null;
105
+ }
106
+
96
107
  // =============================================================================
97
108
  // Text Edits
98
109
  // =============================================================================
@@ -392,7 +403,7 @@ export interface LspClient {
392
403
  config: ServerConfig;
393
404
  proc: ptree.ChildProcess<"pipe">;
394
405
  requestId: number;
395
- diagnostics: Map<string, Diagnostic[]>;
406
+ diagnostics: Map<string, PublishedDiagnostics>;
396
407
  diagnosticsVersion: number;
397
408
  openFiles: Map<string, OpenFile>;
398
409
  pendingRequests: Map<number, PendingRequest>;
package/src/lsp/utils.ts CHANGED
@@ -52,6 +52,7 @@ const LANGUAGE_MAP: Record<string, string> = {
52
52
  ".zsh": "shellscript",
53
53
  ".fish": "fish",
54
54
  ".pl": "perl",
55
+ ".pm": "perl",
55
56
  ".php": "php",
56
57
 
57
58
  // JVM languages
@@ -76,6 +77,7 @@ const LANGUAGE_MAP: Record<string, string> = {
76
77
  ".less": "less",
77
78
  ".vue": "vue",
78
79
  ".svelte": "svelte",
80
+ ".astro": "astro",
79
81
 
80
82
  // Data formats
81
83
  ".json": "json",
@@ -129,6 +131,8 @@ const LANGUAGE_MAP: Record<string, string> = {
129
131
  ".psm1": "powershell",
130
132
  ".bat": "bat",
131
133
  ".cmd": "bat",
134
+ ".tla": "tlaplus",
135
+ ".tlaplus": "tlaplus",
132
136
  };
133
137
 
134
138
  /**
@@ -140,12 +144,15 @@ export function detectLanguageId(filePath: string): string {
140
144
  const basename = path.basename(filePath).toLowerCase();
141
145
 
142
146
  // Handle special filenames
143
- if (basename === "dockerfile" || basename.startsWith("dockerfile.")) {
147
+ if (basename === "dockerfile" || basename.startsWith("dockerfile.") || basename === "containerfile") {
144
148
  return "dockerfile";
145
149
  }
146
150
  if (basename === "makefile" || basename === "gnumakefile") {
147
151
  return "makefile";
148
152
  }
153
+ if (basename === "justfile") {
154
+ return "just";
155
+ }
149
156
  if (basename === "cmakelists.txt" || ext === ".cmake") {
150
157
  return "cmake";
151
158
  }