@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
@@ -55,7 +55,7 @@ import { reset as resetCapabilities } from "../capability";
55
55
  import type { CollabGuestLink } from "../collab/guest";
56
56
  import type { CollabHost } from "../collab/host";
57
57
  import { KeybindingsManager } from "../config/keybindings";
58
- import type { ResolvedModelRoleValue } from "../config/model-resolver";
58
+ import { formatModelString, type ResolvedModelRoleValue } from "../config/model-resolver";
59
59
  import { applyProviderGlobalsFromSettings } from "../config/provider-globals";
60
60
  import {
61
61
  isSettingsInitialized,
@@ -130,8 +130,19 @@ import type { EventBus } from "../utils/event-bus";
130
130
  import { getEditorCommand, openInEditor } from "../utils/external-editor";
131
131
  import { getSessionAccentAnsi, getSessionAccentHex } from "../utils/session-color";
132
132
  import { messageHasDisplayableThinking } from "../utils/thinking-display";
133
- import { popTerminalTitle, pushTerminalTitle, setSessionTerminalTitle } from "../utils/title-generator";
134
- import { aggregateVibeWorkerTokensPerSecond, VibeSessionRegistry } from "../vibe/runtime";
133
+ import {
134
+ disposeTerminalTitleState,
135
+ popTerminalTitle,
136
+ pushTerminalTitle,
137
+ setSessionTerminalTitle,
138
+ setTerminalTitleStateEnabled,
139
+ } from "../utils/title-generator";
140
+ import {
141
+ aggregateVibeWorkerTokensPerSecond,
142
+ type VibeOwnerScope,
143
+ type VibeParentSession,
144
+ VibeSessionRegistry,
145
+ } from "../vibe/runtime";
135
146
  import type { AssistantMessageComponent } from "./components/assistant-message";
136
147
  import type { BashExecutionComponent } from "./components/bash-execution";
137
148
  import { ChatBlock, type ChatBlockHost } from "./components/chat-block";
@@ -142,7 +153,7 @@ import type { EvalExecutionComponent } from "./components/eval-execution";
142
153
  import type { HookEditorComponent } from "./components/hook-editor";
143
154
  import type { HookInputComponent } from "./components/hook-input";
144
155
  import type { HookSelectorComponent, HookSelectorSlider } from "./components/hook-selector";
145
- import { PlanReviewOverlay } from "./components/plan-review-overlay";
156
+ import { type PlanReviewAnnotationState, PlanReviewOverlay } from "./components/plan-review-overlay";
146
157
  import { StatusLineComponent } from "./components/status-line";
147
158
  import type { ToolExecutionHandle } from "./components/tool-execution";
148
159
  import { TranscriptContainer } from "./components/transcript-container";
@@ -152,6 +163,7 @@ import { CommandController } from "./controllers/command-controller";
152
163
  import { EventController } from "./controllers/event-controller";
153
164
  import { ExtensionUiController } from "./controllers/extension-ui-controller";
154
165
  import { InputController } from "./controllers/input-controller";
166
+ import { LiveCommandController } from "./controllers/live-command-controller";
155
167
  import { MCPCommandController } from "./controllers/mcp-command-controller";
156
168
  import { OmfgController } from "./controllers/omfg-controller";
157
169
  import { SelectorController } from "./controllers/selector-controller";
@@ -543,6 +555,8 @@ export class InteractiveMode implements InteractiveModeContext {
543
555
  #planModePreviousTools: string[] | undefined;
544
556
  #goalModePreviousTools: string[] | undefined;
545
557
  #vibeModePreviousTools: string[] | undefined;
558
+ #vibeModeOwnerScope: VibeOwnerScope | undefined;
559
+ #vibeScopeSuspendedForSwitch = false;
546
560
  #goalContinuationTimer: NodeJS.Timeout | undefined;
547
561
  #goalTurnHadToolCalls = false;
548
562
  #goalContinuationTurnInFlight = false;
@@ -555,6 +569,10 @@ export class InteractiveMode implements InteractiveModeContext {
555
569
  #planReviewOverlay: PlanReviewOverlay | undefined;
556
570
  #planReviewOverlayHandle: OverlayHandle | undefined;
557
571
  #planReviewCancel: (() => void) | undefined;
572
+ /** Serializable review annotations keyed by the resolved plan file path. */
573
+ #planReviewAnnotationState = new Map<string, PlanReviewAnnotationState>();
574
+ /** Annotation state held until the associated queued refinement actually starts. */
575
+ #planReviewAnnotationStateBySubmission = new WeakMap<SubmittedUserInput, string>();
558
576
  readonly lspServers: LspStartupServerInfo[] | undefined = undefined;
559
577
  mcpManager?: MCPManager;
560
578
  readonly #toolUiContextSetter: (uiContext: ExtensionUIContext, hasUI: boolean) => void;
@@ -564,6 +582,7 @@ export class InteractiveMode implements InteractiveModeContext {
564
582
  readonly #omfgController: OmfgController;
565
583
  readonly #commandController: CommandController;
566
584
  readonly #todoCommandController: TodoCommandController;
585
+ readonly #liveCommandController: LiveCommandController;
567
586
  readonly #eventController: EventController;
568
587
  get eventController(): EventController {
569
588
  return this.#eventController;
@@ -656,6 +675,9 @@ export class InteractiveMode implements InteractiveModeContext {
656
675
  this.#toolUiContextSetter = setToolUIContext;
657
676
  this.lspServers = lspServers;
658
677
  this.mcpManager = mcpManager;
678
+ this.mcpManager?.setAuthHandler((serverName, challenge) =>
679
+ new MCPCommandController(this).handleMCPAuthChallenge(serverName, challenge),
680
+ );
659
681
  this.#eventBus = eventBus;
660
682
  if (eventBus) {
661
683
  this.#eventBusUnsubscribers.push(
@@ -770,6 +792,7 @@ export class InteractiveMode implements InteractiveModeContext {
770
792
  this.#eventController = new EventController(this);
771
793
  this.#commandController = new CommandController(this);
772
794
  this.#todoCommandController = new TodoCommandController(this);
795
+ this.#liveCommandController = new LiveCommandController(this);
773
796
  this.#selectorController = new SelectorController(this);
774
797
  this.#focusController = new SessionFocusController(this);
775
798
  this.#inputController = new InputController(this);
@@ -973,6 +996,7 @@ export class InteractiveMode implements InteractiveModeContext {
973
996
  // the initial welcome frame does not append over the previous run's scrollback.
974
997
  this.ui.start({ clearScrollback: options.clearInitialTerminalHistory === true });
975
998
  pushTerminalTitle();
999
+ setTerminalTitleStateEnabled(this.settings.get("tui.titleState"));
976
1000
  setSessionTerminalTitle(this.sessionManager.getSessionName(), this.sessionManager.getCwd());
977
1001
  this.updateEditorBorderColor();
978
1002
  // Single side-effect point for title changes: every setSessionName caller
@@ -994,6 +1018,10 @@ export class InteractiveMode implements InteractiveModeContext {
994
1018
  await this.initHooksAndCustomTools();
995
1019
 
996
1020
  // Restore mode from session (e.g. plan mode on resume)
1021
+ this.session.setSessionBeforeSwitchReconciler?.(async () => {
1022
+ await this.#liveCommandController.stop();
1023
+ await this.#quiesceVibeForSessionSwitch();
1024
+ });
997
1025
  this.session.setSessionSwitchReconciler?.(() => this.#reconcileModeFromSession({ preserveActiveGoal: true }));
998
1026
  await this.#reconcileModeFromSession();
999
1027
 
@@ -1563,6 +1591,11 @@ export class InteractiveMode implements InteractiveModeContext {
1563
1591
  return false;
1564
1592
  }
1565
1593
  input.started = true;
1594
+ const annotationStateKey = this.#planReviewAnnotationStateBySubmission.get(input);
1595
+ if (annotationStateKey) {
1596
+ this.#planReviewAnnotationStateBySubmission.delete(input);
1597
+ this.#planReviewAnnotationState.delete(annotationStateKey);
1598
+ }
1566
1599
  return true;
1567
1600
  }
1568
1601
 
@@ -1753,6 +1786,8 @@ export class InteractiveMode implements InteractiveModeContext {
1753
1786
  return theme.fg("accent", `${prefix}${checkbox.unchecked} ${todo.content}`) + marker;
1754
1787
  case "abandoned":
1755
1788
  return theme.fg("error", `${prefix}${checkbox.unchecked} ${chalk.strikethrough(todo.content)}`) + marker;
1789
+ case "blocked":
1790
+ return theme.fg("warning", `${prefix}${checkbox.unchecked} ${todo.content} (blocked)`) + marker;
1756
1791
  default:
1757
1792
  if (matched) return theme.fg("accent", `${prefix}${checkbox.unchecked} ${todo.content}`) + marker;
1758
1793
  return theme.fg("dim", `${prefix}${checkbox.unchecked} ${todo.content}`) + marker;
@@ -1772,12 +1807,15 @@ export class InteractiveMode implements InteractiveModeContext {
1772
1807
  }
1773
1808
 
1774
1809
  /**
1775
- * Auto-complete any pending/in_progress todo whose content matches a
1776
- * subagent that has finished successfully. Fires on every observer
1810
+ * Auto-complete any open todo (pending/in_progress/blocked) whose content
1811
+ * matches a subagent that has finished successfully. Fires on every observer
1777
1812
  * `onChange` so the visual state stays in sync with subagent lifecycle
1778
- * without requiring the agent to issue a follow-up `todo`. Failed
1779
- * and aborted subagents are intentionally NOT auto-completed those
1780
- * stay open so the user (or the next agent turn) can decide what to do.
1813
+ * without requiring the agent to issue a follow-up `todo`. A todo `block`ed
1814
+ * while waiting on a detached subagent is included: that subagent completing
1815
+ * is exactly the unblock signal, and blocked todos are excluded from the stop
1816
+ * reminder, so leaving it blocked would strand it silently. Failed and aborted
1817
+ * subagents are intentionally NOT auto-completed — those stay open so the user
1818
+ * (or the next agent turn) can decide what to do.
1781
1819
  *
1782
1820
  * Idempotent: only flips open tasks, never re-touches completed ones.
1783
1821
  */
@@ -1796,10 +1834,14 @@ export class InteractiveMode implements InteractiveModeContext {
1796
1834
  const next: TodoPhase[] = this.todoPhases.map(phase => ({
1797
1835
  name: phase.name,
1798
1836
  tasks: phase.tasks.map(task => {
1799
- if (task.status !== "pending" && task.status !== "in_progress") return task;
1837
+ if (task.status !== "pending" && task.status !== "in_progress" && task.status !== "blocked") {
1838
+ return task;
1839
+ }
1800
1840
  if (!todoMatchesAnyDescription(task.content, completedDescs)) return task;
1801
1841
  mutated = true;
1802
- return { ...task, status: "completed" as const };
1842
+ // Drop any blocker note along with the blocked status the wait the
1843
+ // note described is over.
1844
+ return { content: task.content, status: "completed" as const };
1803
1845
  }),
1804
1846
  }));
1805
1847
  if (!mutated) return;
@@ -2055,6 +2097,29 @@ export class InteractiveMode implements InteractiveModeContext {
2055
2097
  this.ui.requestRender();
2056
2098
  }
2057
2099
 
2100
+ #vibeParentSession(): VibeParentSession {
2101
+ return {
2102
+ getAgentId: () => this.session.getAgentId() ?? null,
2103
+ getSessionId: () => this.sessionManager.getSessionId(),
2104
+ getSessionFile: () => this.sessionManager.getSessionFile() ?? null,
2105
+ sessionManager: this.sessionManager,
2106
+ asyncJobManager: this.session.asyncJobManager,
2107
+ settings: this.session.settings,
2108
+ // Resolve restored/switched-to workers against this session's active model
2109
+ // (same as the spawn-path ToolSession), not the settings default. This is
2110
+ // the primary fallback in resolveAgentModelPatterns, so the `good` worker's
2111
+ // pi/task inheritance tracks the reopened session's model.
2112
+ getActiveModelString: () => (this.session.model ? formatModelString(this.session.model) : undefined),
2113
+ };
2114
+ }
2115
+
2116
+ async #quiesceVibeForSessionSwitch(): Promise<void> {
2117
+ const ownerScope = this.#vibeModeOwnerScope;
2118
+ if (!this.vibeModeEnabled || !ownerScope) return;
2119
+ await VibeSessionRegistry.global().suspendScope(ownerScope, this.session.asyncJobManager);
2120
+ this.#vibeScopeSuspendedForSwitch = true;
2121
+ }
2122
+
2058
2123
  #updateGoalModeStatus(): void {
2059
2124
  const status =
2060
2125
  this.goalModeEnabled || this.goalModePaused
@@ -2238,7 +2303,10 @@ export class InteractiveMode implements InteractiveModeContext {
2238
2303
  }
2239
2304
  }
2240
2305
 
2241
- async #clearTransientModeState(): Promise<void> {
2306
+ async #clearTransientModeState(options?: {
2307
+ preserveVibe?: boolean;
2308
+ vibeScopeAlreadySuspended?: boolean;
2309
+ }): Promise<void> {
2242
2310
  if (this.planModeEnabled || this.planModePaused) {
2243
2311
  this.session.setPlanModeState(undefined);
2244
2312
  try {
@@ -2274,23 +2342,40 @@ export class InteractiveMode implements InteractiveModeContext {
2274
2342
  this.#updateGoalModeStatus();
2275
2343
  }
2276
2344
 
2277
- if (this.vibeModeEnabled) {
2278
- await this.session.deactivateVibeTools(this.#vibeModePreviousTools ?? []);
2345
+ if (this.vibeModeEnabled && !options?.preserveVibe) {
2346
+ const ownerScope = this.#vibeModeOwnerScope;
2347
+ // This runs only from #reconcileModeFromSession, i.e. after switchSession
2348
+ // already loaded and restored the target session's active tools. The
2349
+ // #vibeModePreviousTools snapshot belongs to the SOURCE session, so
2350
+ // applying it here would clobber the target's tools — strip only the
2351
+ // transient vibe tools and keep the target's active set intact.
2352
+ await this.session.removeVibeToolsPreservingActive();
2279
2353
  this.session.setVibeModeState(undefined);
2280
2354
  this.vibeModeEnabled = false;
2281
2355
  this.#vibeModePreviousTools = undefined;
2282
- await VibeSessionRegistry.global().killAll(
2283
- this.session.getAgentId() ?? MAIN_AGENT_ID,
2284
- this.session.asyncJobManager,
2285
- );
2356
+ this.#vibeModeOwnerScope = undefined;
2357
+ if (ownerScope && !options?.vibeScopeAlreadySuspended) {
2358
+ await VibeSessionRegistry.global().suspendScope(ownerScope, this.session.asyncJobManager);
2359
+ }
2286
2360
  this.#updateVibeModeStatus();
2287
2361
  }
2288
2362
  }
2289
2363
 
2290
2364
  /** Reconcile mode state from session entries on resume/switch. */
2291
2365
  async #reconcileModeFromSession(options?: { preserveActiveGoal?: boolean }): Promise<void> {
2292
- await this.#clearTransientModeState();
2366
+ const vibeScopeAlreadySuspended = this.#vibeScopeSuspendedForSwitch;
2367
+ this.#vibeScopeSuspendedForSwitch = false;
2293
2368
  const sessionContext = this.sessionManager.buildSessionContext();
2369
+ const vibeSession = this.#vibeParentSession();
2370
+ const targetVibeScope = VibeSessionRegistry.global().ownerScope(vibeSession);
2371
+ const preserveVibe =
2372
+ this.vibeModeEnabled &&
2373
+ sessionContext.mode === "vibe" &&
2374
+ this.#vibeModeOwnerScope?.ownerId === targetVibeScope.ownerId &&
2375
+ this.#vibeModeOwnerScope.parentSessionId === targetVibeScope.parentSessionId &&
2376
+ this.#vibeModeOwnerScope.parentSessionFile === targetVibeScope.parentSessionFile;
2377
+ await this.#clearTransientModeState({ preserveVibe, vibeScopeAlreadySuspended });
2378
+ await VibeSessionRegistry.global().rehydrate(vibeSession);
2294
2379
  const goalEnabled = this.session.settings.get("goal.enabled");
2295
2380
  if (!goalEnabled && (sessionContext.mode === "goal" || sessionContext.mode === "goal_paused")) {
2296
2381
  this.session.goalRuntime.clearAccounting();
@@ -2325,7 +2410,7 @@ export class InteractiveMode implements InteractiveModeContext {
2325
2410
  }
2326
2411
  this.session.goalRuntime.clearAccounting();
2327
2412
  if (sessionContext.mode === "vibe") {
2328
- await this.#enterVibeMode();
2413
+ if (!preserveVibe) await this.#enterVibeMode({ persistModeChange: false });
2329
2414
  return;
2330
2415
  }
2331
2416
  if (!this.session.settings.get("plan.enabled")) {
@@ -2643,6 +2728,8 @@ export class InteractiveMode implements InteractiveModeContext {
2643
2728
  onExternalEditor?: () => void;
2644
2729
  onPlanEdited?: (content: string) => void;
2645
2730
  onFeedbackChange?: (feedback: string) => void;
2731
+ annotationState?: PlanReviewAnnotationState;
2732
+ onAnnotationStateChange?: (state: PlanReviewAnnotationState) => void;
2646
2733
  initialIndex?: number;
2647
2734
  },
2648
2735
  extra?: { slider?: HookSelectorSlider },
@@ -2666,6 +2753,7 @@ export class InteractiveMode implements InteractiveModeContext {
2666
2753
  initialIndex: dialogOptions?.initialIndex,
2667
2754
  slider: extra?.slider,
2668
2755
  externalEditorLabel: this.keybindings.getDisplayString("app.editor.external") || undefined,
2756
+ annotationState: dialogOptions?.annotationState,
2669
2757
  },
2670
2758
  {
2671
2759
  onPick: choice => finish(choice),
@@ -2675,6 +2763,7 @@ export class InteractiveMode implements InteractiveModeContext {
2675
2763
  onAnnotationExternalEditor: (draft, commit) => void this.#openPlanAnnotationInExternalEditor(draft, commit),
2676
2764
  onPlanEdited: dialogOptions?.onPlanEdited,
2677
2765
  onFeedbackChange: dialogOptions?.onFeedbackChange,
2766
+ onAnnotationStateChange: dialogOptions?.onAnnotationStateChange,
2678
2767
  },
2679
2768
  );
2680
2769
  this.#planReviewOverlay = overlay;
@@ -2904,7 +2993,7 @@ export class InteractiveMode implements InteractiveModeContext {
2904
2993
  compactBeforeExecute?: boolean;
2905
2994
  executionModel?: ResolvedRoleModel;
2906
2995
  },
2907
- ): Promise<void> {
2996
+ ): Promise<boolean> {
2908
2997
  const previousTools = this.#planModePreviousTools ?? this.session.getEnabledToolNames();
2909
2998
 
2910
2999
  // Mark the pending abort caused by the plan-mode → compaction transition as
@@ -3003,7 +3092,7 @@ export class InteractiveMode implements InteractiveModeContext {
3003
3092
  this.showWarning(
3004
3093
  "Plan approved, but compaction was cancelled — execution not dispatched. Submit a turn to continue.",
3005
3094
  );
3006
- return;
3095
+ return false;
3007
3096
  }
3008
3097
 
3009
3098
  // Approved plans land in a fresh (or compacted) session whose first user-visible
@@ -3045,14 +3134,15 @@ export class InteractiveMode implements InteractiveModeContext {
3045
3134
  // noted below), catch `AgentBusyError` and fall back to the same queue.
3046
3135
  if (this.session.isStreaming) {
3047
3136
  await this.session.followUp(planModePrompt, undefined, { synthetic: true });
3048
- return;
3049
- }
3050
- try {
3051
- await this.session.prompt(planModePrompt, { synthetic: true });
3052
- } catch (error) {
3053
- if (!(error instanceof AgentBusyError)) throw error;
3054
- await this.session.followUp(planModePrompt, undefined, { synthetic: true });
3137
+ } else {
3138
+ try {
3139
+ await this.session.prompt(planModePrompt, { synthetic: true });
3140
+ } catch (error) {
3141
+ if (!(error instanceof AgentBusyError)) throw error;
3142
+ await this.session.followUp(planModePrompt, undefined, { synthetic: true });
3143
+ }
3055
3144
  }
3145
+ return true;
3056
3146
  }
3057
3147
  async #abortPlanApprovalTurnSilently(): Promise<void> {
3058
3148
  this.session.markPlanInternalAbortPending();
@@ -3132,7 +3222,7 @@ export class InteractiveMode implements InteractiveModeContext {
3132
3222
  }
3133
3223
  }
3134
3224
 
3135
- async #enterVibeMode(): Promise<void> {
3225
+ async #enterVibeMode(options?: { persistModeChange?: boolean }): Promise<void> {
3136
3226
  if (this.vibeModeEnabled) {
3137
3227
  return;
3138
3228
  }
@@ -3145,9 +3235,13 @@ export class InteractiveMode implements InteractiveModeContext {
3145
3235
  return;
3146
3236
  }
3147
3237
 
3238
+ const vibeRegistry = VibeSessionRegistry.global();
3239
+ const ownerScope = vibeRegistry.ownerScope(this.#vibeParentSession());
3240
+ vibeRegistry.activateScope(ownerScope);
3148
3241
  const previousTools = this.session.getEnabledToolNames();
3149
3242
  await this.session.activateVibeTools(["read"]);
3150
3243
  this.#vibeModePreviousTools = previousTools;
3244
+ this.#vibeModeOwnerScope = ownerScope;
3151
3245
  this.vibeModeEnabled = true;
3152
3246
  // Suppress cache-miss marker on the next turn: vibe mode changes the
3153
3247
  // injected context, which predictably invalidates the cache.
@@ -3157,7 +3251,7 @@ export class InteractiveMode implements InteractiveModeContext {
3157
3251
  await this.session.sendVibeModeContext({ deliverAs: "steer" });
3158
3252
  }
3159
3253
  this.#updateVibeModeStatus();
3160
- this.sessionManager.appendModeChange("vibe");
3254
+ if (options?.persistModeChange !== false) this.sessionManager.appendModeChange("vibe");
3161
3255
  this.showStatus("Vibe mode enabled. You direct fast/good worker sessions; toolset is read + vibe tools.");
3162
3256
  }
3163
3257
 
@@ -3165,17 +3259,15 @@ export class InteractiveMode implements InteractiveModeContext {
3165
3259
  if (!this.vibeModeEnabled) {
3166
3260
  return;
3167
3261
  }
3262
+ const ownerScope = this.#vibeModeOwnerScope;
3263
+ const killed = await VibeSessionRegistry.global().killAll(this.#vibeParentSession(), ownerScope);
3168
3264
  await this.session.deactivateVibeTools(this.#vibeModePreviousTools ?? []);
3169
3265
  this.session.setVibeModeState(undefined);
3170
3266
  this.vibeModeEnabled = false;
3171
3267
  this.#vibeModePreviousTools = undefined;
3268
+ this.#vibeModeOwnerScope = undefined;
3172
3269
  this.lastAssistantUsage = undefined;
3173
- const killed = await VibeSessionRegistry.global().killAll(
3174
- this.session.getAgentId() ?? MAIN_AGENT_ID,
3175
- this.session.asyncJobManager,
3176
- );
3177
3270
  this.#updateVibeModeStatus();
3178
- this.sessionManager.appendModeChange("none");
3179
3271
  this.showStatus(
3180
3272
  killed > 0
3181
3273
  ? `Vibe mode disabled. Killed ${killed} worker session${killed === 1 ? "" : "s"}.`
@@ -3579,6 +3671,7 @@ export class InteractiveMode implements InteractiveModeContext {
3579
3671
  // that the Refine branch re-prompts the model with.
3580
3672
  let editedContent: string | undefined;
3581
3673
  let feedback = "";
3674
+ const annotationStateKey = this.#resolvePlanFilePath(planFilePath);
3582
3675
 
3583
3676
  const choice = await this.showPlanReview(
3584
3677
  planContent,
@@ -3594,6 +3687,11 @@ export class InteractiveMode implements InteractiveModeContext {
3594
3687
  onFeedbackChange: value => {
3595
3688
  feedback = value;
3596
3689
  },
3690
+ annotationState: this.#planReviewAnnotationState.get(annotationStateKey),
3691
+ onAnnotationStateChange: state => {
3692
+ if (state.annotations.length > 0) this.#planReviewAnnotationState.set(annotationStateKey, state);
3693
+ else this.#planReviewAnnotationState.delete(annotationStateKey);
3694
+ },
3597
3695
  disabledIndices: keepContextDisabled ? [PLAN_KEEP_CONTEXT_OPTION_INDEX] : undefined,
3598
3696
  },
3599
3697
  { slider },
@@ -3645,13 +3743,14 @@ export class InteractiveMode implements InteractiveModeContext {
3645
3743
  : -1;
3646
3744
  const executionModel =
3647
3745
  slider && cycle && selectedTierIndex !== restoredIndex ? cycle.models[selectedTierIndex] : undefined;
3648
- await this.#approvePlan(latestPlanContent, {
3746
+ const executionDispatched = await this.#approvePlan(latestPlanContent, {
3649
3747
  planFilePath,
3650
3748
  title: details.title,
3651
3749
  preserveContext: choice !== "Approve and execute",
3652
3750
  compactBeforeExecute: choice === "Approve and compact context",
3653
3751
  executionModel,
3654
3752
  });
3753
+ if (executionDispatched) this.#planReviewAnnotationState.delete(annotationStateKey);
3655
3754
  } catch (error) {
3656
3755
  this.showError(
3657
3756
  `Failed to finalize approved plan: ${error instanceof Error ? error.message : String(error)}`,
@@ -3666,9 +3765,12 @@ export class InteractiveMode implements InteractiveModeContext {
3666
3765
  try {
3667
3766
  if (refinement) {
3668
3767
  if (this.onInputCallback) {
3669
- this.onInputCallback(this.startPendingSubmission({ text: feedback }));
3768
+ const input = this.startPendingSubmission({ text: feedback });
3769
+ this.#planReviewAnnotationStateBySubmission.set(input, annotationStateKey);
3770
+ this.onInputCallback(input);
3670
3771
  } else {
3671
3772
  await this.session.prompt(feedback);
3773
+ this.#planReviewAnnotationState.delete(annotationStateKey);
3672
3774
  }
3673
3775
  } else {
3674
3776
  this.showStatus("Refine plan: enter a follow-up prompt.");
@@ -3743,6 +3845,7 @@ export class InteractiveMode implements InteractiveModeContext {
3743
3845
  this.#stopLoadingAnimation(false);
3744
3846
  }
3745
3847
  this.#cleanupMicAnimation();
3848
+ this.#liveCommandController.dispose();
3746
3849
  this.#cancelTodoAutoClearTimer();
3747
3850
  this.#cancelObserverUiSyncTimer();
3748
3851
  this.#cancelGoalContinuation();
@@ -3785,6 +3888,8 @@ export class InteractiveMode implements InteractiveModeContext {
3785
3888
  if (this.#isShuttingDown) return;
3786
3889
  this.#isShuttingDown = true;
3787
3890
 
3891
+ await this.#liveCommandController.stop();
3892
+
3788
3893
  this.#btwController.dispose();
3789
3894
  this.#omfgController.dispose();
3790
3895
  this.#focusController.dispose();
@@ -3822,6 +3927,10 @@ export class InteractiveMode implements InteractiveModeContext {
3822
3927
  // Drain any in-flight Kitty key release events before stopping.
3823
3928
  // This prevents escape sequences from leaking to the parent shell over slow SSH.
3824
3929
  await this.ui.terminal.drainInput(1000);
3930
+ // Stop the run-state spinner interval BEFORE restoring the shell title, so a
3931
+ // pending tick cannot re-emit an OSC title after `popTerminalTitle` hands the
3932
+ // terminal back (which would leave the parent shell with a `π ⠋ …` tab).
3933
+ disposeTerminalTitleState();
3825
3934
  popTerminalTitle();
3826
3935
  this.stop();
3827
3936
 
@@ -4294,6 +4403,10 @@ export class InteractiveMode implements InteractiveModeContext {
4294
4403
  }
4295
4404
 
4296
4405
  async handleSTTToggle(): Promise<void> {
4406
+ if (this.#liveCommandController.active) {
4407
+ this.showWarning("End live mode before using push-to-talk speech input.");
4408
+ return;
4409
+ }
4297
4410
  if (!settings.get("stt.enabled")) {
4298
4411
  this.showWarning("Speech-to-text is disabled. Enable it in settings: stt.enabled");
4299
4412
  return;
@@ -4326,6 +4439,15 @@ export class InteractiveMode implements InteractiveModeContext {
4326
4439
  });
4327
4440
  }
4328
4441
 
4442
+ /** Start or stop the Codex-backed realtime voice surface. */
4443
+ async handleLiveCommand(): Promise<void> {
4444
+ if (this.#sttController && this.#sttController.state !== "idle") {
4445
+ this.showWarning("Finish the current speech-to-text capture before starting live mode.");
4446
+ return;
4447
+ }
4448
+ await this.#liveCommandController.handleCommand();
4449
+ }
4450
+
4329
4451
  #setMicCursor(color: { r: number; g: number; b: number }): void {
4330
4452
  this.editor.cursorOverride = `\x1b[38;2;${color.r};${color.g};${color.b}m${theme.icon.mic}\x1b[0m`;
4331
4453
  // Theme symbols can be wide (for example, 🎤), so measure the rendered override.
@@ -12,6 +12,13 @@ import { isRecord, ptree, readJsonl } from "@oh-my-pi/pi-utils";
12
12
  import type { FileSink } from "bun";
13
13
  import type { BashResult } from "../../exec/bash-executor";
14
14
  import type { AgentSessionEvent, SessionStats } from "../../session/agent-session";
15
+ import { MAX_RPC_FRAME_BYTES, MAX_RPC_REASSEMBLED_BYTES, RpcFrameDecoder, type RpcProtocolVersion } from "./rpc-frame";
16
+ import {
17
+ RPC_MESSAGES_PAGE_BUSY_ERROR,
18
+ RPC_MESSAGES_PAGE_STALE_ERROR,
19
+ type RpcMessagesPage,
20
+ type RpcMessagesPageOptions,
21
+ } from "./rpc-messages";
15
22
  import type {
16
23
  RpcAvailableCommandsUpdateFrame,
17
24
  RpcAvailableSlashCommand,
@@ -136,6 +143,16 @@ function isRpcResponse(value: unknown): value is RpcResponse {
136
143
  return true;
137
144
  }
138
145
 
146
+ function supportsRpcProtocolV2(value: Record<string, unknown>): boolean {
147
+ return (
148
+ value.type === "ready" &&
149
+ Array.isArray(value.supportedProtocolVersions) &&
150
+ value.supportedProtocolVersions.includes(2) &&
151
+ value.maxFrameBytes === MAX_RPC_FRAME_BYTES &&
152
+ value.maxReassembledFrameBytes === MAX_RPC_REASSEMBLED_BYTES
153
+ );
154
+ }
155
+
139
156
  function isAgentEvent(value: unknown): value is AgentEvent {
140
157
  if (!isRecord(value)) return false;
141
158
  const type = value.type;
@@ -200,6 +217,26 @@ function normalizeToolResult<TDetails>(result: RpcClientToolResult<TDetails>): A
200
217
  return result;
201
218
  }
202
219
 
220
+ /** Failed RPC command; `code` mirrors the server's machine-readable error code when present. */
221
+ export class RpcCommandError extends Error {
222
+ constructor(
223
+ message: string,
224
+ readonly command: string,
225
+ readonly code?: string,
226
+ ) {
227
+ super(message);
228
+ this.name = "RpcCommandError";
229
+ }
230
+ }
231
+
232
+ /** True when a high-level `getMessages()` drain should discard partial pages and fall back to `get_messages`. */
233
+ function isPageFallbackError(error: unknown): boolean {
234
+ if (!(error instanceof Error)) return false;
235
+ if (error instanceof RpcCommandError && (error.code === "session_busy" || error.code === "stale_cursor"))
236
+ return true;
237
+ return error.message === RPC_MESSAGES_PAGE_BUSY_ERROR || error.message === RPC_MESSAGES_PAGE_STALE_ERROR;
238
+ }
239
+
203
240
  // ============================================================================
204
241
  // RPC Client
205
242
  // ============================================================================
@@ -218,6 +255,7 @@ export class RpcClient {
218
255
  #customTools: RpcClientCustomTool[] = [];
219
256
  #pendingHostToolCalls = new Map<string, { controller: AbortController }>();
220
257
  #requestId = 0;
258
+ #protocolVersion: RpcProtocolVersion = 1;
221
259
  #extensionUiListeners: Set<(req: RpcExtensionUIRequest) => void> = new Set();
222
260
  #abortController = new AbortController();
223
261
 
@@ -242,6 +280,7 @@ export class RpcClient {
242
280
  // Mint a fresh controller so a previous stop()'s abort does not
243
281
  // short-circuit the new stdout reader (issue #4079).
244
282
  this.#abortController = new AbortController();
283
+ this.#protocolVersion = 1;
245
284
 
246
285
  const cliPath = this.options.cliPath ?? "dist/cli.js";
247
286
  const args = ["--mode", "rpc"];
@@ -269,6 +308,9 @@ export class RpcClient {
269
308
  // Wait for the "ready" signal or process exit
270
309
  const { promise: readyPromise, resolve: readyResolve, reject: readyReject } = Promise.withResolvers<void>();
271
310
  let readySettled = false;
311
+ let protocolV2Supported = false;
312
+ let protocolV2Enabled = false;
313
+ const frameDecoder = new RpcFrameDecoder();
272
314
 
273
315
  const reapAfterOutputFailure = async (error: Error) => {
274
316
  if (this.#process !== child) return;
@@ -294,11 +336,15 @@ export class RpcClient {
294
336
  void (async () => {
295
337
  for await (const line of lines) {
296
338
  if (!readySettled && isRecord(line) && line.type === "ready") {
339
+ protocolV2Supported = supportsRpcProtocolV2(line);
297
340
  readySettled = true;
298
341
  readyResolve();
299
342
  continue;
300
343
  }
301
- this.#handleLine(line);
344
+ if (isRecord(line) && line.type === "rpc_chunk" && !protocolV2Enabled)
345
+ throw new Error("RPC chunk received before protocol negotiation");
346
+ const decoded = frameDecoder.push(line);
347
+ if (decoded) this.#handleLine(decoded);
302
348
  }
303
349
  // A closed stdout is terminal even if the child remains alive. Startup
304
350
  // failures are reaped by the readyPromise catch below; established
@@ -359,6 +405,18 @@ export class RpcClient {
359
405
 
360
406
  try {
361
407
  await readyPromise;
408
+ if (protocolV2Supported) {
409
+ protocolV2Enabled = true;
410
+ const response = await this.#send({ type: "negotiate_protocol", protocolVersion: 2 });
411
+ if (
412
+ !response.success ||
413
+ response.command !== "negotiate_protocol" ||
414
+ !isRecord(response.data) ||
415
+ response.data.protocolVersion !== 2
416
+ )
417
+ throw new Error("RPC protocol v2 negotiation failed");
418
+ this.#protocolVersion = 2;
419
+ }
362
420
  if (this.#customTools.length > 0) {
363
421
  await this.setCustomTools(this.#customTools);
364
422
  }
@@ -744,9 +802,42 @@ export class RpcClient {
744
802
  }
745
803
 
746
804
  /**
747
- * Get all messages in the session.
805
+ * Get one stable, byte-bounded message page.
748
806
  */
807
+ async getMessagesPage(options: RpcMessagesPageOptions = {}): Promise<RpcMessagesPage> {
808
+ const response = await this.#send({ type: "get_messages_page", ...options });
809
+ return this.#getData<RpcMessagesPage>(response);
810
+ }
811
+
812
+ /** Get all messages, draining stable pages when protocol v2 is available. */
749
813
  async getMessages(): Promise<AgentMessage[]> {
814
+ if (this.#protocolVersion === 2) {
815
+ try {
816
+ const messages: AgentMessage[] = [];
817
+ const seenCursors = new Set<string>();
818
+ let totalMessages: number | undefined;
819
+ let cursor: string | undefined;
820
+ do {
821
+ const page = await this.getMessagesPage({ cursor, limit: 256 });
822
+ if (
823
+ !Number.isSafeInteger(page.totalMessages) ||
824
+ page.totalMessages < 0 ||
825
+ (totalMessages !== undefined && page.totalMessages !== totalMessages)
826
+ )
827
+ throw new Error("RPC message pagination returned an inconsistent total");
828
+ totalMessages = page.totalMessages;
829
+ messages.push(...page.messages);
830
+ cursor = page.nextCursor;
831
+ if (cursor && seenCursors.has(cursor)) throw new Error("RPC message pagination repeated a cursor");
832
+ if (cursor) seenCursors.add(cursor);
833
+ } while (cursor);
834
+ if (messages.length !== totalMessages)
835
+ throw new Error("RPC message pagination ended before the advertised total");
836
+ return messages;
837
+ } catch (error) {
838
+ if (!isPageFallbackError(error)) throw error;
839
+ }
840
+ }
750
841
  const response = await this.#send({ type: "get_messages" });
751
842
  return this.#getData<{ messages: AgentMessage[] }>(response).messages;
752
843
  }
@@ -1094,7 +1185,7 @@ export class RpcClient {
1094
1185
  #getData<T>(response: RpcResponse): T {
1095
1186
  if (!response.success) {
1096
1187
  const errorResponse = response as Extract<RpcResponse, { success: false }>;
1097
- throw new Error(errorResponse.error);
1188
+ throw new RpcCommandError(errorResponse.error, errorResponse.command, errorResponse.code);
1098
1189
  }
1099
1190
  // Type assertion: we trust response.data matches T based on the command sent.
1100
1191
  // This is safe because each public method specifies the correct T for its command.