@oh-my-pi/pi-coding-agent 17.0.8 → 17.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (340) hide show
  1. package/CHANGELOG.md +87 -1
  2. package/README.md +1 -1
  3. package/dist/cli.js +5495 -5021
  4. package/dist/types/async/job-manager.d.ts +37 -1
  5. package/dist/types/cli/args.d.ts +2 -0
  6. package/dist/types/cli/bench-cli.d.ts +41 -0
  7. package/dist/types/commands/bench.d.ts +15 -3
  8. package/dist/types/commands/launch.d.ts +4 -0
  9. package/dist/types/config/__tests__/model-registry.test.d.ts +1 -0
  10. package/dist/types/config/model-registry.d.ts +30 -1
  11. package/dist/types/config/models-config-schema.d.ts +18 -6
  12. package/dist/types/config/models-config.d.ts +14 -5
  13. package/dist/types/config/provider-globals.d.ts +2 -2
  14. package/dist/types/config/service-tier.d.ts +5 -1
  15. package/dist/types/config/settings-schema.d.ts +350 -50
  16. package/dist/types/config/settings.d.ts +7 -0
  17. package/dist/types/dap/client.d.ts +20 -0
  18. package/dist/types/exec/bash-executor.d.ts +38 -0
  19. package/dist/types/exec/direnv.d.ts +33 -0
  20. package/dist/types/export/html/args.d.ts +15 -0
  21. package/dist/types/export/html/index.d.ts +11 -26
  22. package/dist/types/export/html/web-palette.d.ts +55 -116
  23. package/dist/types/extensibility/extensions/loader.d.ts +3 -0
  24. package/dist/types/extensibility/extensions/types.d.ts +17 -2
  25. package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +1 -0
  26. package/dist/types/extensibility/legacy-pi-tui-shim.d.ts +10 -0
  27. package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +14 -1
  28. package/dist/types/internal-urls/registry-helpers.d.ts +11 -0
  29. package/dist/types/live/controller.d.ts +40 -0
  30. package/dist/types/live/protocol.d.ts +85 -0
  31. package/dist/types/live/protocol.test.d.ts +1 -0
  32. package/dist/types/live/transport.d.ts +48 -0
  33. package/dist/types/live/visualizer.d.ts +33 -0
  34. package/dist/types/lsp/client.d.ts +18 -1
  35. package/dist/types/mcp/config-writer.test.d.ts +1 -0
  36. package/dist/types/mcp/manager.d.ts +6 -2
  37. package/dist/types/mcp/oauth-discovery.d.ts +2 -0
  38. package/dist/types/mcp/render.d.ts +2 -2
  39. package/dist/types/mcp/smithery-auth.d.ts +1 -1
  40. package/dist/types/mcp/smithery-auth.test.d.ts +1 -0
  41. package/dist/types/mcp/tool-bridge.d.ts +7 -3
  42. package/dist/types/mcp/types.d.ts +6 -0
  43. package/dist/types/memory-backend/tool-names.d.ts +2 -0
  44. package/dist/types/modes/components/login-dialog.d.ts +2 -2
  45. package/dist/types/modes/components/oauth-selector.d.ts +8 -0
  46. package/dist/types/modes/components/plan-review-overlay.d.ts +26 -0
  47. package/dist/types/modes/components/settings-defs.d.ts +8 -1
  48. package/dist/types/modes/components/status-line/component.jj-cache.test.d.ts +1 -0
  49. package/dist/types/modes/components/tool-execution.d.ts +9 -1
  50. package/dist/types/modes/controllers/command-controller.d.ts +1 -1
  51. package/dist/types/modes/controllers/event-controller.d.ts +6 -1
  52. package/dist/types/modes/controllers/live-command-controller.d.ts +14 -0
  53. package/dist/types/modes/controllers/mcp-command-controller.d.ts +3 -0
  54. package/dist/types/modes/interactive-mode.d.ts +5 -0
  55. package/dist/types/modes/rpc/rpc-client.d.ts +10 -1
  56. package/dist/types/modes/rpc/rpc-frame.d.ts +16 -0
  57. package/dist/types/modes/rpc/rpc-messages.d.ts +26 -0
  58. package/dist/types/modes/rpc/rpc-types.d.ts +40 -0
  59. package/dist/types/modes/setup-wizard/scenes/sign-in.d.ts +1 -1
  60. package/dist/types/modes/setup-wizard/scenes/types.d.ts +9 -1
  61. package/dist/types/modes/setup-wizard/scenes/web-search.d.ts +1 -1
  62. package/dist/types/modes/theme/theme.d.ts +1 -1
  63. package/dist/types/modes/types.d.ts +3 -1
  64. package/dist/types/modes/utils/context-usage.d.ts +12 -2
  65. package/dist/types/plan-mode/plan-files.d.ts +10 -0
  66. package/dist/types/registry/agent-lifecycle.d.ts +21 -9
  67. package/dist/types/registry/agent-registry.d.ts +11 -4
  68. package/dist/types/sdk.d.ts +15 -1
  69. package/dist/types/secrets/index.d.ts +15 -2
  70. package/dist/types/secrets/obfuscator.d.ts +113 -15
  71. package/dist/types/session/acp-permission-gate.d.ts +19 -0
  72. package/dist/types/session/agent-session-events.d.ts +84 -0
  73. package/dist/types/session/agent-session-types.d.ts +314 -0
  74. package/dist/types/session/agent-session.d.ts +120 -750
  75. package/dist/types/session/async-job-delivery.d.ts +28 -0
  76. package/dist/types/session/bash-runner.d.ts +75 -0
  77. package/dist/types/session/blob-store.d.ts +11 -1
  78. package/dist/types/session/blob-store.test.d.ts +1 -0
  79. package/dist/types/session/checkpoint-entries.d.ts +28 -0
  80. package/dist/types/session/eval-runner.d.ts +49 -0
  81. package/dist/types/session/irc-bridge.d.ts +46 -0
  82. package/dist/types/session/messages.d.ts +22 -0
  83. package/dist/types/session/model-controls.d.ts +180 -0
  84. package/dist/types/session/prewalk.d.ts +46 -0
  85. package/dist/types/session/queued-messages.d.ts +22 -0
  86. package/dist/types/session/redis-session-storage.d.ts +1 -0
  87. package/dist/types/session/retry-fallback-chains.d.ts +74 -0
  88. package/dist/types/session/role-models.d.ts +15 -0
  89. package/dist/types/session/session-advisors.d.ts +228 -0
  90. package/dist/types/session/session-entries.d.ts +8 -0
  91. package/dist/types/session/session-handoff.d.ts +70 -0
  92. package/dist/types/session/session-history-format.d.ts +6 -1
  93. package/dist/types/session/session-loader.d.ts +6 -4
  94. package/dist/types/session/session-maintenance.d.ts +246 -0
  95. package/dist/types/session/session-manager.d.ts +41 -0
  96. package/dist/types/session/session-memory.d.ts +56 -0
  97. package/dist/types/session/session-provider-boundary.d.ts +62 -0
  98. package/dist/types/session/session-stats.d.ts +48 -0
  99. package/dist/types/session/session-tools.d.ts +143 -0
  100. package/dist/types/session/session-workspace.d.ts +32 -0
  101. package/dist/types/session/stream-guards.d.ts +44 -0
  102. package/dist/types/session/todo-tracker.d.ts +56 -0
  103. package/dist/types/session/ttsr-coordinator.d.ts +51 -0
  104. package/dist/types/session/turn-recovery.d.ts +225 -0
  105. package/dist/types/system-prompt.d.ts +2 -0
  106. package/dist/types/task/executor.d.ts +4 -1
  107. package/dist/types/task/output-manager.d.ts +3 -1
  108. package/dist/types/task/types.d.ts +14 -0
  109. package/dist/types/tools/approval.d.ts +1 -0
  110. package/dist/types/tools/browser/launch.d.ts +5 -0
  111. package/dist/types/tools/hub/jobs.d.ts +1 -1
  112. package/dist/types/tools/image-gen.d.ts +4 -4
  113. package/dist/types/tools/image-providers.d.ts +38 -0
  114. package/dist/types/tools/index.d.ts +8 -0
  115. package/dist/types/tools/path-utils.d.ts +8 -0
  116. package/dist/types/tools/report-tool-issue.d.ts +24 -9
  117. package/dist/types/tools/todo.d.ts +10 -4
  118. package/dist/types/tools/xdev.d.ts +5 -1
  119. package/dist/types/tts/streaming-player.d.ts +14 -0
  120. package/dist/types/tts/vocalizer.d.ts +5 -0
  121. package/dist/types/utils/jj.d.ts +29 -0
  122. package/dist/types/utils/lang-from-path.d.ts +1 -0
  123. package/dist/types/utils/title-generator.d.ts +30 -0
  124. package/dist/types/vibe/runtime.d.ts +36 -8
  125. package/dist/types/web/search/index.d.ts +1 -1
  126. package/dist/types/web/search/provider.d.ts +15 -5
  127. package/dist/types/web/search/providers/firecrawl.d.ts +9 -0
  128. package/dist/types/web/search/types.d.ts +95 -1
  129. package/package.json +13 -12
  130. package/scripts/generate-share-viewer.ts +4 -6
  131. package/scripts/legacy-pi-virtual-module.ts +1 -1
  132. package/src/advisor/__tests__/advisor.test.ts +643 -0
  133. package/src/advisor/runtime.ts +144 -43
  134. package/src/async/job-manager.ts +90 -2
  135. package/src/cli/args.ts +2 -0
  136. package/src/cli/auth-gateway-cli.ts +18 -3
  137. package/src/cli/bench-cli.ts +395 -29
  138. package/src/cli/flag-tables.ts +3 -0
  139. package/src/cli/gc-cli.ts +2 -2
  140. package/src/cli/grievances-cli.ts +2 -2
  141. package/src/cli/usage-cli.ts +11 -2
  142. package/src/collab/host.ts +3 -2
  143. package/src/commands/bench.ts +18 -4
  144. package/src/commands/launch.ts +4 -0
  145. package/src/config/__tests__/model-registry.test.ts +147 -0
  146. package/src/config/model-registry.ts +50 -2
  147. package/src/config/model-resolver.ts +0 -1
  148. package/src/config/models-config-schema.ts +15 -4
  149. package/src/config/provider-globals.ts +9 -9
  150. package/src/config/service-tier.ts +26 -1
  151. package/src/config/settings-schema.ts +250 -54
  152. package/src/config/settings.ts +81 -2
  153. package/src/dap/client.ts +37 -4
  154. package/src/edit/index.ts +49 -12
  155. package/src/eval/py/executor.ts +103 -33
  156. package/src/exec/bash-executor.ts +89 -6
  157. package/src/exec/direnv.ts +145 -0
  158. package/src/export/html/args.ts +20 -0
  159. package/src/export/html/index.ts +45 -47
  160. package/src/export/html/template.css +19 -1
  161. package/src/export/html/template.html +6 -0
  162. package/src/export/html/template.js +21 -0
  163. package/src/export/html/tool-views.generated.js +31 -31
  164. package/src/export/html/web-palette.ts +116 -132
  165. package/src/export/share.ts +249 -49
  166. package/src/extensibility/extensions/loader.ts +29 -1
  167. package/src/extensibility/extensions/runner.ts +6 -0
  168. package/src/extensibility/extensions/types.ts +33 -2
  169. package/src/extensibility/legacy-pi-coding-agent-shim.ts +1 -0
  170. package/src/extensibility/legacy-pi-tui-shim.ts +10 -0
  171. package/src/extensibility/plugins/legacy-pi-compat.ts +870 -311
  172. package/src/hindsight/state.ts +64 -6
  173. package/src/internal-urls/local-protocol.ts +2 -1
  174. package/src/internal-urls/memory-protocol.ts +2 -2
  175. package/src/internal-urls/registry-helpers.ts +40 -0
  176. package/src/internal-urls/skill-protocol.ts +2 -2
  177. package/src/internal-urls/ssh-protocol.ts +2 -1
  178. package/src/internal-urls/vault-protocol.ts +2 -1
  179. package/src/live/audio-worklet.txt +59 -0
  180. package/src/live/browser-runtime.txt +221 -0
  181. package/src/live/controller.ts +464 -0
  182. package/src/live/prompts/agent-final-message.md +3 -0
  183. package/src/live/prompts/live-instructions.md +23 -0
  184. package/src/live/protocol.test.ts +140 -0
  185. package/src/live/protocol.ts +233 -0
  186. package/src/live/transport.ts +490 -0
  187. package/src/live/visualizer.ts +234 -0
  188. package/src/lsp/client.ts +52 -9
  189. package/src/lsp/clients/biome-client.ts +3 -4
  190. package/src/lsp/index.ts +44 -11
  191. package/src/main.ts +9 -1
  192. package/src/mcp/config-writer.test.ts +43 -0
  193. package/src/mcp/config-writer.ts +109 -82
  194. package/src/mcp/manager.ts +39 -8
  195. package/src/mcp/oauth-discovery.ts +10 -2
  196. package/src/mcp/render.ts +94 -35
  197. package/src/mcp/smithery-auth.test.ts +29 -0
  198. package/src/mcp/smithery-auth.ts +3 -2
  199. package/src/mcp/tool-bridge.ts +107 -11
  200. package/src/mcp/types.ts +7 -0
  201. package/src/memories/index.ts +40 -20
  202. package/src/memory-backend/tool-names.ts +2 -0
  203. package/src/mnemopi/backend.ts +24 -7
  204. package/src/modes/acp/acp-agent.ts +56 -10
  205. package/src/modes/acp/acp-event-mapper.ts +8 -1
  206. package/src/modes/components/agent-hub.ts +27 -10
  207. package/src/modes/components/login-dialog.ts +14 -4
  208. package/src/modes/components/oauth-selector.ts +24 -7
  209. package/src/modes/components/plan-review-overlay.ts +350 -35
  210. package/src/modes/components/settings-defs.ts +28 -3
  211. package/src/modes/components/settings-selector.ts +178 -6
  212. package/src/modes/components/status-line/component.jj-cache.test.ts +229 -0
  213. package/src/modes/components/status-line/component.ts +123 -2
  214. package/src/modes/components/tool-execution.test.ts +63 -2
  215. package/src/modes/components/tool-execution.ts +11 -2
  216. package/src/modes/controllers/command-controller.ts +70 -22
  217. package/src/modes/controllers/event-controller.ts +111 -16
  218. package/src/modes/controllers/extension-ui-controller.test.ts +1 -0
  219. package/src/modes/controllers/extension-ui-controller.ts +20 -4
  220. package/src/modes/controllers/live-command-controller.ts +178 -0
  221. package/src/modes/controllers/mcp-command-controller.ts +80 -41
  222. package/src/modes/controllers/omfg-controller.ts +44 -40
  223. package/src/modes/controllers/selector-controller.ts +18 -10
  224. package/src/modes/controllers/session-focus-controller.ts +6 -1
  225. package/src/modes/interactive-mode.ts +160 -38
  226. package/src/modes/rpc/rpc-client.ts +94 -3
  227. package/src/modes/rpc/rpc-frame.ts +164 -4
  228. package/src/modes/rpc/rpc-messages.ts +127 -0
  229. package/src/modes/rpc/rpc-mode.ts +79 -7
  230. package/src/modes/rpc/rpc-types.ts +34 -2
  231. package/src/modes/runtime-init.ts +2 -0
  232. package/src/modes/setup-wizard/scenes/model.ts +5 -7
  233. package/src/modes/setup-wizard/scenes/providers.ts +3 -2
  234. package/src/modes/setup-wizard/scenes/sign-in.ts +8 -2
  235. package/src/modes/setup-wizard/scenes/theme.ts +13 -3
  236. package/src/modes/setup-wizard/scenes/types.ts +9 -1
  237. package/src/modes/setup-wizard/scenes/web-search.ts +23 -10
  238. package/src/modes/setup-wizard/wizard-overlay.ts +1 -1
  239. package/src/modes/theme/theme.ts +1 -1
  240. package/src/modes/types.ts +3 -1
  241. package/src/modes/utils/context-usage.ts +23 -7
  242. package/src/modes/utils/ui-helpers.ts +10 -5
  243. package/src/plan-mode/plan-files.ts +40 -0
  244. package/src/prompts/bench/cache-prefix-chunk.md +1 -0
  245. package/src/prompts/bench/cache-prefix.md +3 -0
  246. package/src/prompts/bench/cache-suffix.md +1 -0
  247. package/src/prompts/goals/guided-goal-system.md +24 -3
  248. package/src/prompts/system/custom-system-prompt.md +1 -1
  249. package/src/prompts/system/project-prompt.md +8 -1
  250. package/src/prompts/system/subagent-async-pending.md +6 -0
  251. package/src/prompts/system/system-prompt.md +1 -1
  252. package/src/prompts/system/workflow-notice.md +48 -40
  253. package/src/prompts/system/xdev-mount-notice.md +4 -0
  254. package/src/prompts/tools/task.md +12 -2
  255. package/src/prompts/tools/todo.md +3 -1
  256. package/src/registry/agent-lifecycle.ts +116 -46
  257. package/src/registry/agent-registry.ts +35 -8
  258. package/src/registry/persisted-agents.ts +26 -2
  259. package/src/sdk.ts +255 -139
  260. package/src/secrets/index.ts +127 -4
  261. package/src/secrets/obfuscator.ts +2328 -105
  262. package/src/session/acp-permission-gate.ts +165 -0
  263. package/src/session/agent-session-error-log.test.ts +1 -1
  264. package/src/session/agent-session-events.ts +66 -0
  265. package/src/session/agent-session-types.ts +334 -0
  266. package/src/session/agent-session.ts +1910 -12213
  267. package/src/session/artifacts.ts +6 -5
  268. package/src/session/async-job-delivery.ts +74 -0
  269. package/src/session/bash-runner.ts +326 -0
  270. package/src/session/blob-store.test.ts +56 -0
  271. package/src/session/blob-store.ts +18 -2
  272. package/src/session/checkpoint-entries.ts +81 -0
  273. package/src/session/eval-runner.ts +212 -0
  274. package/src/session/indexed-session-storage.ts +9 -2
  275. package/src/session/irc-bridge.ts +203 -0
  276. package/src/session/messages.ts +263 -1
  277. package/src/session/model-controls.ts +714 -0
  278. package/src/session/prewalk.ts +252 -0
  279. package/src/session/queued-messages.ts +93 -0
  280. package/src/session/redis-session-storage.ts +51 -11
  281. package/src/session/retry-fallback-chains.ts +455 -0
  282. package/src/session/role-models.ts +85 -0
  283. package/src/session/session-advisors.ts +1679 -0
  284. package/src/session/session-context.test.ts +224 -1
  285. package/src/session/session-context.ts +47 -5
  286. package/src/session/session-entries.ts +8 -0
  287. package/src/session/session-handoff.ts +308 -0
  288. package/src/session/session-history-format.ts +20 -9
  289. package/src/session/session-loader.ts +10 -51
  290. package/src/session/session-maintenance.ts +2983 -0
  291. package/src/session/session-manager.ts +390 -30
  292. package/src/session/session-memory.ts +222 -0
  293. package/src/session/session-provider-boundary.ts +307 -0
  294. package/src/session/session-stats.ts +293 -0
  295. package/src/session/session-tools.ts +943 -0
  296. package/src/session/session-workspace.ts +53 -0
  297. package/src/session/stream-guards.ts +417 -0
  298. package/src/session/todo-tracker.ts +380 -0
  299. package/src/session/ttsr-coordinator.ts +496 -0
  300. package/src/session/turn-recovery.ts +1629 -0
  301. package/src/slash-commands/builtin-registry.ts +92 -11
  302. package/src/slash-commands/helpers/usage-report.ts +25 -4
  303. package/src/system-prompt.ts +41 -17
  304. package/src/task/executor.ts +288 -70
  305. package/src/task/index.ts +148 -111
  306. package/src/task/output-manager.ts +25 -3
  307. package/src/task/persisted-revive.ts +4 -3
  308. package/src/task/structured-subagent.ts +4 -1
  309. package/src/task/types.ts +16 -0
  310. package/src/tools/approval.ts +55 -10
  311. package/src/tools/bash-interactive.ts +26 -0
  312. package/src/tools/bash-skill-urls.ts +28 -2
  313. package/src/tools/bash.ts +339 -123
  314. package/src/tools/browser/launch.ts +9 -1
  315. package/src/tools/browser/tab-supervisor.ts +8 -5
  316. package/src/tools/browser.ts +94 -47
  317. package/src/tools/hub/index.ts +1 -1
  318. package/src/tools/hub/jobs.ts +67 -8
  319. package/src/tools/image-gen.ts +20 -28
  320. package/src/tools/image-providers.ts +50 -0
  321. package/src/tools/index.ts +12 -0
  322. package/src/tools/path-utils.ts +49 -2
  323. package/src/tools/read.ts +156 -88
  324. package/src/tools/renderers.ts +7 -1
  325. package/src/tools/report-tool-issue.ts +79 -28
  326. package/src/tools/todo.ts +101 -11
  327. package/src/tools/vibe.ts +1 -2
  328. package/src/tools/write.ts +51 -1
  329. package/src/tools/xdev.ts +67 -3
  330. package/src/tts/streaming-player.ts +62 -4
  331. package/src/tts/vocalizer.ts +18 -1
  332. package/src/utils/jj.ts +125 -4
  333. package/src/utils/lang-from-path.ts +7 -0
  334. package/src/utils/title-generator.ts +137 -1
  335. package/src/vibe/runtime.ts +852 -72
  336. package/src/web/search/index.ts +3 -6
  337. package/src/web/search/provider.ts +31 -16
  338. package/src/web/search/providers/firecrawl.ts +46 -13
  339. package/src/web/search/providers/xai.ts +7 -1
  340. package/src/web/search/types.ts +8 -1
@@ -0,0 +1,234 @@
1
+ import { type Component, matchesKey, truncateToWidth, visibleWidth } from "@oh-my-pi/pi-tui";
2
+ import { sanitizeText } from "@oh-my-pi/pi-utils";
3
+ import { type ThemeColor, theme } from "../modes/theme/theme";
4
+
5
+ /** Distinct states of a realtime call connection. */
6
+ export type LivePhase = "connecting" | "listening" | "working" | "speaking" | "muted" | "error";
7
+
8
+ /** A transcribed turn in the realtime call. */
9
+ export interface LiveTranscript {
10
+ role: "user" | "assistant";
11
+ text: string;
12
+ }
13
+
14
+ /** Configuration callbacks for user interactions in the visualizer. */
15
+ export interface LiveVisualizerOptions {
16
+ onStop(): void;
17
+ onToggleMute(): void;
18
+ }
19
+
20
+ /** A compact, fixed-height terminal component for displaying a realtime call. */
21
+ export class LiveVisualizer implements Component {
22
+ readonly wantsKeyRelease = false;
23
+
24
+ readonly #options: LiveVisualizerOptions;
25
+
26
+ #phase: LivePhase = "connecting";
27
+ #inputLevel = 0;
28
+ #outputLevel = 0;
29
+ #transcript: LiveTranscript | undefined;
30
+ #frame = 0;
31
+
32
+ #cache:
33
+ | {
34
+ width: number;
35
+ phase: LivePhase;
36
+ inputLevel: number;
37
+ outputLevel: number;
38
+ transcriptRole: string | undefined;
39
+ transcriptText: string | undefined;
40
+ frame: number;
41
+ lines: readonly string[];
42
+ }
43
+ | undefined;
44
+
45
+ constructor(options: LiveVisualizerOptions) {
46
+ this.#options = options;
47
+ }
48
+
49
+ /** Updates the current call phase. */
50
+ setPhase(phase: LivePhase): void {
51
+ if (this.#phase !== phase) {
52
+ this.#phase = phase;
53
+ this.invalidate();
54
+ }
55
+ }
56
+
57
+ /** Updates the current input and output volume levels (0..1). */
58
+ setLevels(input: number, output: number): void {
59
+ if (this.#inputLevel !== input || this.#outputLevel !== output) {
60
+ this.#inputLevel = input;
61
+ this.#outputLevel = output;
62
+ this.invalidate();
63
+ }
64
+ }
65
+
66
+ /** Updates the latest transcript fragment. */
67
+ setTranscript(transcript: LiveTranscript | undefined): void {
68
+ if (this.#transcript?.role !== transcript?.role || this.#transcript?.text !== transcript?.text) {
69
+ this.#transcript = transcript ? { ...transcript } : undefined;
70
+ this.invalidate();
71
+ }
72
+ }
73
+
74
+ /** Updates the animation frame for spinners and waveforms. */
75
+ setFrame(frame: number): void {
76
+ if (this.#frame !== frame) {
77
+ this.#frame = frame;
78
+ this.invalidate();
79
+ }
80
+ }
81
+
82
+ /** Processes user keypresses. */
83
+ handleInput(data: string): void {
84
+ if (matchesKey(data, "escape")) {
85
+ this.#options.onStop();
86
+ } else if (matchesKey(data, "space")) {
87
+ this.#options.onToggleMute();
88
+ }
89
+ }
90
+
91
+ /** Clears the render cache. */
92
+ invalidate(): void {
93
+ this.#cache = undefined;
94
+ }
95
+
96
+ /** Renders the visualizer into a fixed array of rows at the given width. */
97
+ render(width: number): readonly string[] {
98
+ if (
99
+ this.#cache &&
100
+ this.#cache.width === width &&
101
+ this.#cache.phase === this.#phase &&
102
+ this.#cache.inputLevel === this.#inputLevel &&
103
+ this.#cache.outputLevel === this.#outputLevel &&
104
+ this.#cache.frame === this.#frame &&
105
+ this.#cache.transcriptRole === this.#transcript?.role &&
106
+ this.#cache.transcriptText === this.#transcript?.text
107
+ ) {
108
+ return this.#cache.lines;
109
+ }
110
+
111
+ const lines = this.#renderLines(width);
112
+
113
+ this.#cache = {
114
+ width,
115
+ phase: this.#phase,
116
+ inputLevel: this.#inputLevel,
117
+ outputLevel: this.#outputLevel,
118
+ frame: this.#frame,
119
+ transcriptRole: this.#transcript?.role,
120
+ transcriptText: this.#transcript?.text,
121
+ lines,
122
+ };
123
+
124
+ return lines;
125
+ }
126
+
127
+ #renderLines(maxWidth: number): readonly string[] {
128
+ const w = Math.max(2, Math.min(maxWidth, 120));
129
+ const innerW = w - 2;
130
+
131
+ const topBorder = theme.fg("border", `┌${"─".repeat(innerW)}${w > 1 ? "┐" : ""}`);
132
+
133
+ const maxWaveW = Math.min(40, Math.max(0, w - 4));
134
+ const sideW = Math.max(0, Math.floor((maxWaveW - 3) / 2));
135
+
136
+ const inWave = this.#generateWaveform(this.#inputLevel, sideW, true);
137
+ const outWave = this.#generateWaveform(this.#outputLevel, sideW, false);
138
+
139
+ const inColor: ThemeColor = this.#phase === "muted" ? "dim" : "success";
140
+ const outColor: ThemeColor = this.#phase === "error" ? "error" : "accent";
141
+
142
+ const waveContent = truncateToWidth(
143
+ theme.fg(inColor, inWave) + theme.fg("dim", " │ ") + theme.fg(outColor, outWave),
144
+ innerW,
145
+ );
146
+ const waveLen = visibleWidth(waveContent);
147
+
148
+ const wavePadL = Math.floor((innerW - waveLen) / 2);
149
+ const wavePadR = innerW - waveLen - wavePadL;
150
+ const row1 =
151
+ theme.fg("border", "│") +
152
+ " ".repeat(Math.max(0, wavePadL)) +
153
+ waveContent +
154
+ " ".repeat(Math.max(0, wavePadR)) +
155
+ (w > 1 ? theme.fg("border", "│") : "");
156
+
157
+ const phaseColors: Record<LivePhase, ThemeColor> = {
158
+ connecting: "dim",
159
+ listening: "success",
160
+ working: "warning",
161
+ speaking: "accent",
162
+ muted: "dim",
163
+ error: "error",
164
+ };
165
+
166
+ const spinners = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
167
+ let phaseIcon: string;
168
+ if (this.#phase === "working") {
169
+ phaseIcon = spinners[this.#frame % spinners.length];
170
+ } else {
171
+ const staticIcons: Record<LivePhase, string> = {
172
+ connecting: "○",
173
+ listening: "●",
174
+ working: "○",
175
+ speaking: "»",
176
+ muted: "×",
177
+ error: "!",
178
+ };
179
+ phaseIcon = staticIcons[this.#phase];
180
+ }
181
+
182
+ const phaseText = truncateToWidth(` ${phaseIcon} ${this.#phase.toUpperCase()} `, innerW);
183
+ const coloredPhase = theme.fg(phaseColors[this.#phase], phaseText);
184
+
185
+ const pLen = visibleWidth(coloredPhase);
186
+ const pPadL = Math.floor((innerW - pLen) / 2);
187
+ const pPadR = innerW - pLen - pPadL;
188
+ const row2 =
189
+ theme.fg("border", "│") +
190
+ " ".repeat(Math.max(0, pPadL)) +
191
+ coloredPhase +
192
+ " ".repeat(Math.max(0, pPadR)) +
193
+ (w > 1 ? theme.fg("border", "│") : "");
194
+
195
+ const row3 = theme.fg("border", "│") + " ".repeat(Math.max(0, innerW)) + (w > 1 ? theme.fg("border", "│") : "");
196
+
197
+ let transcriptText = "";
198
+ if (this.#transcript) {
199
+ const cleanText = sanitizeText(this.#transcript.text).replace(/[\r\n\t]+/g, " ");
200
+ const roleStr = this.#transcript.role === "user" ? "User: " : "Assistant: ";
201
+ transcriptText = theme.fg("dim", roleStr + cleanText);
202
+ }
203
+ const row4Content = truncateToWidth(` ${transcriptText}`, Math.max(0, innerW - 2)) || "";
204
+ const r4Pad = Math.max(0, innerW - visibleWidth(row4Content));
205
+ const row4 = theme.fg("border", "│") + row4Content + " ".repeat(r4Pad) + (w > 1 ? theme.fg("border", "│") : "");
206
+
207
+ const footerContent = truncateToWidth(" space mute · esc end ", innerW);
208
+ const dashCount = Math.max(0, innerW - visibleWidth(footerContent));
209
+ let bottomBorder: string;
210
+ if (innerW >= visibleWidth(footerContent) + 2) {
211
+ bottomBorder =
212
+ theme.fg("border", "└─") +
213
+ theme.fg("dim", footerContent) +
214
+ theme.fg("border", "─".repeat(dashCount - 2) + (w > 1 ? "┘" : ""));
215
+ } else {
216
+ bottomBorder = theme.fg("border", `└${"─".repeat(Math.max(0, innerW))}${w > 1 ? "┘" : ""}`);
217
+ }
218
+
219
+ return [topBorder, row1, row2, row3, row4, bottomBorder];
220
+ }
221
+
222
+ #generateWaveform(level: number, width: number, reverse: boolean): string {
223
+ const chars = [" ", "▂", "▃", "▄", "▅", "▆", "▇", "█"];
224
+ let out = "";
225
+ for (let i = 0; i < width; i++) {
226
+ const dist = reverse ? width - 1 - i : i;
227
+ const decay = Math.max(0, 1 - (dist / width) * 1.5);
228
+ const val = level * decay * (0.5 + 0.5 * Math.sin(this.#frame * 0.5 + dist * 0.8));
229
+ const charIdx = Math.max(0, Math.min(chars.length - 1, Math.floor(val * chars.length)));
230
+ out += chars[charIdx];
231
+ }
232
+ return out;
233
+ }
234
+ }
package/src/lsp/client.ts CHANGED
@@ -364,7 +364,15 @@ async function startMessageReader(client: LspClient): Promise<void> {
364
364
  if (pending) {
365
365
  client.pendingRequests.delete(message.id);
366
366
  if ("error" in message && message.error) {
367
- pending.reject(new Error(`LSP error: ${message.error.message}`));
367
+ // Include the JSON-RPC error code: `isMethodNotFoundError` matches
368
+ // `-32601` by substring, so method-not-found is recognized even when
369
+ // the server's message text is nonstandard (e.g. "Unknown request").
370
+ const code = message.error.code;
371
+ pending.reject(
372
+ new Error(
373
+ `LSP error${typeof code === "number" ? ` ${code}` : ""}: ${message.error.message}`,
374
+ ),
375
+ );
368
376
  } else {
369
377
  pending.resolve(message.result);
370
378
  }
@@ -846,6 +854,29 @@ export async function getOrCreateClient(
846
854
  return clientPromise;
847
855
  }
848
856
 
857
+ /** Return an active or already-starting client without starting a language server. */
858
+ export async function getActiveOrPendingClient(
859
+ config: ServerConfig,
860
+ cwd: string,
861
+ signal?: AbortSignal,
862
+ ): Promise<LspClient | undefined> {
863
+ throwIfAborted(signal);
864
+ const client = clients.get(`${config.command}:${cwd}`);
865
+ if (client) {
866
+ client.lastActivity = Date.now();
867
+ return client;
868
+ }
869
+
870
+ const pending = clientLocks.get(`${config.command}:${cwd}`);
871
+ if (!pending) return undefined;
872
+ try {
873
+ return await untilAborted(signal, pending);
874
+ } catch {
875
+ throwIfAborted(signal);
876
+ return undefined;
877
+ }
878
+ }
879
+
849
880
  /**
850
881
  * Ensure a file is opened in the LSP client.
851
882
  * Sends didOpen notification if the file is not already tracked.
@@ -1158,9 +1189,19 @@ async function waitForExit(client: LspClient, timeoutMs: number): Promise<boolea
1158
1189
  }
1159
1190
 
1160
1191
  /**
1161
- * Shutdown a specific client instance using the LSP shutdown/exit handshake.
1192
+ * Tear down a specific client instance using the LSP shutdown/exit handshake.
1193
+ *
1194
+ * Removes the client from the registry by identity first (never evicting a
1195
+ * newer client already republished under the same key), then performs a bounded
1196
+ * graceful shutdown, force-killing and awaiting confirmed process exit.
1197
+ *
1198
+ * @returns `true` once the process is confirmed exited, `false` if it outlived
1199
+ * the shutdown budget — callers reporting a restart must treat `false` as a
1200
+ * failed teardown, not a completed restart.
1162
1201
  */
1163
- async function shutdownClientInstance(client: LspClient): Promise<void> {
1202
+ export async function shutdownClientInstance(client: LspClient): Promise<boolean> {
1203
+ if (clients.get(client.name) === client) clients.delete(client.name);
1204
+
1164
1205
  const err = new Error("LSP client shutdown");
1165
1206
  for (const pending of Array.from(client.pendingRequests.values())) {
1166
1207
  pending.reject(err);
@@ -1173,21 +1214,23 @@ async function shutdownClientInstance(client: LspClient): Promise<void> {
1173
1214
  );
1174
1215
  if (shutdownCompleted) {
1175
1216
  await sendNotification(client, "exit", undefined).catch(() => {});
1176
- if (await waitForExit(client, EXIT_TIMEOUT_MS)) return;
1217
+ if (await waitForExit(client, EXIT_TIMEOUT_MS)) return true;
1177
1218
  }
1178
1219
 
1179
1220
  client.proc.kill();
1180
- await waitForExit(client, EXIT_TIMEOUT_MS);
1221
+ return await waitForExit(client, EXIT_TIMEOUT_MS);
1181
1222
  }
1182
1223
 
1183
1224
  /**
1184
1225
  * Shutdown a specific client by key.
1226
+ *
1227
+ * @returns `true` when the client is gone (already absent or confirmed exited),
1228
+ * `false` if a live process outlived the shutdown budget.
1185
1229
  */
1186
- export async function shutdownClient(key: string): Promise<void> {
1230
+ export async function shutdownClient(key: string): Promise<boolean> {
1187
1231
  const client = clients.get(key);
1188
- if (!client) return;
1189
- clients.delete(key);
1190
- await shutdownClientInstance(client);
1232
+ if (!client) return true;
1233
+ return await shutdownClientInstance(client);
1191
1234
  }
1192
1235
 
1193
1236
  // =============================================================================
@@ -2,7 +2,7 @@
2
2
  * Biome CLI-based linter client.
3
3
  * Uses Biome's CLI with JSON output instead of LSP (which has stale diagnostics issues).
4
4
  */
5
- import path from "node:path";
5
+ import * as path from "node:path";
6
6
  import { logger } from "@oh-my-pi/pi-utils";
7
7
  import type { Diagnostic, DiagnosticSeverity, LinterClient, ServerConfig } from "../../lsp/types";
8
8
 
@@ -141,14 +141,13 @@ export class BiomeClient implements LinterClient {
141
141
  ) {}
142
142
 
143
143
  async format(filePath: string, content: string): Promise<string> {
144
- // Write content to file first
144
+ // Keep the standalone LinterClient contract: callers supply the content to
145
+ // format, regardless of what is currently on disk.
145
146
  await Bun.write(filePath, content);
146
147
 
147
- // Run biome format --write
148
148
  const result = await runBiome(["format", "--write", filePath], this.cwd, this.config.resolvedCommand);
149
149
 
150
150
  if (result.success) {
151
- // Read back formatted content
152
151
  return await Bun.file(filePath).text();
153
152
  }
154
153
 
package/src/lsp/index.ts CHANGED
@@ -20,6 +20,7 @@ import {
20
20
  ensureFileOpen,
21
21
  FileChangeType,
22
22
  getActiveClients,
23
+ getActiveOrPendingClient,
23
24
  getOrCreateClient,
24
25
  type LspServerStatus,
25
26
  notifySaved,
@@ -28,6 +29,7 @@ import {
28
29
  sendNotification,
29
30
  sendRequest,
30
31
  setIdleTimeout,
32
+ shutdownClientInstance,
31
33
  supportsDocumentDiagnostics,
32
34
  syncContent,
33
35
  WARMUP_TIMEOUT_MS,
@@ -211,6 +213,7 @@ async function syncFileContent(
211
213
  cwd: string,
212
214
  servers: Array<[string, ServerConfig]>,
213
215
  signal?: AbortSignal,
216
+ createMissing = true,
214
217
  ): Promise<void> {
215
218
  throwIfAborted(signal);
216
219
  await Promise.allSettled(
@@ -219,11 +222,15 @@ async function syncFileContent(
219
222
  if (serverConfig.createClient) {
220
223
  return;
221
224
  }
222
- const client = await getOrCreateClient(serverConfig, cwd, undefined, signal);
225
+ const client = createMissing
226
+ ? await getOrCreateClient(serverConfig, cwd, undefined, signal)
227
+ : await getActiveOrPendingClient(serverConfig, cwd, signal);
228
+ if (!client) return;
223
229
  throwIfAborted(signal);
224
230
  await syncContent(client, absolutePath, content, signal);
225
231
  }),
226
232
  );
233
+ throwIfAborted(signal);
227
234
  }
228
235
 
229
236
  /**
@@ -239,6 +246,7 @@ async function notifyFileSaved(
239
246
  cwd: string,
240
247
  servers: Array<[string, ServerConfig]>,
241
248
  signal?: AbortSignal,
249
+ createMissing = true,
242
250
  ): Promise<void> {
243
251
  throwIfAborted(signal);
244
252
  await Promise.allSettled(
@@ -247,10 +255,14 @@ async function notifyFileSaved(
247
255
  if (serverConfig.createClient) {
248
256
  return;
249
257
  }
250
- const client = await getOrCreateClient(serverConfig, cwd, undefined, signal);
258
+ const client = createMissing
259
+ ? await getOrCreateClient(serverConfig, cwd, undefined, signal)
260
+ : await getActiveOrPendingClient(serverConfig, cwd, signal);
261
+ if (!client) return;
251
262
  await notifySaved(client, absolutePath, signal);
252
263
  }),
253
264
  );
265
+ throwIfAborted(signal);
254
266
  }
255
267
 
256
268
  // Cache config per cwd to avoid repeated file I/O
@@ -496,12 +508,18 @@ function isMethodNotFoundError(err: unknown): boolean {
496
508
  }
497
509
 
498
510
  async function reloadServer(client: LspClient, serverName: string, signal?: AbortSignal): Promise<string> {
499
- // rust-analyzer exposes a real reload request.
511
+ throwIfAborted(signal);
512
+ // rust-analyzer exposes a real reload request. Every other server rejects it
513
+ // with method-not-found — that alone justifies the generic fallback. A caller
514
+ // cancel or tool timeout must propagate, never be mistaken for an unsupported
515
+ // method and swallowed into a bogus "Restarted" (issue #6369).
500
516
  try {
501
517
  await sendRequest(client, "rust-analyzer/reloadWorkspace", null, signal);
502
518
  return `Reloaded ${serverName}`;
503
- } catch {
504
- // Method not supported — fall through.
519
+ } catch (err) {
520
+ throwIfAborted(signal);
521
+ if (!isMethodNotFoundError(err)) throw err;
522
+ // Method not supported — fall through to the generic reload.
505
523
  }
506
524
  // workspace/didChangeConfiguration is a notification per spec; sending it
507
525
  // as a request hangs until the tool deadline on servers that route it to
@@ -510,7 +528,15 @@ async function reloadServer(client: LspClient, serverName: string, signal?: Abor
510
528
  await sendNotification(client, "workspace/didChangeConfiguration", { settings: {} }, signal);
511
529
  return `Reloaded ${serverName}`;
512
530
  } catch {
513
- client.proc.kill();
531
+ throwIfAborted(signal);
532
+ // The reload notification could not be delivered — the connection is
533
+ // wedged or the process already died. Tear the client down (removing it
534
+ // from the registry by identity and awaiting confirmed process exit) so
535
+ // the next request cold-starts a fresh client. A kill that never confirms
536
+ // exit is not a restart: surface the teardown failure truthfully.
537
+ if (!(await shutdownClientInstance(client))) {
538
+ throw new Error(`Failed to restart ${serverName}: server process did not exit after kill`);
539
+ }
514
540
  return `Restarted ${serverName}`;
515
541
  }
516
542
  }
@@ -1333,7 +1359,8 @@ async function runLspWritethrough(
1333
1359
  // Capture diagnostic versions BEFORE syncing to detect stale diagnostics
1334
1360
  // Bound client creation by the writethrough budget: a hung/broken server
1335
1361
  // must not add its full init wait (30s default) to every edit.
1336
- const minVersions = enableDiagnostics ? await captureDiagnosticVersions(cwd, servers, 5_000, signal) : undefined;
1362
+ const minVersionsPromise = enableDiagnostics ? captureDiagnosticVersions(cwd, servers, 5_000, signal) : undefined;
1363
+ let minVersions = useCustomFormatter ? undefined : await minVersionsPromise;
1337
1364
  let expectedDocumentVersions: ServerVersionMap | undefined;
1338
1365
 
1339
1366
  let formatter: FileFormatResult | undefined;
@@ -1353,13 +1380,19 @@ async function runLspWritethrough(
1353
1380
  operationSignal = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
1354
1381
  await untilAborted(operationSignal, async () => {
1355
1382
  if (useCustomFormatter) {
1356
- // Custom linters (e.g. Biome CLI) require on-disk input.
1383
+ // Custom linters operate on on-disk input; the shared pre-write also
1384
+ // supports implementations that inspect the file before formatting.
1357
1385
  await writeContent(content);
1358
- finalContent = await formatContent(dst, content, cwd, customLinterServers, operationSignal);
1386
+ const [formattedContent, capturedVersions] = await Promise.all([
1387
+ formatContent(dst, content, cwd, customLinterServers, operationSignal),
1388
+ minVersionsPromise,
1389
+ ]);
1390
+ finalContent = formattedContent;
1391
+ minVersions = capturedVersions;
1359
1392
  formatter = finalContent !== content ? FileFormatResult.FORMATTED : FileFormatResult.UNCHANGED;
1360
1393
  await writeContent(finalContent);
1361
1394
  await notifyWriteCommitted(operationSignal);
1362
- await syncFileContent(dst, finalContent, cwd, lspServers, operationSignal);
1395
+ await syncFileContent(dst, finalContent, cwd, lspServers, operationSignal, enableDiagnostics);
1363
1396
  } else {
1364
1397
  // 1. Sync original content to LSP servers
1365
1398
  await syncFileContent(dst, content, cwd, lspServers, operationSignal);
@@ -1385,7 +1418,7 @@ async function runLspWritethrough(
1385
1418
  }
1386
1419
 
1387
1420
  // 5. Notify saved to LSP servers
1388
- await notifyFileSaved(dst, cwd, lspServers, operationSignal);
1421
+ await notifyFileSaved(dst, cwd, lspServers, operationSignal, !useCustomFormatter || enableDiagnostics);
1389
1422
  });
1390
1423
  synced = true;
1391
1424
  } catch {
package/src/main.ts CHANGED
@@ -134,6 +134,7 @@ async function checkForNewVersion(currentVersion: string): Promise<string | unde
134
134
  // embedders need project-level opt-outs for reminder/prelude prompt injection.
135
135
  const HOST_DEFAULTED_SETTING_PATHS: SettingPath[] = [
136
136
  "task.isolation.mode",
137
+ "task.isolation.apply",
137
138
  "task.isolation.merge",
138
139
  "task.isolation.commits",
139
140
  "task.eager",
@@ -390,7 +391,9 @@ export function createAcpSessionFactory(args: AcpSessionFactoryOptions): AcpSess
390
391
  authStorage: args.authStorage,
391
392
  modelRegistry: args.modelRegistry,
392
393
  agentId,
393
- hasUI: false,
394
+ // Preserve reserve-policy confirmation until ACP capabilities are known
395
+ // without enabling AskTool or other UI-only session behavior.
396
+ deferUsageReserveConfirmation: true,
394
397
  enableMCP: false,
395
398
  titleSystemPrompt,
396
399
  });
@@ -833,6 +836,11 @@ export async function buildSessionOptions(
833
836
  cwd: parsed.cwd ?? getProjectDir(),
834
837
  autoApprove: parsed.autoApprove ?? false,
835
838
  };
839
+ const cliDirs = parsed.addDir ?? [];
840
+ const settingsDirs = activeSettings.get("workspace.additionalDirectories");
841
+ if (cliDirs.length > 0 || settingsDirs.length > 0) {
842
+ options.additionalDirectories = [...new Set([...cliDirs, ...settingsDirs])];
843
+ }
836
844
  if (parsed.maxTime !== undefined) {
837
845
  options.deadline = Date.now() + parsed.maxTime * 1000;
838
846
  }
@@ -0,0 +1,43 @@
1
+ import { afterEach, beforeEach, describe, expect, it } from "bun:test";
2
+ import * as fs from "node:fs/promises";
3
+ import * as os from "node:os";
4
+ import * as path from "node:path";
5
+ import { addMCPServer, readDisabledServers, readMCPConfigFile, setServerDisabled } from "./config-writer";
6
+
7
+ describe("config-writer concurrent mutations", () => {
8
+ let dir: string;
9
+ let filePath: string;
10
+
11
+ beforeEach(async () => {
12
+ dir = await fs.mkdtemp(path.join(os.tmpdir(), "omp-mcp-config-"));
13
+ filePath = path.join(dir, "mcp.json");
14
+ });
15
+
16
+ afterEach(async () => {
17
+ await fs.rm(dir, { recursive: true, force: true });
18
+ });
19
+
20
+ it("preserves both servers when two adds race the same file", async () => {
21
+ await Promise.all([
22
+ addMCPServer(filePath, "alpha", { type: "stdio", command: "a" }),
23
+ addMCPServer(filePath, "bravo", { type: "stdio", command: "b" }),
24
+ ]);
25
+
26
+ const config = await readMCPConfigFile(filePath);
27
+ expect(Object.keys(config.mcpServers ?? {}).sort()).toEqual(["alpha", "bravo"]);
28
+ });
29
+
30
+ it("preserves both denylist edits when disable calls race", async () => {
31
+ await Promise.all([setServerDisabled(filePath, "alpha", true), setServerDisabled(filePath, "bravo", true)]);
32
+
33
+ expect((await readDisabledServers(filePath)).sort()).toEqual(["alpha", "bravo"]);
34
+ });
35
+
36
+ it("writes into a directory that does not exist yet", async () => {
37
+ const nestedPath = path.join(dir, "nested", "deep", "mcp.json");
38
+ await addMCPServer(nestedPath, "alpha", { type: "stdio", command: "a" });
39
+
40
+ const config = await readMCPConfigFile(nestedPath);
41
+ expect(Object.keys(config.mcpServers ?? {})).toEqual(["alpha"]);
42
+ });
43
+ });