oh-my-opencode 5.0.0-beta.25 → 5.0.0-beta.28

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 (193) hide show
  1. package/dist/cli/config-manager/parse-opencode-config-file.d.ts +2 -1
  2. package/dist/cli/doctor/checks/system-plugin.d.ts +4 -3
  3. package/dist/cli/index.js +309 -182
  4. package/dist/cli-node/index.js +332 -182
  5. package/dist/features/team-mode/tools/messaging-fallback-wake.d.ts +15 -0
  6. package/dist/hooks/todo-continuation-enforcer/types.d.ts +1 -0
  7. package/dist/index.js +1958 -1668
  8. package/dist/shared/index.d.ts +1 -0
  9. package/dist/shared/legacy-plugin-warning.d.ts +2 -1
  10. package/dist/shared/plugin-entry-migrator.d.ts +4 -3
  11. package/dist/shared/plugin-entry-shape.d.ts +6 -0
  12. package/dist/skills/data-scientist/SKILL.md +99 -239
  13. package/dist/skills/data-scientist/references/execution-surfaces.md +91 -0
  14. package/dist/skills/data-scientist/references/placement.md +74 -0
  15. package/dist/skills/data-scientist/references/polars-lane.md +95 -0
  16. package/dist/skills/data-scientist/references/uv-setup.md +1 -1
  17. package/dist/skills/data-scientist/references/visualization.md +64 -0
  18. package/dist/skills/data-scientist/scripts/ensure-js-deps.sh +28 -0
  19. package/dist/skills/data-scientist/scripts/ensure-py-deps.sh +37 -0
  20. package/dist/skills/debugging/SKILL.md +2 -0
  21. package/dist/skills/debugging/references/methodology/00-setup.md +12 -0
  22. package/dist/skills/debugging/references/methodology/02-investigate.md +21 -0
  23. package/dist/skills/debugging/references/runtimes/go.md +15 -1
  24. package/dist/skills/debugging/references/runtimes/native-binary.md +38 -0
  25. package/dist/skills/debugging/references/runtimes/node.md +25 -0
  26. package/dist/skills/debugging/references/runtimes/python.md +13 -0
  27. package/dist/skills/debugging/references/runtimes/rust.md +41 -0
  28. package/dist/skills/debugging/references/scripts/dap.mjs +267 -0
  29. package/dist/skills/debugging/references/scripts/dap.test.ts +86 -0
  30. package/dist/skills/debugging/references/scripts/fixture-adapter.mjs +59 -0
  31. package/dist/skills/debugging/references/tools/dap.md +103 -0
  32. package/dist/skills/debugging/references/tools/frida.md +193 -0
  33. package/dist/skills/ulw-research/SKILL.md +3 -1
  34. package/dist/tools/delegate-task/types.d.ts +3 -0
  35. package/dist/tui.d.ts +12 -0
  36. package/dist/tui.js +106 -15
  37. package/package.json +18 -15
  38. package/packages/lsp-core/src/index.ts +1 -0
  39. package/packages/lsp-core/src/lsp/client-wrapper.ts +25 -5
  40. package/packages/lsp-core/src/lsp/client.ts +26 -0
  41. package/packages/lsp-core/src/lsp/connection.ts +16 -0
  42. package/packages/lsp-core/src/lsp/constants.ts +1 -0
  43. package/packages/lsp-core/src/lsp/format-document.test.ts +143 -0
  44. package/packages/lsp-core/src/lsp/format-document.ts +90 -0
  45. package/packages/lsp-core/src/lsp/manager-max-clients.test.ts +151 -0
  46. package/packages/lsp-core/src/lsp/manager.ts +34 -1
  47. package/packages/lsp-core/src/lsp/server-definitions.ts +29 -0
  48. package/packages/lsp-core/src/lsp/server-installation.test.ts +376 -0
  49. package/packages/lsp-core/src/lsp/server-installation.ts +200 -27
  50. package/packages/lsp-core/src/lsp/server-resolution-local-binary.test.ts +170 -0
  51. package/packages/lsp-core/src/lsp/server-resolution.ts +18 -5
  52. package/packages/lsp-core/src/lsp/transport.ts +9 -0
  53. package/packages/lsp-core/src/lsp/types.ts +8 -0
  54. package/packages/lsp-core/src/request-context.test.ts +29 -0
  55. package/packages/lsp-core/src/request-context.ts +1 -1
  56. package/packages/lsp-core/src/tool-surface.test.ts +13 -1
  57. package/packages/lsp-core/src/tools/definitions.ts +14 -0
  58. package/packages/lsp-core/src/tools/format.test.ts +56 -0
  59. package/packages/lsp-core/src/tools/format.ts +56 -0
  60. package/packages/lsp-core/src/tools/index.ts +1 -0
  61. package/packages/lsp-core/src/tools/types.ts +10 -0
  62. package/packages/lsp-daemon/dist/cli.js +433 -145
  63. package/packages/lsp-daemon/dist/client.d.ts +10 -0
  64. package/packages/lsp-daemon/dist/client.js +402 -108
  65. package/packages/lsp-daemon/dist/daemon-client.d.ts +1 -0
  66. package/packages/lsp-daemon/dist/daemon-client.js +3 -0
  67. package/packages/lsp-daemon/dist/index.js +396 -108
  68. package/packages/lsp-tools-mcp/dist/cli.js +385 -100
  69. package/packages/lsp-tools-mcp/dist/lsp/manager.js +53 -0
  70. package/packages/lsp-tools-mcp/dist/mcp.js +385 -100
  71. package/packages/lsp-tools-mcp/dist/request-context.js +1 -1
  72. package/packages/lsp-tools-mcp/dist/tools.js +386 -100
  73. package/packages/omo-codex/plugin/.codex-plugin/plugin.json +1 -1
  74. package/packages/omo-codex/plugin/components/bootstrap/dist/cli.js +2 -2
  75. package/packages/omo-codex/plugin/components/bootstrap/hooks/hooks.json +1 -1
  76. package/packages/omo-codex/plugin/components/bootstrap/package.json +1 -1
  77. package/packages/omo-codex/plugin/components/codegraph/dist/cli.js +69 -35
  78. package/packages/omo-codex/plugin/components/codegraph/dist/serve.js +21 -2
  79. package/packages/omo-codex/plugin/components/codegraph/package.json +1 -1
  80. package/packages/omo-codex/plugin/components/comment-checker/hooks/hooks.json +1 -1
  81. package/packages/omo-codex/plugin/components/comment-checker/package.json +1 -1
  82. package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +2 -2
  83. package/packages/omo-codex/plugin/components/git-bash/package.json +1 -1
  84. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/hooks/hooks.json +1 -1
  85. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/package.json +1 -1
  86. package/packages/omo-codex/plugin/components/lsp/dist/.omo-runtime-manifest.json +3 -3
  87. package/packages/omo-codex/plugin/components/lsp/dist/cli.js +404 -121
  88. package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +2 -2
  89. package/packages/omo-codex/plugin/components/lsp/package.json +1 -1
  90. package/packages/omo-codex/plugin/components/rules/dist/cli.js +3 -3
  91. package/packages/omo-codex/plugin/components/rules/hooks/hooks.json +4 -4
  92. package/packages/omo-codex/plugin/components/rules/package.json +1 -1
  93. package/packages/omo-codex/plugin/components/rules/src/post-compact-budget.ts +3 -3
  94. package/packages/omo-codex/plugin/components/rules/test/post-compact-budget.test.ts +4 -4
  95. package/packages/omo-codex/plugin/components/teammode/hooks/hooks.json +1 -1
  96. package/packages/omo-codex/plugin/components/teammode/package.json +1 -1
  97. package/packages/omo-codex/plugin/components/telemetry/dist/cli.js +30 -15
  98. package/packages/omo-codex/plugin/components/telemetry/dist/posthog.js +30 -15
  99. package/packages/omo-codex/plugin/components/telemetry/hooks/hooks.json +1 -1
  100. package/packages/omo-codex/plugin/components/telemetry/package.json +1 -1
  101. package/packages/omo-codex/plugin/components/ultrawork/hooks/hooks.json +1 -1
  102. package/packages/omo-codex/plugin/components/ultrawork/package.json +1 -1
  103. package/packages/omo-codex/plugin/components/ulw-execute-continuation/hooks/hooks.json +2 -2
  104. package/packages/omo-codex/plugin/components/ulw-execute-continuation/package.json +1 -1
  105. package/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json +4 -4
  106. package/packages/omo-codex/plugin/components/ulw-loop/package.json +1 -1
  107. package/packages/omo-codex/plugin/hooks/post-compact-resetting-git-bash-mcp-reminder.json +1 -1
  108. package/packages/omo-codex/plugin/hooks/post-compact-resetting-lsp-diagnostics-cache.json +1 -1
  109. package/packages/omo-codex/plugin/hooks/post-compact-resetting-project-rule-cache.json +1 -1
  110. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-codegraph-init-guidance.json +1 -1
  111. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-comments.json +1 -1
  112. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-lsp-diagnostics.json +1 -1
  113. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-thread-title-hygiene.json +1 -1
  114. package/packages/omo-codex/plugin/hooks/post-tool-use-matching-project-rules.json +1 -1
  115. package/packages/omo-codex/plugin/hooks/pre-tool-use-enforcing-unlimited-goal-budget.json +1 -1
  116. package/packages/omo-codex/plugin/hooks/pre-tool-use-guarding-ulw-loop-spawns.json +1 -1
  117. package/packages/omo-codex/plugin/hooks/pre-tool-use-recommending-git-bash-mcp.json +1 -1
  118. package/packages/omo-codex/plugin/hooks/session-start-checking-auto-update.json +1 -1
  119. package/packages/omo-codex/plugin/hooks/session-start-checking-bootstrap-provisioning.json +1 -1
  120. package/packages/omo-codex/plugin/hooks/session-start-checking-codegraph-bootstrap.json +1 -1
  121. package/packages/omo-codex/plugin/hooks/session-start-loading-project-rules.json +1 -1
  122. package/packages/omo-codex/plugin/hooks/session-start-recording-session-telemetry.json +1 -1
  123. package/packages/omo-codex/plugin/hooks/stop-checking-ulw-execute-continuation.json +1 -1
  124. package/packages/omo-codex/plugin/hooks/stop-checking-ulw-loop-resume.json +1 -1
  125. package/packages/omo-codex/plugin/hooks/subagent-stop-checking-ulw-execute-continuation.json +1 -1
  126. package/packages/omo-codex/plugin/hooks/subagent-stop-verifying-lazycodex-executor-evidence.json +1 -1
  127. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ultrawork-trigger.json +1 -1
  128. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ulw-loop-steering.json +1 -1
  129. package/packages/omo-codex/plugin/hooks/user-prompt-submit-loading-project-rules.json +1 -1
  130. package/packages/omo-codex/plugin/model-catalog.json +3 -3
  131. package/packages/omo-codex/plugin/package-lock.json +13 -13
  132. package/packages/omo-codex/plugin/package.json +1 -1
  133. package/packages/omo-codex/plugin/scripts/migrate-codex-config/catalog.mjs +3 -3
  134. package/packages/omo-codex/plugin/skills/data-scientist/SKILL.md +99 -239
  135. package/packages/omo-codex/plugin/skills/data-scientist/references/execution-surfaces.md +91 -0
  136. package/packages/omo-codex/plugin/skills/data-scientist/references/placement.md +74 -0
  137. package/packages/omo-codex/plugin/skills/data-scientist/references/polars-lane.md +95 -0
  138. package/packages/omo-codex/plugin/skills/data-scientist/references/uv-setup.md +1 -1
  139. package/packages/omo-codex/plugin/skills/data-scientist/references/visualization.md +64 -0
  140. package/packages/omo-codex/plugin/skills/data-scientist/scripts/ensure-js-deps.sh +28 -0
  141. package/packages/omo-codex/plugin/skills/data-scientist/scripts/ensure-py-deps.sh +37 -0
  142. package/packages/omo-codex/plugin/skills/debugging/SKILL.md +2 -0
  143. package/packages/omo-codex/plugin/skills/debugging/references/methodology/00-setup.md +12 -0
  144. package/packages/omo-codex/plugin/skills/debugging/references/methodology/02-investigate.md +21 -0
  145. package/packages/omo-codex/plugin/skills/debugging/references/runtimes/go.md +15 -1
  146. package/packages/omo-codex/plugin/skills/debugging/references/runtimes/native-binary.md +38 -0
  147. package/packages/omo-codex/plugin/skills/debugging/references/runtimes/node.md +25 -0
  148. package/packages/omo-codex/plugin/skills/debugging/references/runtimes/python.md +13 -0
  149. package/packages/omo-codex/plugin/skills/debugging/references/runtimes/rust.md +41 -0
  150. package/packages/omo-codex/plugin/skills/debugging/references/scripts/dap.mjs +267 -0
  151. package/packages/omo-codex/plugin/skills/debugging/references/scripts/fixture-adapter.mjs +59 -0
  152. package/packages/omo-codex/plugin/skills/debugging/references/tools/dap.md +103 -0
  153. package/packages/omo-codex/plugin/skills/debugging/references/tools/frida.md +193 -0
  154. package/packages/omo-codex/plugin/skills/ulw-research/SKILL.md +3 -1
  155. package/packages/omo-codex/plugin/test/aggregate-model-catalog.test.mjs +1 -1
  156. package/packages/omo-codex/plugin/test/auto-update.test.mjs +2 -2
  157. package/packages/omo-codex/plugin/test/migrate-codex-config.test.mjs +8 -8
  158. package/packages/omo-codex/scripts/install-config-reasoning.test.mjs +2 -2
  159. package/packages/omo-codex/scripts/install-dist/install-local.mjs +38 -23
  160. package/packages/shared-skills/skills/data-scientist/SKILL.md +99 -239
  161. package/packages/shared-skills/skills/data-scientist/references/execution-surfaces.md +91 -0
  162. package/packages/shared-skills/skills/data-scientist/references/placement.md +74 -0
  163. package/packages/shared-skills/skills/data-scientist/references/polars-lane.md +95 -0
  164. package/packages/shared-skills/skills/data-scientist/references/uv-setup.md +1 -1
  165. package/packages/shared-skills/skills/data-scientist/references/visualization.md +64 -0
  166. package/packages/shared-skills/skills/data-scientist/scripts/ensure-js-deps.sh +28 -0
  167. package/packages/shared-skills/skills/data-scientist/scripts/ensure-py-deps.sh +37 -0
  168. package/packages/shared-skills/skills/debugging/SKILL.md +2 -0
  169. package/packages/shared-skills/skills/debugging/references/methodology/00-setup.md +12 -0
  170. package/packages/shared-skills/skills/debugging/references/methodology/02-investigate.md +21 -0
  171. package/packages/shared-skills/skills/debugging/references/runtimes/go.md +15 -1
  172. package/packages/shared-skills/skills/debugging/references/runtimes/native-binary.md +38 -0
  173. package/packages/shared-skills/skills/debugging/references/runtimes/node.md +25 -0
  174. package/packages/shared-skills/skills/debugging/references/runtimes/python.md +13 -0
  175. package/packages/shared-skills/skills/debugging/references/runtimes/rust.md +41 -0
  176. package/packages/shared-skills/skills/debugging/references/scripts/dap.mjs +267 -0
  177. package/packages/shared-skills/skills/debugging/references/scripts/dap.test.ts +86 -0
  178. package/packages/shared-skills/skills/debugging/references/scripts/fixture-adapter.mjs +59 -0
  179. package/packages/shared-skills/skills/debugging/references/tools/dap.md +103 -0
  180. package/packages/shared-skills/skills/debugging/references/tools/frida.md +193 -0
  181. package/packages/shared-skills/skills/ulw-research/SKILL.md +3 -1
  182. package/dist/skills/data-scientist/references/common-scenarios.md +0 -176
  183. package/dist/skills/data-scientist/references/execution-templates.md +0 -197
  184. package/dist/skills/data-scientist/references/integration-patterns.md +0 -153
  185. package/dist/skills/data-scientist/references/performance-benchmarks.md +0 -37
  186. package/packages/omo-codex/plugin/skills/data-scientist/references/common-scenarios.md +0 -176
  187. package/packages/omo-codex/plugin/skills/data-scientist/references/execution-templates.md +0 -197
  188. package/packages/omo-codex/plugin/skills/data-scientist/references/integration-patterns.md +0 -153
  189. package/packages/omo-codex/plugin/skills/data-scientist/references/performance-benchmarks.md +0 -37
  190. package/packages/shared-skills/skills/data-scientist/references/common-scenarios.md +0 -176
  191. package/packages/shared-skills/skills/data-scientist/references/execution-templates.md +0 -197
  192. package/packages/shared-skills/skills/data-scientist/references/integration-patterns.md +0 -153
  193. package/packages/shared-skills/skills/data-scientist/references/performance-benchmarks.md +0 -37
package/dist/tui.js CHANGED
@@ -17499,6 +17499,25 @@ var init_telemetry = __esm(() => {
17499
17499
  }).strict();
17500
17500
  });
17501
17501
 
17502
+ // packages/omo-config-core/src/schema/format-on-mutation.ts
17503
+ var mode, languages, OmoFormatOnMutationLayerSchema, OmoFormatOnMutationSchema;
17504
+ var init_format_on_mutation = __esm(() => {
17505
+ init_zod();
17506
+ mode = _enum2(["off", "best-effort", "required"]);
17507
+ languages = record(string2(), boolean2()).optional();
17508
+ OmoFormatOnMutationLayerSchema = object({
17509
+ mode: mode.optional(),
17510
+ languages,
17511
+ maxFileBytes: number2().int().positive().optional(),
17512
+ timeoutMs: number2().int().positive().optional()
17513
+ }).strict();
17514
+ OmoFormatOnMutationSchema = OmoFormatOnMutationLayerSchema.extend({
17515
+ mode: mode.default("best-effort"),
17516
+ maxFileBytes: number2().int().positive().default(1048576),
17517
+ timeoutMs: number2().int().positive().default(3000)
17518
+ }).strict();
17519
+ });
17520
+
17502
17521
  // packages/omo-config-core/src/schema/config.ts
17503
17522
  var OmoOpenCodeHarnessConfigSchema, OmoTypedHarnessConfigSchema, OmoConfigProfileSchema, OmoConfigSchema, OmoConfigLayerSchema;
17504
17523
  var init_config = __esm(() => {
@@ -17512,8 +17531,10 @@ var init_config = __esm(() => {
17512
17531
  init_task();
17513
17532
  init_team();
17514
17533
  init_telemetry();
17534
+ init_format_on_mutation();
17515
17535
  OmoOpenCodeHarnessConfigSchema = record(string2(), unknown());
17516
17536
  OmoTypedHarnessConfigSchema = object({
17537
+ formatOnMutation: OmoFormatOnMutationLayerSchema.optional(),
17517
17538
  categories: OmoCategoriesConfigSchema.optional(),
17518
17539
  agents: OmoAgentsConfigSchema.optional(),
17519
17540
  codegraph: OmoCodegraphSettingsLayerSchema.optional(),
@@ -17525,6 +17546,7 @@ var init_config = __esm(() => {
17525
17546
  telemetry: OmoTelemetrySettingsLayerSchema.optional()
17526
17547
  }).strict();
17527
17548
  OmoConfigProfileSchema = object({
17549
+ formatOnMutation: OmoFormatOnMutationLayerSchema.optional(),
17528
17550
  categories: OmoCategoriesConfigSchema.optional(),
17529
17551
  agents: OmoAgentsConfigSchema.optional(),
17530
17552
  codegraph: OmoCodegraphSettingsLayerSchema.optional(),
@@ -17539,6 +17561,7 @@ var init_config = __esm(() => {
17539
17561
  "[codex]": OmoTypedHarnessConfigSchema.optional()
17540
17562
  }).strict();
17541
17563
  OmoConfigSchema = object({
17564
+ formatOnMutation: OmoFormatOnMutationSchema.optional(),
17542
17565
  $schema: string2().optional(),
17543
17566
  categories: OmoCategoriesConfigSchema.optional(),
17544
17567
  agents: OmoAgentsConfigSchema.optional(),
@@ -17557,6 +17580,7 @@ var init_config = __esm(() => {
17557
17580
  legacy_migrations: record(string2(), unknown()).optional()
17558
17581
  }).strict();
17559
17582
  OmoConfigLayerSchema = object({
17583
+ formatOnMutation: OmoFormatOnMutationLayerSchema.optional(),
17560
17584
  $schema: string2().optional(),
17561
17585
  categories: OmoCategoriesConfigSchema.optional(),
17562
17586
  agents: OmoAgentsConfigSchema.optional(),
@@ -17583,6 +17607,7 @@ var init_schema = __esm(() => {
17583
17607
  init_codegraph();
17584
17608
  init_config();
17585
17609
  init_fallback_models();
17610
+ init_format_on_mutation();
17586
17611
  init_git_master();
17587
17612
  init_harness();
17588
17613
  init_memory();
@@ -18651,6 +18676,9 @@ var init_message_inspection_error = () => {};
18651
18676
  function getPromptGateMessagesFetchTimeoutMs() {
18652
18677
  return promptGateMessagesFetchTimeoutMsForTesting ?? DEFAULT_PROMPT_GATE_MESSAGES_FETCH_TIMEOUT_MS;
18653
18678
  }
18679
+ function getPromptGateRevalidationTimeoutMs() {
18680
+ return promptGateRevalidationTimeoutMsForTesting ?? DEFAULT_PROMPT_REVALIDATION_TIMEOUT_MS;
18681
+ }
18654
18682
  async function withDispatchTimeout(operation, dispatchTimeoutMs, operationName) {
18655
18683
  if (dispatchTimeoutMs <= 0) {
18656
18684
  return operation;
@@ -18669,7 +18697,7 @@ async function withDispatchTimeout(operation, dispatchTimeoutMs, operationName)
18669
18697
  }
18670
18698
  }
18671
18699
  }
18672
- var DEFAULT_PROMPT_GATE_MESSAGES_FETCH_TIMEOUT_MS = 5000, promptGateMessagesFetchTimeoutMsForTesting;
18700
+ var DEFAULT_PROMPT_GATE_MESSAGES_FETCH_TIMEOUT_MS = 5000, DEFAULT_PROMPT_REVALIDATION_TIMEOUT_MS = 5000, promptGateMessagesFetchTimeoutMsForTesting, promptGateRevalidationTimeoutMsForTesting;
18673
18701
 
18674
18702
  // packages/utils/src/prompt-async-gate/pending-tool-turn.ts
18675
18703
  function getPromptQuery(input) {
@@ -18866,6 +18894,7 @@ async function dispatchAfterSessionIdle(args) {
18866
18894
  checkStatus,
18867
18895
  checkToolState,
18868
18896
  shouldDispatch,
18897
+ retryDispatchFailure,
18869
18898
  dispatch
18870
18899
  } = args;
18871
18900
  const existing = getActiveReservation(sessionID);
@@ -18920,7 +18949,9 @@ async function dispatchAfterSessionIdle(args) {
18920
18949
  });
18921
18950
  return { status: "active" };
18922
18951
  }
18923
- if (shouldDispatch?.() === false) {
18952
+ const dispatchDecision = shouldDispatch?.();
18953
+ const resolvedDispatchDecision = dispatchDecision !== undefined && typeof dispatchDecision !== "boolean" ? await withDispatchTimeout(dispatchDecision, Math.min(dispatchTimeoutMs, getPromptGateRevalidationTimeoutMs()), `[prompt-async-gate] ${sessionName} shouldDispatch`) : dispatchDecision;
18954
+ if (resolvedDispatchDecision === false) {
18924
18955
  log(`[prompt-async-gate] ${sessionName} cancelled before dispatch`, { sessionID, source });
18925
18956
  return { status: "cancelled" };
18926
18957
  }
@@ -18936,6 +18967,7 @@ async function dispatchAfterSessionIdle(args) {
18936
18967
  log(`[prompt-async-gate] ${sessionName} dispatched`, { sessionID, source });
18937
18968
  return { status: "dispatched", response };
18938
18969
  } catch (error51) {
18970
+ const queueRetryable = dispatchAttempted && retryDispatchFailure?.(error51) === true;
18939
18971
  if (dispatchAttempted) {
18940
18972
  rememberRecentPromptDispatch({
18941
18973
  sessionID,
@@ -18946,7 +18978,12 @@ async function dispatchAfterSessionIdle(args) {
18946
18978
  }
18947
18979
  const errorText = error51 instanceof Error ? `${error51.name}: ${error51.message}` : String(error51);
18948
18980
  log(`[prompt-async-gate] ${sessionName} failed`, { sessionID, source, error: errorText });
18949
- return { status: "failed", error: error51, dispatchAttempted };
18981
+ return {
18982
+ status: "failed",
18983
+ error: error51,
18984
+ dispatchAttempted,
18985
+ ...queueRetryable ? { queueRetryable: true } : {}
18986
+ };
18950
18987
  } finally {
18951
18988
  finishPromptReservation(sessionID, reservation, dispatchAttempted, postDispatchHoldMs);
18952
18989
  }
@@ -18999,6 +19036,7 @@ function schedulePromptQueueDrain(sessionID, delayMs) {
18999
19036
  promptQueueTimers.set(sessionID, timer);
19000
19037
  }
19001
19038
  function removePromptQueueEntry(sessionID, entry) {
19039
+ promptQueueRetryStates.delete(entry.id);
19002
19040
  const queue = promptQueues.get(sessionID);
19003
19041
  if (!queue) {
19004
19042
  return;
@@ -19013,6 +19051,7 @@ async function drainPromptQueue(sessionID, awaitedEntry) {
19013
19051
  promptQueueDraining.add(sessionID);
19014
19052
  clearPromptQueueTimer(sessionID);
19015
19053
  let awaitedResult;
19054
+ const deferredRetryIds = new Set;
19016
19055
  try {
19017
19056
  while (true) {
19018
19057
  const queue = promptQueues.get(sessionID);
@@ -19020,6 +19059,20 @@ async function drainPromptQueue(sessionID, awaitedEntry) {
19020
19059
  if (!entry) {
19021
19060
  break;
19022
19061
  }
19062
+ const retryState = promptQueueRetryStates.get(entry.id);
19063
+ if (entry.durableRetry && retryState?.retryAfter !== undefined && retryState.retryAfter > Date.now()) {
19064
+ if (deferredRetryIds.has(entry.id)) {
19065
+ const nextRetryAt = Math.min(...queue.flatMap((queued) => {
19066
+ const retryAfter = promptQueueRetryStates.get(queued.id)?.retryAfter;
19067
+ return retryAfter === undefined ? [] : [retryAfter];
19068
+ }));
19069
+ schedulePromptQueueDrain(sessionID, nextRetryAt - Date.now());
19070
+ break;
19071
+ }
19072
+ deferredRetryIds.add(entry.id);
19073
+ setPromptQueue(sessionID, [...queue.slice(1), entry]);
19074
+ continue;
19075
+ }
19023
19076
  promptQueueInFlight.set(sessionID, entry);
19024
19077
  const result = await dispatchAfterSessionIdle({
19025
19078
  sessionName: entry.sessionName,
@@ -19035,12 +19088,45 @@ async function drainPromptQueue(sessionID, awaitedEntry) {
19035
19088
  checkStatus: entry.checkStatus,
19036
19089
  checkToolState: entry.checkToolState,
19037
19090
  shouldDispatch: entry.shouldDispatch,
19091
+ retryDispatchFailure: entry.retryDispatchFailure,
19038
19092
  dispatch: entry.dispatch
19039
19093
  });
19040
19094
  if (promptQueueInFlight.get(sessionID)?.id === entry.id) {
19041
19095
  promptQueueInFlight.delete(sessionID);
19042
19096
  }
19043
- if (result.status === "active" || result.status === "reserved") {
19097
+ const retryableQueuedFailure = result.status === "failed" && (!result.dispatchAttempted || result.queueRetryable === true);
19098
+ if (retryableQueuedFailure) {
19099
+ const failureCount = (promptQueueRetryStates.get(entry.id)?.failureCount ?? 0) + 1;
19100
+ if (entry.durableRetry) {
19101
+ const exponentialBackoffMs = entry.queueRetryMs * 2 ** Math.min(failureCount, 7);
19102
+ const retryDelayMs = Math.max(entry.postDispatchHoldMs, Math.min(exponentialBackoffMs, MAX_DURABLE_RETRY_BACKOFF_MS));
19103
+ promptQueueRetryStates.set(entry.id, {
19104
+ failureCount,
19105
+ retryAfter: Date.now() + retryDelayMs
19106
+ });
19107
+ const currentQueue = promptQueues.get(sessionID);
19108
+ if (currentQueue && currentQueue.length > 1) {
19109
+ setPromptQueue(sessionID, [...currentQueue.slice(1), entry]);
19110
+ continue;
19111
+ }
19112
+ schedulePromptQueueDrain(sessionID, retryDelayMs);
19113
+ break;
19114
+ }
19115
+ if (failureCount >= MAX_QUEUED_RETRYABLE_FAILURES) {
19116
+ log("[prompt-async-gate] dropping queued prompt after repeated retryable failures", {
19117
+ sessionID,
19118
+ source: entry.source,
19119
+ failureCount
19120
+ });
19121
+ removePromptQueueEntry(sessionID, entry);
19122
+ if (awaitedEntry?.id === entry.id) {
19123
+ awaitedResult = result;
19124
+ }
19125
+ continue;
19126
+ }
19127
+ promptQueueRetryStates.set(entry.id, { failureCount });
19128
+ }
19129
+ if (result.status === "active" || result.status === "reserved" || retryableQueuedFailure) {
19044
19130
  const queued = queuedResult(entry, 1, result.status === "reserved" ? result.reservedBy : entry.source);
19045
19131
  if (awaitedEntry?.id === entry.id) {
19046
19132
  awaitedResult = queued;
@@ -19064,7 +19150,7 @@ async function drainPromptQueue(sessionID, awaitedEntry) {
19064
19150
  }
19065
19151
  return awaitedResult;
19066
19152
  }
19067
- var promptQueues, promptQueueDraining, promptQueueInFlight, promptQueueTimers;
19153
+ var promptQueues, promptQueueDraining, promptQueueInFlight, promptQueueTimers, promptQueueRetryStates, MAX_QUEUED_RETRYABLE_FAILURES = 3, MAX_DURABLE_RETRY_BACKOFF_MS = 30000;
19068
19154
  var init_queue = __esm(() => {
19069
19155
  init_session_idle_dispatch();
19070
19156
  init_reservations();
@@ -19072,6 +19158,7 @@ var init_queue = __esm(() => {
19072
19158
  promptQueueDraining = new Set;
19073
19159
  promptQueueInFlight = new Map;
19074
19160
  promptQueueTimers = new Map;
19161
+ promptQueueRetryStates = new Map;
19075
19162
  setExpiredReservationHandler((sessionID) => {
19076
19163
  schedulePromptQueueDrain(sessionID, 0);
19077
19164
  });
@@ -78070,7 +78157,17 @@ function isPreSendConnectionFailure(error51) {
78070
78157
  if (error51.name === "AbortError") {
78071
78158
  return false;
78072
78159
  }
78073
- const CONNECTION_CODES = new Set(["ECONNREFUSED", "ENOTFOUND", "EAI_AGAIN"]);
78160
+ const CONNECTION_CODES = new Set([
78161
+ "ECONNREFUSED",
78162
+ "ENOTFOUND",
78163
+ "EAI_AGAIN",
78164
+ "ENETUNREACH",
78165
+ "EHOSTUNREACH",
78166
+ "ENETDOWN",
78167
+ "EHOSTDOWN",
78168
+ "EADDRNOTAVAIL",
78169
+ "UND_ERR_CONNECT_TIMEOUT"
78170
+ ]);
78074
78171
  const self = error51;
78075
78172
  if (self.code && CONNECTION_CODES.has(self.code)) {
78076
78173
  return true;
@@ -78082,12 +78179,6 @@ function isPreSendConnectionFailure(error51) {
78082
78179
  return true;
78083
78180
  }
78084
78181
  }
78085
- if (error51 instanceof TypeError) {
78086
- const msg = error51.message;
78087
- if (msg.includes("fetch failed") || msg.includes("Unable to connect")) {
78088
- return true;
78089
- }
78090
- }
78091
78182
  return false;
78092
78183
  }
78093
78184
  function markLiveRouteUnavailable(reason) {
@@ -78411,7 +78502,6 @@ var sessionCategoryMap;
78411
78502
  var init_session_category_registry = __esm(() => {
78412
78503
  sessionCategoryMap = new Map;
78413
78504
  });
78414
-
78415
78505
  // packages/omo-opencode/src/shared/legacy-plugin-warning.ts
78416
78506
  var init_legacy_plugin_warning = __esm(() => {
78417
78507
  init_jsonc_parser2();
@@ -81989,7 +82079,7 @@ function registerSidebarContentSlot({
81989
82079
  registerSlot({
81990
82080
  order: 900,
81991
82081
  slots: {
81992
- sidebar_content: renderSidebar
82082
+ sidebar_content: () => renderSidebar
81993
82083
  }
81994
82084
  });
81995
82085
  requestRender();
@@ -82110,5 +82200,6 @@ var module = {
82110
82200
  var tui_default = module;
82111
82201
  export {
82112
82202
  tui_default as default,
82113
- handleTuiPollError
82203
+ handleTuiPollError,
82204
+ registerSidebarContentSlot
82114
82205
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oh-my-opencode",
3
- "version": "5.0.0-beta.25",
3
+ "version": "5.0.0-beta.28",
4
4
  "description": "The Best AI Agent Harness - Batteries-Included OpenCode Plugin with Multi-Model Orchestration, Parallel Background Agents, and Crafted LSP/AST Tools",
5
5
  "main": "./dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -181,7 +181,7 @@
181
181
  "zod": "^4.4.3"
182
182
  },
183
183
  "devDependencies": {
184
- "@code-yeongyu/senpi": "2026.8.26-2",
184
+ "@code-yeongyu/senpi": "2026.8.30",
185
185
  "@oh-my-opencode/agents-md-core": "workspace:*",
186
186
  "@oh-my-opencode/ast-grep-mcp": "workspace:*",
187
187
  "@oh-my-opencode/boulder-state": "workspace:*",
@@ -222,18 +222,18 @@
222
222
  "typescript": "^7.0.2"
223
223
  },
224
224
  "optionalDependencies": {
225
- "oh-my-opencode-darwin-arm64": "5.0.0-beta.25",
226
- "oh-my-opencode-darwin-x64": "5.0.0-beta.25",
227
- "oh-my-opencode-darwin-x64-baseline": "5.0.0-beta.25",
228
- "oh-my-opencode-linux-arm64": "5.0.0-beta.25",
229
- "oh-my-opencode-linux-arm64-musl": "5.0.0-beta.25",
230
- "oh-my-opencode-linux-x64": "5.0.0-beta.25",
231
- "oh-my-opencode-linux-x64-baseline": "5.0.0-beta.25",
232
- "oh-my-opencode-linux-x64-musl": "5.0.0-beta.25",
233
- "oh-my-opencode-linux-x64-musl-baseline": "5.0.0-beta.25",
234
- "oh-my-opencode-windows-arm64": "5.0.0-beta.25",
235
- "oh-my-opencode-windows-x64": "5.0.0-beta.25",
236
- "oh-my-opencode-windows-x64-baseline": "5.0.0-beta.25"
225
+ "oh-my-opencode-darwin-arm64": "5.0.0-beta.28",
226
+ "oh-my-opencode-darwin-x64": "5.0.0-beta.28",
227
+ "oh-my-opencode-darwin-x64-baseline": "5.0.0-beta.28",
228
+ "oh-my-opencode-linux-arm64": "5.0.0-beta.28",
229
+ "oh-my-opencode-linux-arm64-musl": "5.0.0-beta.28",
230
+ "oh-my-opencode-linux-x64": "5.0.0-beta.28",
231
+ "oh-my-opencode-linux-x64-baseline": "5.0.0-beta.28",
232
+ "oh-my-opencode-linux-x64-musl": "5.0.0-beta.28",
233
+ "oh-my-opencode-linux-x64-musl-baseline": "5.0.0-beta.28",
234
+ "oh-my-opencode-windows-arm64": "5.0.0-beta.28",
235
+ "oh-my-opencode-windows-x64": "5.0.0-beta.28",
236
+ "oh-my-opencode-windows-x64-baseline": "5.0.0-beta.28"
237
237
  },
238
238
  "overrides": {
239
239
  "@earendil-works/pi-agent-core": "0.84.2",
@@ -249,5 +249,8 @@
249
249
  },
250
250
  "trustedDependencies": [
251
251
  "@code-yeongyu/comment-checker"
252
- ]
252
+ ],
253
+ "patchedDependencies": {
254
+ "@code-yeongyu/senpi@2026.8.30": "patches/@code-yeongyu%2Fsenpi@2026.8.30.patch"
255
+ }
253
256
  }
@@ -7,6 +7,7 @@ export * from "./lsp/constants.js";
7
7
  export * from "./lsp/directory-diagnostics.js";
8
8
  export * from "./lsp/effective-extension.js";
9
9
  export * from "./lsp/errors.js";
10
+ export * from "./lsp/format-document.js";
10
11
  export * from "./lsp/formatters.js";
11
12
  export * from "./lsp/infer-extension.js";
12
13
  export * from "./lsp/json-rpc-connection.js";
@@ -17,6 +17,7 @@ import {
17
17
  } from "./errors.js";
18
18
  import { getLspManager, type LspManager } from "./manager.js";
19
19
  import { findWorkspaceRootOutsideContext } from "./outside-context-workspace.js";
20
+ import { LSP_LOCAL_INSTALL_HINTS } from "./server-definitions.js";
20
21
  import { loadInstallDecision } from "./server-install-state.js";
21
22
  import { findServerForExtension } from "./server-resolution.js";
22
23
  import type { ServerLookupResult } from "./types.js";
@@ -138,6 +139,26 @@ export function formatServerLookupError(result: Exclude<ServerLookupResult, { st
138
139
  ].join("\n");
139
140
  }
140
141
 
142
+ /**
143
+ * Builds the install guidance, offering the repo-local install first.
144
+ *
145
+ * A repo-local install is resolvable without touching the global environment, so it is the
146
+ * recommended route whenever the server ships as a project dependency.
147
+ */
148
+ function formatInstallOptions(serverId: string, installHint: string): string[] {
149
+ const localHint = LSP_LOCAL_INSTALL_HINTS[serverId];
150
+ if (localHint === undefined) {
151
+ return ["To install, run:", ` ${installHint}`];
152
+ }
153
+ return [
154
+ "To install in THIS repository (preferred — no global install needed):",
155
+ ` ${localHint}`,
156
+ "",
157
+ "Or install it globally:",
158
+ ` ${installHint}`,
159
+ ];
160
+ }
161
+
141
162
  function formatNotInstalled(result: Extract<ServerLookupResult, { status: "not_installed" }>): string {
142
163
  const { server, installHint } = result;
143
164
  const extensions = server.extensions.join(", ");
@@ -154,20 +175,20 @@ function formatNotInstalled(result: Extract<ServerLookupResult, { status: "not_i
154
175
  `Command not found: ${server.command[0]}`,
155
176
  "",
156
177
  ];
178
+ const installOptions = formatInstallOptions(server.id, installHint);
157
179
 
158
180
  if (decision === "allowed") {
159
181
  return [
160
182
  ...header,
161
183
  "The user has pre-authorized LSP installation. Run the install command, then retry this tool:",
162
- ` ${installHint}`,
184
+ ...installOptions.slice(1),
163
185
  ].join("\n");
164
186
  }
165
187
 
166
188
  if (!context.capabilities.installDecisionTool) {
167
189
  return [
168
190
  ...header,
169
- "To install, run:",
170
- ` ${installHint}`,
191
+ ...installOptions,
171
192
  "",
172
193
  "ACTION REQUIRED — ASK THE USER whether to install this LSP server.",
173
194
  "Install-decision recording is unavailable in this harness; proceed without LSP if the user declines.",
@@ -176,8 +197,7 @@ function formatNotInstalled(result: Extract<ServerLookupResult, { status: "not_i
176
197
 
177
198
  return [
178
199
  ...header,
179
- "To install, run:",
180
- ` ${installHint}`,
200
+ ...installOptions,
181
201
  "",
182
202
  "ACTION REQUIRED — ASK THE USER whether to install this LSP server.",
183
203
  "- If the user agrees: run the install command above, then retry this tool.",
@@ -6,6 +6,7 @@ import type { LspClientTimeoutOptions } from "./transport.js";
6
6
  import type {
7
7
  Diagnostic,
8
8
  DocumentSymbol,
9
+ FormattingOptions,
9
10
  Location,
10
11
  LocationLink,
11
12
  PrepareRenameDefaultBehavior,
@@ -13,6 +14,7 @@ import type {
13
14
  Range,
14
15
  ResolvedServer,
15
16
  SymbolInfo,
17
+ TextEdit,
16
18
  WorkspaceEdit,
17
19
  } from "./types.js";
18
20
  import { LspRequestTimeoutError } from "./errors.js";
@@ -133,6 +135,30 @@ export class LspClient extends LspClientConnection {
133
135
  }, options);
134
136
  }
135
137
 
138
+ /**
139
+ * Requests whole-document formatting edits, returning null when the server never advertised the
140
+ * capability so callers can report unavailability instead of surfacing a method-not-found error.
141
+ */
142
+ async formatDocument(
143
+ filePath: string,
144
+ options: FormattingOptions,
145
+ signal?: AbortSignal,
146
+ ): Promise<TextEdit[] | null> {
147
+ if (!this.isDocumentFormattingSupported()) return null;
148
+ const absPath = this.resolveWorkspacePath(filePath);
149
+ await this.openFile(absPath);
150
+ const requestOptions = signal === undefined ? {} : { signal };
151
+ const edits = await this.sendRequest<TextEdit[] | null>(
152
+ "textDocument/formatting",
153
+ {
154
+ textDocument: { uri: pathToFileURL(absPath).href },
155
+ options,
156
+ },
157
+ requestOptions,
158
+ );
159
+ return edits ?? [];
160
+ }
161
+
136
162
  async workspaceSymbols(query: string, signal?: AbortSignal): Promise<SymbolInfo[]> {
137
163
  const options = signal === undefined ? {} : { signal };
138
164
  return this.sendRequest<SymbolInfo[]>("workspace/symbol", { query }, options);
@@ -4,6 +4,7 @@ import { LspClientTransport } from "./transport.js";
4
4
 
5
5
  interface InitializeCapabilities {
6
6
  readonly diagnosticProvider?: unknown;
7
+ readonly documentFormattingProvider?: unknown;
7
8
  }
8
9
 
9
10
  function supportsDiagnosticPull(capabilities: InitializeCapabilities | undefined): boolean {
@@ -11,6 +12,19 @@ function supportsDiagnosticPull(capabilities: InitializeCapabilities | undefined
11
12
  return Object.hasOwn(capabilities, "diagnosticProvider");
12
13
  }
13
14
 
15
+ /**
16
+ * Reads the whole-document formatting capability the server advertised at initialize time.
17
+ *
18
+ * The provider is either a boolean or an options object, and an explicit `false` means the
19
+ * server refuses the request, so both shapes are checked instead of mere key presence.
20
+ */
21
+ function supportsDocumentFormatting(capabilities: InitializeCapabilities | undefined): boolean {
22
+ if (capabilities === undefined) return false;
23
+ const provider = capabilities.documentFormattingProvider;
24
+ if (provider === undefined || provider === null || provider === false) return false;
25
+ return provider === true || typeof provider === "object";
26
+ }
27
+
14
28
  export class LspClientConnection extends LspClientTransport {
15
29
  async initialize(): Promise<void> {
16
30
  const rootUri = pathToFileURL(this.root).href;
@@ -28,6 +42,7 @@ export class LspClientConnection extends LspClientTransport {
28
42
  references: {},
29
43
  documentSymbol: { hierarchicalDocumentSymbolSupport: true },
30
44
  publishDiagnostics: {},
45
+ formatting: { dynamicRegistration: false },
31
46
  rename: {
32
47
  prepareSupport: true,
33
48
  prepareSupportDefaultBehavior: 1,
@@ -71,6 +86,7 @@ export class LspClientConnection extends LspClientTransport {
71
86
  { timeoutMs: this.initializeTimeoutMs },
72
87
  );
73
88
  this.setDiagnosticPullSupported(supportsDiagnosticPull(result?.capabilities));
89
+ this.setDocumentFormattingSupported(supportsDocumentFormatting(result?.capabilities));
74
90
  await this.sendNotification("initialized", {});
75
91
  await this.sendNotification("workspace/didChangeConfiguration", {
76
92
  settings: { json: { validate: { enable: true } } },
@@ -6,6 +6,7 @@ export const DEFAULT_MAX_DIRECTORY_FILES = 50;
6
6
  export const REQUEST_TIMEOUT_MS = 15_000;
7
7
  export const INIT_TIMEOUT_MS = 60_000;
8
8
  export const IDLE_TIMEOUT_MS = 5 * 60_000;
9
+ export const MAX_RESIDENT_CLIENTS = 6;
9
10
  export const REAPER_INTERVAL_MS = 60_000;
10
11
  export const STOP_HARD_KILL_TIMEOUT_MS = 5_000;
11
12
  export const STOP_SIGKILL_GRACE_MS = 1_000;
@@ -0,0 +1,143 @@
1
+ import { mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+
5
+ import { afterEach, describe, expect, it } from "bun:test";
6
+
7
+ import type { LspClient } from "./client.js";
8
+ import { formatDocumentWithClient } from "./format-document.js";
9
+ import type { TextEdit } from "./types.js";
10
+
11
+ const workspaces: string[] = [];
12
+
13
+ afterEach(() => {
14
+ for (const workspace of workspaces.splice(0)) rmSync(workspace, { recursive: true, force: true });
15
+ });
16
+
17
+ function workspaceWith(fileName: string, content: string): { readonly root: string; readonly filePath: string } {
18
+ const root = mkdtempSync(join(tmpdir(), "lsp-format-"));
19
+ workspaces.push(root);
20
+ const filePath = join(root, fileName);
21
+ writeFileSync(filePath, content, "utf-8");
22
+ return { root, filePath };
23
+ }
24
+
25
+ function stubClient(edits: TextEdit[] | null): LspClient {
26
+ return {
27
+ async formatDocument() {
28
+ return edits;
29
+ },
30
+ async openFile() {},
31
+ } as Pick<LspClient, "formatDocument" | "openFile"> as LspClient;
32
+ }
33
+
34
+ describe("formatDocumentWithClient", () => {
35
+ it("#given a server returning text edits #when the document is formatted #then the file is rewritten and line deltas are reported", async () => {
36
+ // given
37
+ const { filePath } = workspaceWith("sample.ts", "const a=1\nconst b =2\nconst c=3\n");
38
+ const edits: TextEdit[] = [
39
+ { range: { start: { line: 0, character: 0 }, end: { line: 1, character: 12 } }, newText: "const a = 1;\nconst b = 2;" },
40
+ ];
41
+
42
+ // when
43
+ const result = await formatDocumentWithClient(stubClient(edits), filePath);
44
+
45
+ // then
46
+ expect(result.status).toBe("formatted");
47
+ expect(readFileSync(filePath, "utf-8")).toBe("const a = 1;\nconst b = 2;\nconst c=3\n");
48
+ expect(result).toMatchObject({ status: "formatted", linesAdded: 2, linesRemoved: 2 });
49
+ });
50
+
51
+ it("#given a server returning an edit that adds lines #when the document is formatted #then the added and removed line counts differ", async () => {
52
+ // given
53
+ const { filePath } = workspaceWith("sample.ts", "const a=1;const b=2\n");
54
+ const edits: TextEdit[] = [
55
+ { range: { start: { line: 0, character: 0 }, end: { line: 0, character: 19 } }, newText: "const a = 1;\nconst b = 2;" },
56
+ ];
57
+
58
+ // when
59
+ const result = await formatDocumentWithClient(stubClient(edits), filePath);
60
+
61
+ // then
62
+ expect(result).toMatchObject({ status: "formatted", linesAdded: 2, linesRemoved: 1 });
63
+ expect(readFileSync(filePath, "utf-8")).toBe("const a = 1;\nconst b = 2;\n");
64
+ });
65
+
66
+ it("#given a server that does not advertise document formatting #when the document is formatted #then the result is unavailable and the file bytes are untouched", async () => {
67
+ // given
68
+ const original = "const a=1\n";
69
+ const { filePath } = workspaceWith("sample.ts", original);
70
+ const before = statSync(filePath);
71
+
72
+ // when
73
+ const result = await formatDocumentWithClient(stubClient(null), filePath);
74
+
75
+ // then
76
+ expect(result).toEqual({ status: "unavailable", reason: "capability_not_advertised" });
77
+ expect(readFileSync(filePath)).toEqual(Buffer.from(original));
78
+ expect(statSync(filePath).mtimeMs).toBe(before.mtimeMs);
79
+ });
80
+
81
+ it("#given a server returning no edits #when the document is formatted #then the result is unchanged and the file bytes are untouched", async () => {
82
+ // given
83
+ const original = "const a = 1;\n";
84
+ const { filePath } = workspaceWith("sample.ts", original);
85
+
86
+ // when
87
+ const result = await formatDocumentWithClient(stubClient([]), filePath);
88
+
89
+ // then
90
+ expect(result).toEqual({ status: "unchanged", linesAdded: 0, linesRemoved: 0 });
91
+ expect(readFileSync(filePath)).toEqual(Buffer.from(original));
92
+ });
93
+
94
+ it("#given edits that reproduce the current bytes #when the document is formatted #then the result is unchanged", async () => {
95
+ // given
96
+ const original = "const a = 1;\n";
97
+ const { filePath } = workspaceWith("sample.ts", original);
98
+ const edits: TextEdit[] = [
99
+ { range: { start: { line: 0, character: 0 }, end: { line: 0, character: 12 } }, newText: "const a = 1;" },
100
+ ];
101
+
102
+ // when
103
+ const result = await formatDocumentWithClient(stubClient(edits), filePath);
104
+
105
+ // then
106
+ expect(result.status).toBe("unchanged");
107
+ expect(readFileSync(filePath, "utf-8")).toBe(original);
108
+ });
109
+
110
+ it("#given the file shrinks after the server computed its edits #when the edits are applied #then the stale edit is rejected instead of corrupting the file", async () => {
111
+ // given
112
+ const { filePath } = workspaceWith("sample.ts", "const a=1\nconst b=2\n");
113
+ const staleEdits: TextEdit[] = [
114
+ { range: { start: { line: 1, character: 0 }, end: { line: 1, character: 9 } }, newText: "const b = 2;" },
115
+ ];
116
+ const racingClient = {
117
+ async formatDocument() {
118
+ writeFileSync(filePath, "short\n", "utf-8");
119
+ return staleEdits;
120
+ },
121
+ async openFile() {},
122
+ } as Pick<LspClient, "formatDocument" | "openFile"> as LspClient;
123
+
124
+ // when / then
125
+ await expect(formatDocumentWithClient(racingClient, filePath)).rejects.toThrow(/outside line/);
126
+ expect(readFileSync(filePath, "utf-8")).toBe("short\n");
127
+ });
128
+
129
+ it("#given a formatted document #when the write completes #then no temporary file is left behind", async () => {
130
+ // given
131
+ const { root, filePath } = workspaceWith("sample.ts", "const a=1\n");
132
+ const edits: TextEdit[] = [
133
+ { range: { start: { line: 0, character: 0 }, end: { line: 0, character: 9 } }, newText: "const a = 1;" },
134
+ ];
135
+
136
+ // when
137
+ await formatDocumentWithClient(stubClient(edits), filePath);
138
+
139
+ // then
140
+ expect(readFileSync(filePath, "utf-8")).toBe("const a = 1;\n");
141
+ expect(() => statSync(join(root, "sample.ts.tmp"))).toThrow();
142
+ });
143
+ });