@oh-my-pi/pi-coding-agent 15.12.3 → 15.13.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 (457) hide show
  1. package/CHANGELOG.md +347 -7
  2. package/dist/cli.js +1615 -1231
  3. package/dist/types/async/job-manager.d.ts +15 -0
  4. package/dist/types/autolearn/controller.d.ts +25 -0
  5. package/dist/types/autolearn/managed-skills.d.ts +45 -0
  6. package/dist/types/autoresearch/state.d.ts +1 -1
  7. package/dist/types/autoresearch/tools/init-experiment.d.ts +1 -1
  8. package/dist/types/autoresearch/tools/log-experiment.d.ts +1 -1
  9. package/dist/types/autoresearch/tools/run-experiment.d.ts +1 -1
  10. package/dist/types/autoresearch/tools/update-notes.d.ts +1 -1
  11. package/dist/types/autoresearch/types.d.ts +1 -1
  12. package/dist/types/cli/args.d.ts +19 -2
  13. package/dist/types/cli/models-cli.d.ts +49 -0
  14. package/dist/types/cli/session-picker.d.ts +1 -1
  15. package/dist/types/cli/setup-cli.d.ts +1 -1
  16. package/dist/types/cli/setup-model-picker.d.ts +14 -0
  17. package/dist/types/collab/protocol.d.ts +1 -1
  18. package/dist/types/commands/launch.d.ts +0 -3
  19. package/dist/types/commands/models.d.ts +33 -0
  20. package/dist/types/commands/say.d.ts +24 -0
  21. package/dist/types/commands/token.d.ts +25 -0
  22. package/dist/types/commit/agentic/tools/analyze-file.d.ts +1 -1
  23. package/dist/types/commit/agentic/tools/git-file-diff.d.ts +1 -1
  24. package/dist/types/commit/agentic/tools/git-hunk.d.ts +1 -1
  25. package/dist/types/commit/agentic/tools/git-overview.d.ts +1 -1
  26. package/dist/types/commit/agentic/tools/propose-changelog.d.ts +1 -1
  27. package/dist/types/commit/agentic/tools/propose-commit.d.ts +1 -1
  28. package/dist/types/commit/agentic/tools/recent-commits.d.ts +1 -1
  29. package/dist/types/commit/agentic/tools/schemas.d.ts +1 -1
  30. package/dist/types/commit/agentic/tools/split-commit.d.ts +1 -1
  31. package/dist/types/commit/changelog/generate.d.ts +1 -1
  32. package/dist/types/commit/shared-llm.d.ts +1 -1
  33. package/dist/types/config/keybindings.d.ts +3 -3
  34. package/dist/types/config/model-registry.d.ts +17 -0
  35. package/dist/types/config/models-config-schema.d.ts +13 -1
  36. package/dist/types/config/models-config.d.ts +8 -2
  37. package/dist/types/config/settings-schema.d.ts +281 -58
  38. package/dist/types/edit/hashline/params.d.ts +1 -1
  39. package/dist/types/edit/modes/apply-patch.d.ts +1 -1
  40. package/dist/types/edit/modes/patch.d.ts +1 -1
  41. package/dist/types/edit/modes/replace.d.ts +1 -1
  42. package/dist/types/export/html/index.d.ts +2 -1
  43. package/dist/types/extensibility/custom-commands/types.d.ts +2 -2
  44. package/dist/types/extensibility/custom-tools/types.d.ts +2 -2
  45. package/dist/types/extensibility/extensions/model-api.d.ts +17 -0
  46. package/dist/types/extensibility/extensions/runner.d.ts +3 -1
  47. package/dist/types/extensibility/extensions/types.d.ts +49 -3
  48. package/dist/types/extensibility/hooks/index.d.ts +2 -1
  49. package/dist/types/extensibility/hooks/types.d.ts +2 -2
  50. package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +9 -0
  51. package/dist/types/extensibility/plugins/loader.d.ts +11 -0
  52. package/dist/types/extensibility/shared-events.d.ts +1 -1
  53. package/dist/types/extensibility/skills.d.ts +10 -0
  54. package/dist/types/goals/guided-setup.d.ts +18 -0
  55. package/dist/types/goals/state.d.ts +1 -1
  56. package/dist/types/goals/tools/goal-tool.d.ts +1 -1
  57. package/dist/types/hindsight/transcript.d.ts +1 -1
  58. package/dist/types/index.d.ts +5 -0
  59. package/dist/types/internal-urls/local-protocol.d.ts +4 -2
  60. package/dist/types/lsp/types.d.ts +1 -1
  61. package/dist/types/main.d.ts +4 -3
  62. package/dist/types/mcp/manager.d.ts +8 -0
  63. package/dist/types/mcp/startup-events.d.ts +11 -0
  64. package/dist/types/memories/index.d.ts +7 -0
  65. package/dist/types/memory-backend/local-backend.d.ts +4 -3
  66. package/dist/types/mnemopi/config.d.ts +28 -0
  67. package/dist/types/modes/acp/acp-agent.d.ts +1 -2
  68. package/dist/types/modes/components/agent-hub.d.ts +6 -0
  69. package/dist/types/modes/components/assistant-message.d.ts +1 -2
  70. package/dist/types/modes/components/compaction-summary-message.d.ts +15 -1
  71. package/dist/types/modes/components/custom-editor.d.ts +39 -1
  72. package/dist/types/modes/components/custom-editor.test.d.ts +1 -0
  73. package/dist/types/modes/components/index.d.ts +1 -0
  74. package/dist/types/modes/components/logout-account-selector.d.ts +8 -0
  75. package/dist/types/modes/components/session-selector.d.ts +1 -1
  76. package/dist/types/modes/components/status-line/component.d.ts +9 -5
  77. package/dist/types/modes/components/status-line/types.d.ts +2 -1
  78. package/dist/types/modes/components/tool-execution.d.ts +26 -16
  79. package/dist/types/modes/components/transcript-container.d.ts +23 -2
  80. package/dist/types/modes/components/tree-selector.d.ts +1 -1
  81. package/dist/types/modes/components/usage-row.d.ts +3 -0
  82. package/dist/types/modes/controllers/command-controller.d.ts +2 -2
  83. package/dist/types/modes/controllers/event-controller.d.ts +0 -17
  84. package/dist/types/modes/controllers/input-controller.d.ts +14 -0
  85. package/dist/types/modes/controllers/selector-controller.d.ts +3 -1
  86. package/dist/types/modes/gradient-highlight.d.ts +9 -4
  87. package/dist/types/modes/image-references.d.ts +6 -0
  88. package/dist/types/modes/interactive-mode.d.ts +27 -6
  89. package/dist/types/modes/magic-keywords.d.ts +13 -1
  90. package/dist/types/modes/rpc/rpc-mode.d.ts +35 -1
  91. package/dist/types/modes/rpc/rpc-types.d.ts +9 -1
  92. package/dist/types/modes/runtime-init.d.ts +4 -0
  93. package/dist/types/modes/theme/theme.d.ts +13 -2
  94. package/dist/types/modes/types.d.ts +8 -7
  95. package/dist/types/modes/utils/ui-helpers.d.ts +1 -1
  96. package/dist/types/registry/agent-registry.d.ts +17 -0
  97. package/dist/types/secrets/obfuscator.d.ts +1 -1
  98. package/dist/types/session/agent-session.d.ts +28 -35
  99. package/dist/types/session/agent-storage.d.ts +2 -1
  100. package/dist/types/session/indexed-session-storage.d.ts +3 -3
  101. package/dist/types/session/messages.d.ts +8 -10
  102. package/dist/types/session/session-context.d.ts +39 -0
  103. package/dist/types/session/session-entries.d.ts +159 -0
  104. package/dist/types/session/session-listing.d.ts +69 -0
  105. package/dist/types/session/session-loader.d.ts +16 -0
  106. package/dist/types/session/session-manager.d.ts +85 -462
  107. package/dist/types/session/session-migrations.d.ts +12 -0
  108. package/dist/types/session/session-paths.d.ts +25 -0
  109. package/dist/types/session/session-persistence.d.ts +8 -0
  110. package/dist/types/session/session-storage.d.ts +11 -7
  111. package/dist/types/session/snapcompact-inline.d.ts +12 -1
  112. package/dist/types/session/snapcompact-savings-journal.d.ts +46 -0
  113. package/dist/types/session/tool-choice-queue.d.ts +6 -6
  114. package/dist/types/slash-commands/helpers/logout.d.ts +15 -0
  115. package/dist/types/stt/asr-client.d.ts +90 -0
  116. package/dist/types/stt/asr-protocol.d.ts +97 -0
  117. package/dist/types/stt/asr-worker.d.ts +2 -0
  118. package/dist/types/stt/downloader.d.ts +38 -0
  119. package/dist/types/stt/endpointer.d.ts +59 -0
  120. package/dist/types/stt/index.d.ts +5 -1
  121. package/dist/types/stt/models.d.ts +120 -0
  122. package/dist/types/stt/recorder.d.ts +17 -0
  123. package/dist/types/stt/stt-controller.d.ts +6 -0
  124. package/dist/types/stt/transcriber.d.ts +5 -7
  125. package/dist/types/stt/wav.d.ts +29 -0
  126. package/dist/types/system-prompt.d.ts +4 -0
  127. package/dist/types/task/executor.d.ts +2 -0
  128. package/dist/types/task/index.d.ts +9 -1
  129. package/dist/types/task/types.d.ts +37 -1
  130. package/dist/types/tools/ask.d.ts +1 -1
  131. package/dist/types/tools/ast-edit.d.ts +1 -1
  132. package/dist/types/tools/ast-grep.d.ts +1 -1
  133. package/dist/types/tools/bash.d.ts +3 -3
  134. package/dist/types/tools/browser/cmux/cmux-tab.d.ts +202 -0
  135. package/dist/types/tools/browser/cmux/rpc.d.ts +70 -0
  136. package/dist/types/tools/browser/cmux/socket-client.d.ts +19 -0
  137. package/dist/types/tools/browser/registry.d.ts +16 -3
  138. package/dist/types/tools/browser/render.d.ts +2 -0
  139. package/dist/types/tools/browser/tab-protocol.d.ts +2 -0
  140. package/dist/types/tools/browser/tab-supervisor.d.ts +16 -4
  141. package/dist/types/tools/browser.d.ts +3 -1
  142. package/dist/types/tools/checkpoint.d.ts +1 -1
  143. package/dist/types/tools/debug.d.ts +1 -1
  144. package/dist/types/tools/eval-render.d.ts +1 -1
  145. package/dist/types/tools/eval.d.ts +1 -1
  146. package/dist/types/tools/find.d.ts +1 -1
  147. package/dist/types/tools/gh.d.ts +1 -1
  148. package/dist/types/tools/image-gen.d.ts +1 -1
  149. package/dist/types/tools/index.d.ts +14 -2
  150. package/dist/types/tools/inspect-image.d.ts +1 -1
  151. package/dist/types/tools/irc.d.ts +2 -1
  152. package/dist/types/tools/job.d.ts +1 -1
  153. package/dist/types/tools/learn.d.ts +51 -0
  154. package/dist/types/tools/manage-skill.d.ts +40 -0
  155. package/dist/types/tools/memory-edit.d.ts +1 -1
  156. package/dist/types/tools/memory-recall.d.ts +1 -1
  157. package/dist/types/tools/memory-reflect.d.ts +1 -1
  158. package/dist/types/tools/memory-retain.d.ts +1 -1
  159. package/dist/types/tools/plan-mode-guard.d.ts +10 -0
  160. package/dist/types/tools/read.d.ts +1 -1
  161. package/dist/types/tools/render-mermaid.d.ts +1 -1
  162. package/dist/types/tools/renderers.d.ts +7 -11
  163. package/dist/types/tools/resolve.d.ts +1 -1
  164. package/dist/types/tools/review.d.ts +1 -1
  165. package/dist/types/tools/search-tool-bm25.d.ts +1 -1
  166. package/dist/types/tools/search.d.ts +1 -1
  167. package/dist/types/tools/ssh.d.ts +2 -2
  168. package/dist/types/tools/todo.d.ts +2 -2
  169. package/dist/types/tools/tts.d.ts +26 -1
  170. package/dist/types/tools/write.d.ts +2 -2
  171. package/dist/types/tts/downloader.d.ts +20 -0
  172. package/dist/types/tts/index.d.ts +8 -0
  173. package/dist/types/tts/models.d.ts +82 -0
  174. package/dist/types/tts/player.d.ts +32 -0
  175. package/dist/types/tts/runtime.d.ts +6 -0
  176. package/dist/types/tts/streaming-player.d.ts +41 -0
  177. package/dist/types/tts/tts-client.d.ts +93 -0
  178. package/dist/types/tts/tts-protocol.d.ts +95 -0
  179. package/dist/types/tts/tts-worker.d.ts +2 -0
  180. package/dist/types/tts/vocalizer.d.ts +41 -0
  181. package/dist/types/tts/wav.d.ts +8 -0
  182. package/dist/types/utils/clipboard.d.ts +4 -3
  183. package/dist/types/utils/image-loading.d.ts +18 -1
  184. package/dist/types/utils/thinking-display.d.ts +17 -0
  185. package/dist/types/utils/tool-choice.d.ts +8 -0
  186. package/dist/types/utils/tools-manager.d.ts +2 -1
  187. package/dist/types/utils/tools-manager.test.d.ts +1 -0
  188. package/dist/types/web/scrapers/github.d.ts +1 -1
  189. package/dist/types/web/search/index.d.ts +1 -1
  190. package/package.json +17 -16
  191. package/src/async/job-manager.ts +49 -0
  192. package/src/autolearn/controller.ts +139 -0
  193. package/src/autolearn/managed-skills.ts +257 -0
  194. package/src/autoresearch/state.ts +1 -1
  195. package/src/autoresearch/storage.ts +2 -1
  196. package/src/autoresearch/tools/init-experiment.ts +1 -1
  197. package/src/autoresearch/tools/log-experiment.ts +1 -1
  198. package/src/autoresearch/tools/run-experiment.ts +1 -1
  199. package/src/autoresearch/tools/update-notes.ts +1 -1
  200. package/src/autoresearch/types.ts +1 -1
  201. package/src/cli/args.ts +56 -10
  202. package/src/cli/auth-gateway-cli.ts +1 -1
  203. package/src/cli/bench-cli.ts +1 -1
  204. package/src/cli/dry-balance-cli.ts +1 -1
  205. package/src/cli/models-cli.ts +427 -0
  206. package/src/cli/session-picker.ts +2 -1
  207. package/src/cli/setup-cli.ts +148 -47
  208. package/src/cli/setup-model-picker.ts +43 -0
  209. package/src/cli-commands.ts +3 -0
  210. package/src/cli.ts +45 -13
  211. package/src/collab/host.ts +10 -13
  212. package/src/collab/protocol.ts +1 -1
  213. package/src/commands/launch.ts +0 -3
  214. package/src/commands/models.ts +61 -0
  215. package/src/commands/say.ts +102 -0
  216. package/src/commands/setup.ts +1 -1
  217. package/src/commands/token.ts +89 -0
  218. package/src/commit/agentic/tools/analyze-file.ts +4 -1
  219. package/src/commit/agentic/tools/git-file-diff.ts +1 -1
  220. package/src/commit/agentic/tools/git-hunk.ts +1 -1
  221. package/src/commit/agentic/tools/git-overview.ts +1 -1
  222. package/src/commit/agentic/tools/propose-changelog.ts +1 -1
  223. package/src/commit/agentic/tools/propose-commit.ts +1 -1
  224. package/src/commit/agentic/tools/recent-commits.ts +1 -1
  225. package/src/commit/agentic/tools/schemas.ts +1 -1
  226. package/src/commit/agentic/tools/split-commit.ts +1 -1
  227. package/src/commit/analysis/summary.ts +1 -1
  228. package/src/commit/changelog/generate.ts +1 -1
  229. package/src/commit/shared-llm.ts +1 -1
  230. package/src/config/keybindings.ts +2 -2
  231. package/src/config/model-discovery.ts +11 -5
  232. package/src/config/model-registry.ts +79 -21
  233. package/src/config/model-resolver.ts +2 -2
  234. package/src/config/models-config-schema.ts +5 -2
  235. package/src/config/models-config.ts +2 -1
  236. package/src/config/settings-schema.ts +266 -32
  237. package/src/config/settings.ts +10 -0
  238. package/src/discovery/builtin.ts +23 -1
  239. package/src/discovery/claude-plugins.ts +44 -5
  240. package/src/discovery/helpers.ts +41 -1
  241. package/src/edit/hashline/params.ts +1 -1
  242. package/src/edit/modes/apply-patch.ts +1 -1
  243. package/src/edit/modes/patch.ts +1 -1
  244. package/src/edit/modes/replace.ts +1 -1
  245. package/src/eval/__tests__/budget-bridge.test.ts +1 -1
  246. package/src/eval/agent-bridge.ts +1 -1
  247. package/src/eval/completion-bridge.ts +1 -1
  248. package/src/eval/js/shared/prelude.txt +69 -17
  249. package/src/export/html/index.ts +3 -6
  250. package/src/export/html/template.js +24 -2
  251. package/src/export/html/tool-views.generated.js +2 -2
  252. package/src/extensibility/custom-commands/loader.ts +1 -1
  253. package/src/extensibility/custom-commands/types.ts +2 -2
  254. package/src/extensibility/custom-tools/loader.ts +1 -1
  255. package/src/extensibility/custom-tools/types.ts +2 -2
  256. package/src/extensibility/extensions/loader.ts +2 -2
  257. package/src/extensibility/extensions/model-api.ts +41 -0
  258. package/src/extensibility/extensions/runner.ts +4 -0
  259. package/src/extensibility/extensions/types.ts +54 -3
  260. package/src/extensibility/extensions/wrapper.ts +41 -5
  261. package/src/extensibility/hooks/index.ts +2 -1
  262. package/src/extensibility/hooks/loader.ts +1 -1
  263. package/src/extensibility/hooks/types.ts +2 -2
  264. package/src/extensibility/plugins/legacy-pi-compat.ts +43 -13
  265. package/src/extensibility/plugins/loader.ts +30 -19
  266. package/src/extensibility/plugins/manager.ts +221 -90
  267. package/src/extensibility/shared-events.ts +1 -1
  268. package/src/extensibility/skills.ts +101 -5
  269. package/src/goals/guided-setup.ts +133 -0
  270. package/src/goals/state.ts +1 -1
  271. package/src/goals/tools/goal-tool.ts +1 -1
  272. package/src/hindsight/transcript.ts +1 -1
  273. package/src/index.ts +5 -0
  274. package/src/internal-urls/docs-index.generated.ts +13 -10
  275. package/src/internal-urls/history-protocol.ts +1 -1
  276. package/src/internal-urls/local-protocol.ts +29 -7
  277. package/src/lsp/types.ts +1 -1
  278. package/src/main.ts +27 -32
  279. package/src/mcp/config-writer.ts +7 -3
  280. package/src/mcp/manager.ts +11 -0
  281. package/src/mcp/startup-events.ts +21 -0
  282. package/src/mcp/transports/stdio.ts +2 -1
  283. package/src/memories/index.ts +149 -12
  284. package/src/memories/storage.ts +2 -1
  285. package/src/memory-backend/local-backend.ts +11 -5
  286. package/src/mnemopi/backend.ts +1 -0
  287. package/src/mnemopi/config.ts +112 -12
  288. package/src/modes/acp/acp-agent.ts +8 -53
  289. package/src/modes/acp/acp-event-mapper.ts +5 -1
  290. package/src/modes/components/agent-hub.ts +51 -5
  291. package/src/modes/components/assistant-message.ts +12 -44
  292. package/src/modes/components/compaction-summary-message.ts +125 -26
  293. package/src/modes/components/custom-editor.test.ts +96 -0
  294. package/src/modes/components/custom-editor.ts +164 -8
  295. package/src/modes/components/index.ts +1 -0
  296. package/src/modes/components/logout-account-selector.ts +130 -0
  297. package/src/modes/components/mcp-add-wizard.ts +1 -1
  298. package/src/modes/components/model-selector.ts +2 -2
  299. package/src/modes/components/session-selector.ts +1 -1
  300. package/src/modes/components/settings-defs.ts +7 -0
  301. package/src/modes/components/status-line/component.ts +54 -157
  302. package/src/modes/components/status-line/segments.ts +1 -1
  303. package/src/modes/components/status-line/types.ts +2 -1
  304. package/src/modes/components/tool-execution.ts +82 -43
  305. package/src/modes/components/transcript-container.ts +70 -1
  306. package/src/modes/components/tree-selector.ts +1 -1
  307. package/src/modes/components/usage-row.ts +18 -0
  308. package/src/modes/components/user-message.ts +4 -2
  309. package/src/modes/controllers/command-controller.ts +14 -16
  310. package/src/modes/controllers/event-controller.ts +101 -73
  311. package/src/modes/controllers/extension-ui-controller.ts +6 -0
  312. package/src/modes/controllers/input-controller.ts +311 -57
  313. package/src/modes/controllers/mcp-command-controller.ts +44 -3
  314. package/src/modes/controllers/selector-controller.ts +68 -12
  315. package/src/modes/controllers/streaming-reveal.ts +4 -3
  316. package/src/modes/gradient-highlight.ts +21 -9
  317. package/src/modes/image-references.ts +20 -0
  318. package/src/modes/interactive-mode.ts +288 -48
  319. package/src/modes/magic-keywords.ts +27 -5
  320. package/src/modes/rpc/rpc-mode.ts +146 -14
  321. package/src/modes/rpc/rpc-subagents.ts +2 -2
  322. package/src/modes/rpc/rpc-types.ts +8 -2
  323. package/src/modes/runtime-init.ts +28 -3
  324. package/src/modes/theme/theme.ts +99 -51
  325. package/src/modes/types.ts +6 -7
  326. package/src/modes/utils/hotkeys-markdown.ts +1 -1
  327. package/src/modes/utils/ui-helpers.ts +36 -7
  328. package/src/priority.json +5 -1
  329. package/src/prompts/agents/task.md +1 -0
  330. package/src/prompts/goals/guided-goal-interview.md +8 -0
  331. package/src/prompts/goals/guided-goal-system.md +12 -0
  332. package/src/prompts/memories/read-path.md +6 -0
  333. package/src/prompts/system/autolearn-guidance-learn.md +1 -0
  334. package/src/prompts/system/autolearn-guidance.md +7 -0
  335. package/src/prompts/system/autolearn-nudge.md +3 -0
  336. package/src/prompts/system/eager-task.md +7 -0
  337. package/src/prompts/system/eager-todo.md +11 -6
  338. package/src/prompts/system/empty-stop-retry.md +4 -6
  339. package/src/prompts/system/subagent-system-prompt.md +4 -0
  340. package/src/prompts/system/system-prompt.md +10 -5
  341. package/src/prompts/system/title-marker-instruction.md +1 -0
  342. package/src/prompts/system/title-system-marker.md +16 -0
  343. package/src/prompts/tools/job.md +1 -0
  344. package/src/prompts/tools/learn.md +7 -0
  345. package/src/prompts/tools/manage-skill.md +9 -0
  346. package/src/prompts/tools/task.md +3 -0
  347. package/src/registry/agent-registry.ts +30 -0
  348. package/src/sdk.ts +103 -43
  349. package/src/secrets/obfuscator.ts +1 -1
  350. package/src/session/agent-session.ts +331 -318
  351. package/src/session/agent-storage.ts +18 -9
  352. package/src/session/history-storage.ts +3 -2
  353. package/src/session/indexed-session-storage.ts +7 -10
  354. package/src/session/messages.ts +9 -11
  355. package/src/session/session-context.ts +352 -0
  356. package/src/session/session-dump-format.ts +4 -2
  357. package/src/session/session-entries.ts +194 -0
  358. package/src/session/session-listing.ts +588 -0
  359. package/src/session/session-loader.ts +106 -0
  360. package/src/session/session-manager.ts +968 -3064
  361. package/src/session/session-migrations.ts +78 -0
  362. package/src/session/session-paths.ts +193 -0
  363. package/src/session/session-persistence.ts +131 -0
  364. package/src/session/session-storage.ts +91 -30
  365. package/src/session/snapcompact-inline.ts +21 -1
  366. package/src/session/snapcompact-savings-journal.ts +113 -0
  367. package/src/session/tool-choice-queue.ts +23 -11
  368. package/src/slash-commands/builtin-registry.ts +40 -4
  369. package/src/slash-commands/helpers/logout.ts +88 -0
  370. package/src/stt/asr-client.ts +520 -0
  371. package/src/stt/asr-protocol.ts +65 -0
  372. package/src/stt/asr-worker.ts +790 -0
  373. package/src/stt/downloader.ts +107 -47
  374. package/src/stt/endpointer.ts +259 -0
  375. package/src/stt/index.ts +5 -1
  376. package/src/stt/models.ts +150 -0
  377. package/src/stt/recorder.ts +247 -60
  378. package/src/stt/stt-controller.ts +201 -22
  379. package/src/stt/transcriber.ts +37 -68
  380. package/src/stt/wav.ts +173 -0
  381. package/src/system-prompt.ts +8 -0
  382. package/src/task/agents.ts +1 -2
  383. package/src/task/executor.ts +49 -15
  384. package/src/task/index.ts +60 -6
  385. package/src/task/render.ts +83 -8
  386. package/src/task/types.ts +54 -1
  387. package/src/tools/ask.ts +9 -1
  388. package/src/tools/ast-edit.ts +1 -1
  389. package/src/tools/ast-grep.ts +1 -1
  390. package/src/tools/bash.ts +5 -4
  391. package/src/tools/browser/cmux/cmux-tab.ts +1264 -0
  392. package/src/tools/browser/cmux/rpc.ts +156 -0
  393. package/src/tools/browser/cmux/socket-client.ts +309 -0
  394. package/src/tools/browser/registry.ts +37 -3
  395. package/src/tools/browser/render.ts +6 -1
  396. package/src/tools/browser/tab-protocol.ts +2 -0
  397. package/src/tools/browser/tab-supervisor.ts +189 -18
  398. package/src/tools/browser/tab-worker.ts +1 -1
  399. package/src/tools/browser.ts +16 -1
  400. package/src/tools/checkpoint.ts +1 -1
  401. package/src/tools/debug.ts +1 -1
  402. package/src/tools/eval-render.ts +4 -3
  403. package/src/tools/eval.ts +11 -6
  404. package/src/tools/fetch.ts +13 -2
  405. package/src/tools/find.ts +1 -1
  406. package/src/tools/gh.ts +1 -1
  407. package/src/tools/github-cache.ts +2 -1
  408. package/src/tools/image-gen.ts +1 -1
  409. package/src/tools/index.ts +43 -5
  410. package/src/tools/inspect-image.ts +3 -1
  411. package/src/tools/irc.ts +11 -3
  412. package/src/tools/job.ts +15 -3
  413. package/src/tools/learn.ts +144 -0
  414. package/src/tools/manage-skill.ts +104 -0
  415. package/src/tools/memory-edit.ts +1 -1
  416. package/src/tools/memory-recall.ts +1 -1
  417. package/src/tools/memory-reflect.ts +1 -1
  418. package/src/tools/memory-retain.ts +1 -1
  419. package/src/tools/plan-mode-guard.ts +53 -19
  420. package/src/tools/read.ts +8 -2
  421. package/src/tools/render-mermaid.ts +1 -1
  422. package/src/tools/renderers.ts +7 -11
  423. package/src/tools/report-tool-issue.ts +3 -2
  424. package/src/tools/resolve.ts +1 -1
  425. package/src/tools/review.ts +1 -1
  426. package/src/tools/search-tool-bm25.ts +1 -1
  427. package/src/tools/search.ts +1 -1
  428. package/src/tools/ssh.ts +5 -4
  429. package/src/tools/todo.ts +2 -2
  430. package/src/tools/tts.ts +204 -93
  431. package/src/tools/write.ts +19 -3
  432. package/src/tts/downloader.ts +64 -0
  433. package/src/tts/index.ts +8 -0
  434. package/src/tts/models.ts +137 -0
  435. package/src/tts/player.ts +137 -0
  436. package/src/tts/runtime.ts +21 -0
  437. package/src/tts/streaming-player.ts +266 -0
  438. package/src/tts/tts-client.ts +647 -0
  439. package/src/tts/tts-protocol.ts +60 -0
  440. package/src/tts/tts-worker.ts +497 -0
  441. package/src/tts/vocalizer.ts +162 -0
  442. package/src/tts/wav.ts +58 -0
  443. package/src/utils/clipboard.ts +35 -18
  444. package/src/utils/image-loading.ts +35 -4
  445. package/src/utils/thinking-display.ts +37 -0
  446. package/src/utils/title-generator.ts +48 -5
  447. package/src/utils/tool-choice.ts +16 -0
  448. package/src/utils/tools-manager.test.ts +25 -0
  449. package/src/utils/tools-manager.ts +19 -1
  450. package/src/web/scrapers/github.ts +96 -0
  451. package/src/web/search/index.ts +14 -1
  452. package/src/web/search/providers/searxng.ts +13 -1
  453. package/dist/types/cli/list-models.d.ts +0 -30
  454. package/dist/types/stt/setup.d.ts +0 -18
  455. package/src/cli/list-models.ts +0 -194
  456. package/src/stt/setup.ts +0 -52
  457. package/src/stt/transcribe.py +0 -70
@@ -4,6 +4,7 @@ import {
4
4
  type NativeScrollbackCommittedRows,
5
5
  type NativeScrollbackLiveRegion,
6
6
  type RenderStablePrefix,
7
+ type ViewportTailProvider,
7
8
  } from "@oh-my-pi/pi-tui";
8
9
 
9
10
  const kSnapshot = Symbol("transcript.liveDiffSnapshot");
@@ -139,6 +140,8 @@ interface BlockSegment {
139
140
  }
140
141
 
141
142
  const EMPTY_SEGMENTS: BlockSegment[] = [];
143
+ /** Shared empty result for an empty viewport-tail render (no allocation). */
144
+ const EMPTY_TAIL: readonly string[] = [];
142
145
 
143
146
  interface LiveCommitState {
144
147
  appendOnly: boolean;
@@ -415,7 +418,7 @@ function deriveLiveCommitState(
415
418
  */
416
419
  export class TranscriptContainer
417
420
  extends Container
418
- implements NativeScrollbackLiveRegion, NativeScrollbackCommittedRows, RenderStablePrefix
421
+ implements NativeScrollbackLiveRegion, NativeScrollbackCommittedRows, RenderStablePrefix, ViewportTailProvider
419
422
  {
420
423
  // Bumped to retire every block's diff snapshot at once (theme change /
421
424
  // clear); a snapshot is only honored when its stored generation matches.
@@ -432,6 +435,14 @@ export class TranscriptContainer
432
435
  // until it re-earns append-only via VOLATILE_REARM_FRAMES clean frames;
433
436
  // the engine then backfills the stalled gap.
434
437
  #nativeScrollbackCommitSafeEnd: number | undefined;
438
+ // Local line index up to which the leading run of live blocks is DURABLE: a
439
+ // commit-stable block's full body is permanent content even while its interior
440
+ // rows re-lay-out (a streaming markdown table re-aligning columns), so the
441
+ // engine must append their scroll-off snapshot rather than drop it. Reported
442
+ // separately from the byte-stable commit-safe end because these rows may still
443
+ // drift after commit; the engine commits them audit-exempt. Provisional
444
+ // (commit-unstable) blocks never extend it.
445
+ #nativeScrollbackSnapshotSafeEnd: number | undefined;
435
446
  // Persistent assembled transcript rows. Rows before the stable floor are
436
447
  // byte-identical to the previous render; rows at/after it were re-pushed.
437
448
  #lines: string[] = [];
@@ -476,6 +487,10 @@ export class TranscriptContainer
476
487
  return this.#nativeScrollbackCommitSafeEnd;
477
488
  }
478
489
 
490
+ getNativeScrollbackSnapshotSafeEnd(): number | undefined {
491
+ return this.#nativeScrollbackSnapshotSafeEnd;
492
+ }
493
+
479
494
  /**
480
495
  * Whether `component` sits below a still-mutating block — i.e. inside the
481
496
  * live region, where its rows cannot have been committed to native
@@ -520,10 +535,54 @@ export class TranscriptContainer
520
535
  return index === children.length - 1;
521
536
  }
522
537
 
538
+ /**
539
+ * Render only the bottom `maxRows` rows of the transcript at `width`, walking
540
+ * blocks from the last toward the first and stopping the instant enough rows
541
+ * are collected — blocks above the fold are never rendered. The engine's
542
+ * resize viewport fast path uses this so a drag (a SIGWINCH burst, each event
543
+ * a fresh width that misses every per-width cache) re-lays-out only the
544
+ * handful of visible blocks instead of the whole history every event.
545
+ *
546
+ * State-isolated by contract: touches none of the persistent full-compose
547
+ * fields (#lines, #segments, the per-block diff snapshots, the commit/stable
548
+ * bookkeeping), so the authoritative full render on settle reconciles exactly
549
+ * as if this never ran. Calling each block's render() still warms its own
550
+ * per-width cache, which that settle render then reuses for free.
551
+ *
552
+ * Consecutive visible blocks are joined by exactly one blank separator, the
553
+ * same rule render() applies, so the result equals the bottom of a full
554
+ * render except for an at-most-one-row separator on the topmost included
555
+ * block — a transient discrepancy the settle paint overwrites.
556
+ */
557
+ renderViewportTail(width: number, maxRows: number): readonly string[] {
558
+ width = Math.max(1, width);
559
+ if (maxRows <= 0) return EMPTY_TAIL;
560
+ const collected: (readonly string[])[] = [];
561
+ let total = 0;
562
+ for (let i = this.children.length - 1; i >= 0 && total < maxRows; i--) {
563
+ const contribution = stripPlainBlankEdges(this.children[i]!.render(width));
564
+ if (contribution.length === 0) continue;
565
+ // One blank separator sits between this block and the (already
566
+ // collected) visible block below it.
567
+ if (collected.length > 0) total += 1;
568
+ collected.push(contribution);
569
+ total += contribution.length;
570
+ }
571
+ if (collected.length === 0) return EMPTY_TAIL;
572
+ const rows: string[] = [];
573
+ for (let k = collected.length - 1; k >= 0; k--) {
574
+ if (rows.length > 0) rows.push("");
575
+ const body = collected[k]!;
576
+ for (let j = 0; j < body.length; j++) rows.push(body[j]!);
577
+ }
578
+ return rows.length > maxRows ? rows.slice(rows.length - maxRows) : rows;
579
+ }
580
+
523
581
  override render(width: number): readonly string[] {
524
582
  width = Math.max(1, width);
525
583
  this.#nativeScrollbackLiveRegionStart = undefined;
526
584
  this.#nativeScrollbackCommitSafeEnd = undefined;
585
+ this.#nativeScrollbackSnapshotSafeEnd = undefined;
527
586
 
528
587
  const count = this.children.length;
529
588
 
@@ -696,6 +755,16 @@ export class TranscriptContainer
696
755
  if (safeLength > 0) {
697
756
  this.#nativeScrollbackCommitSafeEnd = blockStart + safeLength;
698
757
  }
758
+ // Durable snapshot end: a commit-stable block's whole body is durable
759
+ // content — its scrolled-off rows are permanent even while interior
760
+ // rows re-lay-out (a streaming table re-aligning columns), so the
761
+ // engine must commit their snapshot on scroll-off rather than drop it.
762
+ // Finalized blocks are wholly durable; provisional (commit-unstable)
763
+ // blocks offer nothing beyond their byte-stable safe length.
764
+ const snapshotLength = finalized || isBlockCommitStable(child) ? contribution.length : safeLength;
765
+ if (snapshotLength > 0) {
766
+ this.#nativeScrollbackSnapshotSafeEnd = blockStart + snapshotLength;
767
+ }
699
768
  // A finalized, fully safe block may let the contiguous safe run extend
700
769
  // into blocks rendered below it. A still-live block keeps pushing lower
701
770
  // rows around as it grows, so the run closes there.
@@ -15,7 +15,7 @@ import {
15
15
  import type { TreeFilterMode } from "../../config/settings-schema";
16
16
  import { theme } from "../../modes/theme/theme";
17
17
  import { matchesAppInterrupt, matchesSelectDown, matchesSelectUp } from "../../modes/utils/keybinding-matchers";
18
- import type { SessionTreeNode } from "../../session/session-manager";
18
+ import type { SessionTreeNode } from "../../session/session-entries";
19
19
  import { shortenPath } from "../../tools/render-utils";
20
20
  import { toPathList } from "../../tools/search";
21
21
  import { DynamicBorder } from "./dynamic-border";
@@ -0,0 +1,18 @@
1
+ import type { Usage } from "@oh-my-pi/pi-ai";
2
+ import { Container, Spacer, Text } from "@oh-my-pi/pi-tui";
3
+ import { formatNumber } from "@oh-my-pi/pi-utils";
4
+ import { theme } from "../../modes/theme/theme";
5
+
6
+ export function createUsageRowBlock(usage: Usage): Container {
7
+ const totalInput = usage.input + usage.cacheWrite;
8
+ const parts: string[] = [];
9
+ parts.push(`${theme.icon.input} ${formatNumber(totalInput)}`);
10
+ parts.push(`${theme.icon.output} ${formatNumber(usage.output)}`);
11
+ if (usage.cacheRead > 0) {
12
+ parts.push(`cache: ${formatNumber(usage.cacheRead)}`);
13
+ }
14
+ const block = new Container();
15
+ block.addChild(new Spacer(1));
16
+ block.addChild(new Text(theme.fg("dim", parts.join(" ")), 1, 0));
17
+ return block;
18
+ }
@@ -4,9 +4,11 @@ import { imageReferenceHyperlink, renderPlaceholders } from "../image-references
4
4
  import { highlightMagicKeywords } from "../magic-keywords";
5
5
 
6
6
  // OSC 133 shell integration: marks prompt zones for terminal multiplexers
7
+ // Do not emit OSC 133 C ("command start") here: the transcript has no matching
8
+ // command-finished marker, so terminals can group later assistant/tool output
9
+ // under the first submitted prompt.
7
10
  const OSC133_ZONE_START = "\x1b]133;A\x07";
8
11
  const OSC133_ZONE_END = "\x1b]133;B\x07";
9
- const OSC133_ZONE_FINAL = "\x1b]133;C\x07";
10
12
 
11
13
  /**
12
14
  * Component that renders a user message
@@ -58,7 +60,7 @@ export class UserMessageComponent extends Container {
58
60
  }
59
61
  const wrapped = lines.slice();
60
62
  wrapped[0] = OSC133_ZONE_START + wrapped[0];
61
- wrapped[wrapped.length - 1] = wrapped[wrapped.length - 1] + OSC133_ZONE_END + OSC133_ZONE_FINAL;
63
+ wrapped[wrapped.length - 1] = wrapped[wrapped.length - 1] + OSC133_ZONE_END;
62
64
  this.#zoneSource = lines;
63
65
  this.#zoneLines = wrapped;
64
66
  return wrapped;
@@ -38,7 +38,7 @@ import { buildHotkeysMarkdown } from "../../modes/utils/hotkeys-markdown";
38
38
  import { buildToolsMarkdown } from "../../modes/utils/tools-markdown";
39
39
  import type { AsyncJobSnapshotItem } from "../../session/agent-session";
40
40
  import type { AuthStorage, OAuthAccountIdentity } from "../../session/auth-storage";
41
- import type { NewSessionOptions } from "../../session/session-manager";
41
+ import type { NewSessionOptions } from "../../session/session-entries";
42
42
  import { formatShakeSummary, type ShakeMode, type ShakeResult } from "../../session/shake-types";
43
43
  import { limitMatchesActiveAccount } from "../../slash-commands/helpers/active-oauth-account";
44
44
  import { outputMeta } from "../../tools/output-meta";
@@ -965,7 +965,10 @@ export class CommandController {
965
965
  this.ctx.ui.requestRender();
966
966
  }
967
967
 
968
- async handleCompactCommand(customInstructions?: string): Promise<CompactionOutcome> {
968
+ async handleCompactCommand(
969
+ customInstructions?: string,
970
+ beforeFlush?: (outcome: CompactionOutcome) => void | Promise<void>,
971
+ ): Promise<CompactionOutcome> {
969
972
  const entries = this.ctx.sessionManager.getEntries();
970
973
  const messageCount = entries.filter(e => e.type === "message").length;
971
974
 
@@ -974,7 +977,7 @@ export class CommandController {
974
977
  return "ok";
975
978
  }
976
979
 
977
- return this.executeCompaction(customInstructions, false);
980
+ return this.executeCompaction(customInstructions, false, beforeFlush);
978
981
  }
979
982
 
980
983
  /**
@@ -1019,6 +1022,7 @@ export class CommandController {
1019
1022
  async executeCompaction(
1020
1023
  customInstructionsOrOptions?: string | CompactOptions,
1021
1024
  isAuto = false,
1025
+ beforeFlush?: (outcome: CompactionOutcome) => void | Promise<void>,
1022
1026
  ): Promise<CompactionOutcome> {
1023
1027
  if (this.ctx.loadingAnimation) {
1024
1028
  this.ctx.loadingAnimation.stop();
@@ -1026,12 +1030,6 @@ export class CommandController {
1026
1030
  }
1027
1031
  this.ctx.statusContainer.clear();
1028
1032
 
1029
- const originalOnEscape = this.ctx.editor.onEscape;
1030
- this.ctx.editor.onEscape = () => {
1031
- this.ctx.session.abortCompaction();
1032
- };
1033
-
1034
- this.ctx.chatContainer.addChild(new Spacer(1));
1035
1033
  const label = isAuto ? "Auto-compacting context... (esc to cancel)" : "Compacting context... (esc to cancel)";
1036
1034
  const compactingLoader = new Loader(
1037
1035
  this.ctx.ui,
@@ -1052,6 +1050,8 @@ export class CommandController {
1052
1050
  : undefined;
1053
1051
  await this.ctx.session.compact(instructions, options);
1054
1052
 
1053
+ compactingLoader.stop();
1054
+ this.ctx.statusContainer.clear();
1055
1055
  this.ctx.rebuildChatFromMessages();
1056
1056
 
1057
1057
  this.ctx.statusLine.invalidate();
@@ -1068,8 +1068,12 @@ export class CommandController {
1068
1068
  } finally {
1069
1069
  compactingLoader.stop();
1070
1070
  this.ctx.statusContainer.clear();
1071
- this.ctx.editor.onEscape = originalOnEscape;
1072
1071
  }
1072
+ // Run the caller's pre-flush hook (e.g. the plan-approval model transition)
1073
+ // before queued user input is dispatched, so any turn queued during
1074
+ // compaction executes on the post-compaction model rather than the model
1075
+ // compaction itself ran on.
1076
+ if (beforeFlush) await beforeFlush(outcome);
1073
1077
  await this.ctx.flushCompactionQueue({ willRetry: false });
1074
1078
  return outcome;
1075
1079
  }
@@ -1089,11 +1093,6 @@ export class CommandController {
1089
1093
  }
1090
1094
  this.ctx.statusContainer.clear();
1091
1095
 
1092
- const originalOnEscape = this.ctx.editor.onEscape;
1093
- this.ctx.editor.onEscape = () => {
1094
- this.ctx.session.abortHandoff();
1095
- };
1096
-
1097
1096
  const handoffLoader = new Loader(
1098
1097
  this.ctx.ui,
1099
1098
  spinner => theme.fg("accent", spinner),
@@ -1138,7 +1137,6 @@ export class CommandController {
1138
1137
  } finally {
1139
1138
  handoffLoader.stop();
1140
1139
  this.ctx.statusContainer.clear();
1141
- this.ctx.editor.onEscape = originalOnEscape;
1142
1140
  }
1143
1141
  this.ctx.ui.requestRender();
1144
1142
  }
@@ -2,6 +2,7 @@ import { INTENT_FIELD } from "@oh-my-pi/pi-agent-core";
2
2
  import { calculatePromptTokens } from "@oh-my-pi/pi-agent-core/compaction/compaction";
3
3
  import type { AssistantMessage, ImageContent } from "@oh-my-pi/pi-ai";
4
4
  import { type Component, Loader, TERMINAL } from "@oh-my-pi/pi-tui";
5
+ import { extractTextContent } from "../../commit/utils";
5
6
  import { settings } from "../../config/settings";
6
7
  import { getFileSnapshotStore } from "../../edit/file-snapshot-store";
7
8
  import { AssistantMessageComponent } from "../../modes/components/assistant-message";
@@ -13,12 +14,15 @@ import {
13
14
  import { TodoReminderComponent } from "../../modes/components/todo-reminder";
14
15
  import { ToolExecutionComponent } from "../../modes/components/tool-execution";
15
16
  import { TtsrNotificationComponent } from "../../modes/components/ttsr-notification";
17
+ import { createUsageRowBlock } from "../../modes/components/usage-row";
16
18
  import { getSymbolTheme, theme } from "../../modes/theme/theme";
17
19
  import type { InteractiveModeContext, TodoPhase } from "../../modes/types";
18
20
  import type { PlanApprovalDetails } from "../../plan-mode/approved-plan";
19
21
  import type { AgentSessionEvent } from "../../session/agent-session";
20
- import { isSilentAbort, readPendingDisplayTag, resolveAbortLabel } from "../../session/messages";
22
+ import { isSilentAbort, readQueueChipText, resolveAbortLabel } from "../../session/messages";
21
23
  import type { ResolveToolDetails } from "../../tools/resolve";
24
+ import { vocalizer } from "../../tts/vocalizer";
25
+ import { hasVisibleThinking } from "../../utils/thinking-display";
22
26
  import { interruptHint } from "../shared";
23
27
  import { StreamingRevealController } from "./streaming-reveal";
24
28
  import { ToolArgsRevealController } from "./tool-args-reveal";
@@ -37,16 +41,6 @@ const IRC_MESSAGE_VISIBLE_TTL_MS = 10_000;
37
41
  */
38
42
  const MAX_LIVE_IRC_CARDS = 4;
39
43
 
40
- /**
41
- * Loader label shown the instant a user interrupt (Esc) is requested, kept until
42
- * the agent turn fully unwinds. Esc fires the abort synchronously, but the loop
43
- * only stops the spinner at `agent_end`, which it cannot reach until every
44
- * in-flight tool settles its abort in `executeToolCalls` (`Promise.allSettled`).
45
- * Swapping the steady "Working…" for this acknowledges the keypress instead of
46
- * reading as an ignored Esc for the seconds a slow tool takes to tear down.
47
- */
48
- export const INTERRUPTING_WORKING_MESSAGE = "Interrupting…";
49
-
50
44
  type AgentSessionEventHandlers = {
51
45
  [E in AgentSessionEventKind]: (event: Extract<AgentSessionEvent, { type: E }>) => Promise<void>;
52
46
  };
@@ -63,8 +57,6 @@ export class EventController {
63
57
  #renderedCustomMessages = new Set<string>();
64
58
  #lastIntent: string | undefined = undefined;
65
59
  #backgroundToolCallIds = new Set<string>();
66
- #agentTurnActive = false;
67
- #interrupting = false;
68
60
  #readToolCallArgs = new Map<string, Record<string, unknown>>();
69
61
  #readToolCallAssistantComponents = new Map<string, AssistantMessageComponent>();
70
62
  #lastAssistantComponent: AssistantMessageComponent | undefined = undefined;
@@ -103,8 +95,8 @@ export class EventController {
103
95
  this.#handlers = {
104
96
  agent_start: e => this.#handleAgentStart(e),
105
97
  agent_end: e => this.#handleAgentEnd(e),
106
- turn_start: async () => {},
107
- turn_end: async () => {},
98
+ turn_start: async () => this.#handleTurnStart(),
99
+ turn_end: async e => this.#handleTurnEnd(e),
108
100
  message_start: e => this.#handleMessageStart(e),
109
101
  message_update: e => this.#handleMessageUpdate(e),
110
102
  message_end: e => this.#handleMessageEnd(e),
@@ -193,7 +185,7 @@ export class EventController {
193
185
  return true;
194
186
  }
195
187
  #updateWorkingMessageFromIntent(intent: unknown): void {
196
- if (this.#interrupting) return;
188
+ if (this.ctx.session.isAborting) return;
197
189
  // Streamed JSON can deliver non-string `_i` (object, number, boolean) before
198
190
  // schema validation; `?.` only guards null/undefined, so guard the type too.
199
191
  if (typeof intent !== "string") return;
@@ -203,19 +195,6 @@ export class EventController {
203
195
  this.ctx.setWorkingMessage(`${trimmed}${interruptHint()}`);
204
196
  }
205
197
 
206
- /**
207
- * Acknowledge a user interrupt (Esc) immediately: switch the loader to
208
- * `INTERRUPTING_WORKING_MESSAGE` and freeze intent-driven working-message
209
- * updates for the rest of the turn so a late `tool_execution_start` intent
210
- * cannot repaint a "Working…/<intent>" line over the acknowledgment. Reset at
211
- * the next `agent_start`. No-op outside an active turn or if already set.
212
- */
213
- notifyInterrupting(): void {
214
- if (!this.#agentTurnActive || this.#interrupting) return;
215
- this.#interrupting = true;
216
- this.ctx.setWorkingMessage(INTERRUPTING_WORKING_MESSAGE);
217
- }
218
-
219
198
  subscribeToAgent(): void {
220
199
  this.ctx.unsubscribe = this.ctx.session.subscribe(async (event: AgentSessionEvent) => {
221
200
  await this.handleEvent(event);
@@ -233,8 +212,6 @@ export class EventController {
233
212
  this.#renderedCustomMessages.clear();
234
213
  this.#lastIntent = undefined;
235
214
  this.#backgroundToolCallIds.clear();
236
- this.#agentTurnActive = false;
237
- this.#interrupting = false;
238
215
  this.#readToolCallArgs.clear();
239
216
  this.#readToolCallAssistantComponents.clear();
240
217
  this.#lastAssistantComponent = undefined;
@@ -264,8 +241,6 @@ export class EventController {
264
241
  }
265
242
 
266
243
  async #handleAgentStart(_event: Extract<AgentSessionEvent, { type: "agent_start" }>): Promise<void> {
267
- this.#agentTurnActive = true;
268
- this.#interrupting = false;
269
244
  this.#lastIntent = undefined;
270
245
  this.#readToolCallArgs.clear();
271
246
  this.#readToolCallAssistantComponents.clear();
@@ -276,10 +251,6 @@ export class EventController {
276
251
  this.#pinnedErrorComponent?.setErrorPinned(false);
277
252
  this.#pinnedErrorComponent = undefined;
278
253
  this.ctx.clearPinnedError();
279
- if (this.ctx.retryEscapeHandler) {
280
- this.ctx.editor.onEscape = this.ctx.retryEscapeHandler;
281
- this.ctx.retryEscapeHandler = undefined;
282
- }
283
254
  if (this.ctx.retryLoader) {
284
255
  this.ctx.retryLoader.stop();
285
256
  this.ctx.retryLoader = undefined;
@@ -299,15 +270,10 @@ export class EventController {
299
270
  this.#renderedCustomMessages.add(signature);
300
271
  this.#resetReadGroup();
301
272
  this.ctx.addMessageToChat(event.message);
302
- // Tag-keyed pending-bar refresh: when AgentSession.#handleAgentEvent
303
- // spliced this dequeued custom message out of #steeringMessages /
304
- // #followUpMessages (it ran before this emit), the array state is
305
- // already correct pendingMessagesContainer just needs to be
306
- // re-rendered to match. Gated on tag presence so non-queued customs
307
- // (ttsr-injection, irc:*, async-result, hookMessage) skip the
308
- // rebuild; their dispatch path never registered a pending chip.
309
- // Mirrors the user-role refresh at the bottom of this function.
310
- if (event.message.role === "custom" && readPendingDisplayTag(event.message.details)) {
273
+ // Queued custom-message chips are derived from the agent queue; refresh the
274
+ // pending bar when the queued custom is consumed so the chip disappears
275
+ // immediately.
276
+ if (event.message.role === "custom" && readQueueChipText(event.message.details)) {
311
277
  this.ctx.updatePendingMessagesDisplay();
312
278
  }
313
279
  this.ctx.ui.requestRender();
@@ -467,7 +433,47 @@ export class EventController {
467
433
  }
468
434
  }
469
435
 
436
+ /** A new turn interrupts any speech still queued/playing from the previous one. */
437
+ #handleTurnStart(): void {
438
+ vocalizer.clear();
439
+ }
440
+
441
+ /**
442
+ * Speak streamed assistant output as a side effect of the turn. The mode
443
+ * decides which deltas feed the vocalizer (the vocalizer re-checks enabled):
444
+ * assistant|all speak text; all also speaks thinking; yield speaks nothing
445
+ * live (the final message is spoken at turn end).
446
+ */
447
+ #vocalizeDelta(event: Extract<AgentSessionEvent, { type: "message_update" }>): void {
448
+ if (!settings.get("speech.enabled")) return;
449
+ const mode = settings.get("speech.mode");
450
+ const delta = event.assistantMessageEvent;
451
+ if (delta.type === "text_delta" && (mode === "assistant" || mode === "all")) {
452
+ vocalizer.pushDelta(delta.delta);
453
+ } else if (delta.type === "thinking_delta" && mode === "all") {
454
+ vocalizer.pushDelta(delta.delta);
455
+ }
456
+ }
457
+
458
+ /**
459
+ * End-of-turn vocalization: yield mode speaks the final assistant message in
460
+ * one shot here (the only mode that is post-hoc); every other mode just makes
461
+ * sure the live buffer's trailing partial gets flushed.
462
+ */
463
+ #handleTurnEnd(event: Extract<AgentSessionEvent, { type: "turn_end" }>): void {
464
+ if (!settings.get("speech.enabled")) return;
465
+ if (settings.get("speech.mode") !== "yield") {
466
+ vocalizer.flush();
467
+ return;
468
+ }
469
+ if (event.message.role !== "assistant") return;
470
+ if (event.message.stopReason === "aborted") return; // interrupted: never speak the aborted partial
471
+ const text = extractTextContent(event.message);
472
+ if (text) vocalizer.speak(text);
473
+ }
474
+
470
475
  async #handleMessageUpdate(event: Extract<AgentSessionEvent, { type: "message_update" }>): Promise<void> {
476
+ this.#vocalizeDelta(event);
471
477
  if (this.ctx.streamingComponent && event.message.role === "assistant") {
472
478
  this.ctx.streamingMessage = event.message;
473
479
  this.#streamingReveal.setTarget(this.ctx.streamingMessage);
@@ -475,7 +481,7 @@ export class EventController {
475
481
  const visibleBlockCount = this.ctx.streamingMessage.content.filter(
476
482
  content =>
477
483
  (content.type === "text" && content.text.trim().length > 0) ||
478
- (content.type === "thinking" && content.thinking.trim().length > 0),
484
+ (content.type === "thinking" && hasVisibleThinking(content)),
479
485
  ).length;
480
486
  if (visibleBlockCount > this.#lastVisibleBlockCount) {
481
487
  this.#resetReadGroup();
@@ -491,14 +497,7 @@ export class EventController {
491
497
  // stream (a big write/edit/eval) sits below a still-live block and
492
498
  // can never reach native scrollback: the head of the preview is
493
499
  // neither committed nor on screen and the transcript reads as cut.
494
- // Skipped when the per-turn usage row is enabled: that row is only
495
- // known at message_end and appends to this block, which would shift
496
- // committed tool rows below it every turn (audit recommit →
497
- // duplicated preview copies in scrollback).
498
- if (
499
- this.ctx.streamingMessage.content.some(content => content.type === "toolCall") &&
500
- !settings.get("display.showTokenUsage")
501
- ) {
500
+ if (this.ctx.streamingMessage.content.some(content => content.type === "toolCall")) {
502
501
  this.ctx.streamingComponent.markTranscriptBlockFinalized();
503
502
  }
504
503
  for (const content of this.ctx.streamingMessage.content) {
@@ -603,6 +602,17 @@ export class EventController {
603
602
 
604
603
  async #handleMessageEnd(event: Extract<AgentSessionEvent, { type: "message_end" }>): Promise<void> {
605
604
  if (event.message.role === "user") return;
605
+ if (event.message.role === "assistant" && settings.get("speech.enabled")) {
606
+ if (event.message.stopReason === "aborted") {
607
+ // Esc / Ctrl+C / interrupt: stop speaking now and drop the trailing partial.
608
+ vocalizer.clear();
609
+ } else {
610
+ const mode = settings.get("speech.mode");
611
+ // Speak the last partial sentence of a completed message; yield mode
612
+ // instead speaks the whole final message at turn end.
613
+ if (mode === "assistant" || mode === "all") vocalizer.flush();
614
+ }
615
+ }
606
616
  if (this.ctx.streamingComponent && event.message.role === "assistant") {
607
617
  this.ctx.streamingMessage = event.message;
608
618
  this.#streamingReveal.stop();
@@ -651,8 +661,10 @@ export class EventController {
651
661
  this.#resolveDisplaceablePoll();
652
662
  }
653
663
  this.#lastAssistantComponent = this.ctx.streamingComponent;
654
- this.#lastAssistantComponent.setUsageInfo(event.message.usage);
655
664
  this.#lastAssistantComponent.markTranscriptBlockFinalized();
665
+ if (settings.get("display.showTokenUsage")) {
666
+ this.ctx.chatContainer.addChild(createUsageRowBlock(event.message.usage));
667
+ }
656
668
  this.ctx.streamingComponent = undefined;
657
669
  this.ctx.streamingMessage = undefined;
658
670
  // Pin a turn-ending provider error (e.g. Anthropic content-filter block)
@@ -823,7 +835,21 @@ export class EventController {
823
835
  }
824
836
  }
825
837
  async #handleAgentEnd(_event: Extract<AgentSessionEvent, { type: "agent_end" }>): Promise<void> {
826
- this.#agentTurnActive = false;
838
+ // A superseded agent_end: the agent is already streaming a fresh turn, so
839
+ // this event belongs to a turn that has already been replaced. The session
840
+ // dispatches to listeners fire-and-forget across an async extension-emit hop
841
+ // (#emitSessionEvent), so an interrupted turn's agent_end can land AFTER the
842
+ // resumed turn's agent_start (e.g. any post-turn agent.continue()). Running
843
+ // the turn-end teardown now would stop the loader the live turn just created,
844
+ // leaving "Working…" gone while the agent keeps running. The live turn owns
845
+ // the loader and finalizes it at its own agent_end (isStreaming === false by
846
+ // then). Mirrors the collab guest's !isStreaming loader reconciler.
847
+ if (this.ctx.session.isStreaming) return;
848
+
849
+ await this.#finishAgentEnd();
850
+ }
851
+
852
+ async #finishAgentEnd(): Promise<void> {
827
853
  this.#streamingReveal.stop();
828
854
  this.#toolArgsReveal.flushAll();
829
855
  if (this.ctx.loadingAnimation) {
@@ -867,14 +893,27 @@ export class EventController {
867
893
  this.sendCompletionNotification();
868
894
  }
869
895
 
896
+ /**
897
+ * Tear down the live "Working…" loader: stop its animation timer AND clear the
898
+ * reference. A transient overlay (auto-compaction / auto-retry) that only ran
899
+ * `statusContainer.clear()` detached the loader from the container but left
900
+ * `ctx.loadingAnimation` set, so the resumed turn's `agent_start` →
901
+ * `ensureLoadingAnimation()` (guarded by `if (!this.loadingAnimation)`) skipped
902
+ * re-adding it and the spinner vanished while the agent kept streaming. Nulling
903
+ * the reference here lets the next `agent_start` recreate and re-attach it.
904
+ */
905
+ #stopWorkingLoader(): void {
906
+ if (this.ctx.loadingAnimation) {
907
+ this.ctx.loadingAnimation.stop();
908
+ this.ctx.loadingAnimation = undefined;
909
+ }
910
+ }
911
+
870
912
  async #handleAutoCompactionStart(
871
913
  event: Extract<AgentSessionEvent, { type: "auto_compaction_start" }>,
872
914
  ): Promise<void> {
873
915
  this.#cancelIdleCompaction();
874
- this.ctx.autoCompactionEscapeHandler = this.ctx.editor.onEscape;
875
- this.ctx.editor.onEscape = () => {
876
- this.ctx.viewSession.abortCompaction();
877
- };
916
+ this.#stopWorkingLoader();
878
917
  this.ctx.statusContainer.clear();
879
918
  const reasonText =
880
919
  event.reason === "overflow"
@@ -903,10 +942,6 @@ export class EventController {
903
942
 
904
943
  async #handleAutoCompactionEnd(event: Extract<AgentSessionEvent, { type: "auto_compaction_end" }>): Promise<void> {
905
944
  this.#cancelIdleCompaction();
906
- if (this.ctx.autoCompactionEscapeHandler) {
907
- this.ctx.editor.onEscape = this.ctx.autoCompactionEscapeHandler;
908
- this.ctx.autoCompactionEscapeHandler = undefined;
909
- }
910
945
  if (this.ctx.autoCompactionLoader) {
911
946
  this.ctx.autoCompactionLoader.stop();
912
947
  this.ctx.autoCompactionLoader = undefined;
@@ -964,10 +999,7 @@ export class EventController {
964
999
  }
965
1000
 
966
1001
  async #handleAutoRetryStart(event: Extract<AgentSessionEvent, { type: "auto_retry_start" }>): Promise<void> {
967
- this.ctx.retryEscapeHandler = this.ctx.editor.onEscape;
968
- this.ctx.editor.onEscape = () => {
969
- this.ctx.viewSession.abortRetry();
970
- };
1002
+ this.#stopWorkingLoader();
971
1003
  this.ctx.statusContainer.clear();
972
1004
  const delaySeconds = Math.round(event.delayMs / 1000);
973
1005
  this.ctx.retryLoader = new Loader(
@@ -982,10 +1014,6 @@ export class EventController {
982
1014
  }
983
1015
 
984
1016
  async #handleAutoRetryEnd(event: Extract<AgentSessionEvent, { type: "auto_retry_end" }>): Promise<void> {
985
- if (this.ctx.retryEscapeHandler) {
986
- this.ctx.editor.onEscape = this.ctx.retryEscapeHandler;
987
- this.ctx.retryEscapeHandler = undefined;
988
- }
989
1017
  if (this.ctx.retryLoader) {
990
1018
  this.ctx.retryLoader.stop();
991
1019
  this.ctx.retryLoader = undefined;
@@ -17,6 +17,7 @@ import type {
17
17
  TerminalInputHandler,
18
18
  } from "../../extensibility/extensions";
19
19
  import { getSessionSlashCommands } from "../../extensibility/extensions/get-commands-handler";
20
+ import { createExtensionModelQuery } from "../../extensibility/extensions/model-api";
20
21
  import { HookEditorComponent } from "../../modes/components/hook-editor";
21
22
  import { HookInputComponent } from "../../modes/components/hook-input";
22
23
  import { HookSelectorComponent, type HookSelectorSlider } from "../../modes/components/hook-selector";
@@ -491,6 +492,11 @@ export class ExtensionUiController {
491
492
  sessionManager: this.ctx.session.sessionManager,
492
493
  modelRegistry: this.ctx.session.modelRegistry,
493
494
  model: this.ctx.session.model,
495
+ models: createExtensionModelQuery(
496
+ this.ctx.session.modelRegistry,
497
+ this.ctx.session.settings,
498
+ () => this.ctx.session.model,
499
+ ),
494
500
  isIdle: () => !this.ctx.session.isStreaming,
495
501
  hasPendingMessages: () => this.ctx.session.queuedMessageCount > 0,
496
502
  abort: () => {