@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
@@ -1,13 +1,15 @@
1
1
  import * as fs from "node:fs/promises";
2
+ import * as path from "node:path";
2
3
  import type { ImageContent } from "@oh-my-pi/pi-ai";
3
4
  import { type AutocompleteProvider, matchesKey, type SlashCommand } from "@oh-my-pi/pi-tui";
4
5
  import { $env, isEnoent, logger, sanitizeText } from "@oh-my-pi/pi-utils";
5
6
  import { isSettingsInitialized, settings } from "../../config/settings";
7
+ import { resolveLocalRoot } from "../../internal-urls";
6
8
  import { AssistantMessageComponent } from "../../modes/components/assistant-message";
7
9
  import { renderSegmentTrack } from "../../modes/components/segment-track";
8
10
  import { TinyTitleDownloadProgressComponent } from "../../modes/components/tiny-title-download-progress";
9
11
  import { expandEmoticons } from "../../modes/emoji-autocomplete";
10
- import { materializeImageReferenceLinks } from "../../modes/image-references";
12
+ import { materializeImageReferenceLinks, shiftImageMarkers } from "../../modes/image-references";
11
13
  import { createPromptActionAutocompleteProvider } from "../../modes/prompt-action-autocomplete";
12
14
  import type { InteractiveModeContext } from "../../modes/types";
13
15
  import manualContinuePrompt from "../../prompts/system/manual-continue.md" with { type: "text" };
@@ -42,12 +44,44 @@ function hasPasteText(value: unknown): value is PasteTarget {
42
44
  return typeof value === "object" && value !== null && typeof (value as PasteTarget).pasteText === "function";
43
45
  }
44
46
 
47
+ /** Wrap pasted text in a fenced code block, using a backtick fence longer than any run of
48
+ * backticks already in the content so an embedded fence cannot terminate the block early. */
49
+ function wrapPasteInCodeBlock(content: string): string {
50
+ let longestRun = 0;
51
+ let run = 0;
52
+ for (let i = 0; i < content.length; i++) {
53
+ if (content.charCodeAt(i) === 96 /* backtick */) {
54
+ run++;
55
+ if (run > longestRun) longestRun = run;
56
+ } else {
57
+ run = 0;
58
+ }
59
+ }
60
+ const fence = "`".repeat(Math.max(3, longestRun + 1));
61
+ return `${fence}\n${content}\n${fence}`;
62
+ }
63
+
64
+ /** Wrap pasted text in `<pasted_text>` tags so the model treats it as one quoted block. */
65
+ function wrapPasteInXml(content: string): string {
66
+ return `<pasted_text>\n${content}\n</pasted_text>`;
67
+ }
68
+
45
69
  const TINY_TITLE_PROGRESS_DONE_TTL_MS = 3_000;
46
70
  // A cached model fires its file-load events in a short burst and then goes silent
47
71
  // while onnxruntime builds the session; a genuine download keeps streaming progress
48
72
  // events for seconds. Only reveal the bar once a still-incomplete event arrives after
49
73
  // this grace window, so an already-downloaded model never flashes the bar.
50
74
  const TINY_TITLE_PROGRESS_REVEAL_DELAY_MS = 1_000;
75
+ // Double-tap ← on an empty editor opens the Agent Hub (and, in a focused
76
+ // subagent view, ←← returns to the main session). The second tap must land
77
+ // inside this window. The lower bound rejects terminal-synthesized arrow-key
78
+ // bursts: "click to move cursor" / pointer features in iTerm2, WezTerm, kitty,
79
+ // and tmux emit several arrow keys in a single stdin read (sub-millisecond
80
+ // apart) on a stray click, which used to pop the hub with no key ever pressed.
81
+ // Three or more rapid taps are likewise treated as a burst, not a gesture. A
82
+ // deliberate human double-tap is always tens of milliseconds apart.
83
+ const LEFT_DOUBLE_TAP_MIN_GAP_MS = 40;
84
+ const LEFT_DOUBLE_TAP_MAX_GAP_MS = 500;
51
85
 
52
86
  export class InputController {
53
87
  constructor(
@@ -61,6 +95,13 @@ export class InputController {
61
95
 
62
96
  #enhancedPaste?: EnhancedPasteController;
63
97
  #focusedLeftTapListenerInstalled = false;
98
+ // Tap counter for the double-← gesture; reset whenever a quiet gap
99
+ // (>= LEFT_DOUBLE_TAP_MAX_GAP_MS) starts a fresh sequence. See
100
+ // #detectLeftDoubleTap.
101
+ #leftTapCount = 0;
102
+ // Sequential index for `local://attachment-N` references created by the large-paste "attach as
103
+ // file" action. Seeded from 0 and bumped past any existing attachment files in #attachPasteAsFile.
104
+ #attachmentCounter = 0;
64
105
 
65
106
  #showTinyTitleDownloadProgress(modelKey: string): void {
66
107
  if (!isTinyTitleLocalModelKey(modelKey)) return;
@@ -120,10 +161,38 @@ export class InputController {
120
161
  });
121
162
  }
122
163
  this.ctx.editor.onEscape = () => {
164
+ // Active context maintenance owns Esc: auto/manual compaction,
165
+ // handoff generation, and auto-retry backoff all advertise
166
+ // "(esc to cancel)". Dispatch on live session state instead of
167
+ // swapping onEscape handlers — interleaved start/end events used
168
+ // to clobber the single saved-handler slot (auto-compaction start
169
+ // → /compact → auto end → manual finally), leaving Esc wired to a
170
+ // stale no-op closure until restart.
171
+ const viewSession = this.ctx.viewSession;
172
+ let aborted = false;
173
+ if (viewSession.isCompacting) {
174
+ try {
175
+ viewSession.abortCompaction();
176
+ } catch {}
177
+ aborted = true;
178
+ }
179
+ if (viewSession.isGeneratingHandoff) {
180
+ try {
181
+ viewSession.abortHandoff();
182
+ } catch {}
183
+ aborted = true;
184
+ }
185
+ if (viewSession.isRetrying) {
186
+ try {
187
+ viewSession.abortRetry();
188
+ } catch {}
189
+ aborted = true;
190
+ }
191
+ if (aborted) return;
192
+
123
193
  if (this.ctx.loopModeEnabled) {
124
194
  this.ctx.pauseLoop();
125
195
  if (this.ctx.session.isStreaming) {
126
- this.ctx.notifyInterrupting();
127
196
  void this.ctx.session.abort({ reason: USER_INTERRUPT_LABEL });
128
197
  } else {
129
198
  this.ctx.cancelPendingSubmission();
@@ -153,7 +222,6 @@ export class InputController {
153
222
  // session is never streaming, so the native abort path below would
154
223
  // no-op.
155
224
  if (this.ctx.collabGuest.state?.isStreaming || this.ctx.loadingAnimation) {
156
- if (!this.ctx.collabGuest.readOnly) this.ctx.notifyInterrupting();
157
225
  this.ctx.collabGuest.sendAbort();
158
226
  }
159
227
  return;
@@ -176,7 +244,6 @@ export class InputController {
176
244
  this.ctx.isPythonMode = false;
177
245
  this.ctx.updateEditorBorderColor();
178
246
  } else if (this.ctx.session.isStreaming) {
179
- this.ctx.notifyInterrupting();
180
247
  void this.ctx.session.abort({ reason: USER_INTERRUPT_LABEL });
181
248
  } else if (this.ctx.editor.getText().trim()) {
182
249
  // Esc with typed text clears the draft instead of (or before) any double-Esc action
@@ -244,6 +311,7 @@ export class InputController {
244
311
  this.ctx.keybindings.getKeys("app.clipboard.pasteTextRaw"),
245
312
  );
246
313
  this.ctx.editor.onPasteTextRaw = () => void this.handleClipboardTextRawPaste();
314
+ this.ctx.editor.onLargePaste = (text, lineCount) => this.handleLargePaste(text, lineCount);
247
315
  this.ctx.editor.setActionKeys(
248
316
  "app.clipboard.copyPrompt",
249
317
  this.ctx.keybindings.getKeys("app.clipboard.copyPrompt"),
@@ -279,6 +347,12 @@ export class InputController {
279
347
  for (const key of this.ctx.keybindings.getKeys("app.stt.toggle")) {
280
348
  this.ctx.editor.setCustomKeyHandler(key, () => void this.ctx.handleSTTToggle());
281
349
  }
350
+ // Hold the space bar to push-to-talk: the editor recognizes the auto-repeat burst, tracks
351
+ // the spam back out, and toggles STT on hold start / release. Gated on `stt.enabled` so a
352
+ // disabled STT leaves the space bar typing normally.
353
+ this.ctx.editor.sttHoldEnabled = () => settings.get("stt.enabled");
354
+ this.ctx.editor.onSpaceHoldStart = () => void this.ctx.handleSTTToggle();
355
+ this.ctx.editor.onSpaceHoldEnd = () => void this.ctx.handleSTTToggle();
282
356
  for (const key of this.ctx.keybindings.getKeys("app.clipboard.copyLine")) {
283
357
  this.ctx.editor.setCustomKeyHandler(key, () => this.handleCopyCurrentLine());
284
358
  }
@@ -292,18 +366,16 @@ export class InputController {
292
366
 
293
367
  // Double-tap left arrow on an empty editor: opens the agent hub from the
294
368
  // main session, or returns the focused subagent view to the main session.
295
- // Focused ←← intentionally matches Esc.
369
+ // Focused ←← intentionally matches Esc. From the main session the gesture
370
+ // stays inert when there are no subagents (requireContent); the explicit
371
+ // hub key still opens the empty roster.
296
372
  this.ctx.editor.onLeftAtStart = () => {
297
373
  if (this.ctx.focusedAgentId) {
298
374
  this.#handleFocusedLeftTap();
299
375
  return;
300
376
  }
301
- const now = Date.now();
302
- if (now - this.ctx.lastLeftTapTime < 500) {
303
- this.ctx.lastLeftTapTime = 0;
304
- this.ctx.showAgentHub();
305
- } else {
306
- this.ctx.lastLeftTapTime = now;
377
+ if (this.#detectLeftDoubleTap()) {
378
+ this.ctx.showAgentHub({ requireContent: true });
307
379
  }
308
380
  };
309
381
 
@@ -322,13 +394,37 @@ export class InputController {
322
394
  }
323
395
 
324
396
  #handleFocusedLeftTap(): void {
397
+ if (this.#detectLeftDoubleTap()) {
398
+ void this.ctx.unfocusSession();
399
+ }
400
+ }
401
+
402
+ /**
403
+ * Detect a deliberate double-← gesture, rejecting terminal-synthesized arrow
404
+ * bursts. Returns true only on the *second* tap of a fresh sequence when it
405
+ * lands a human-plausible interval after the first
406
+ * (`[LEFT_DOUBLE_TAP_MIN_GAP_MS, LEFT_DOUBLE_TAP_MAX_GAP_MS)`). Taps closer
407
+ * than the lower bound, or any third-and-later tap before a quiet gap, are a
408
+ * burst and never fire — so a stray click that makes the terminal emit a run
409
+ * of ← keys can no longer pop the Agent Hub.
410
+ */
411
+ #detectLeftDoubleTap(): boolean {
325
412
  const now = Date.now();
326
- if (now - this.ctx.lastLeftTapTime < 500) {
413
+ const sinceLast = now - this.ctx.lastLeftTapTime;
414
+ this.ctx.lastLeftTapTime = now;
415
+ if (sinceLast >= LEFT_DOUBLE_TAP_MAX_GAP_MS) {
416
+ // Quiet gap: this tap starts a fresh sequence.
417
+ this.#leftTapCount = 1;
418
+ return false;
419
+ }
420
+ this.#leftTapCount += 1;
421
+ if (this.#leftTapCount === 2 && sinceLast >= LEFT_DOUBLE_TAP_MIN_GAP_MS) {
422
+ // Exactly two taps, the second a human-plausible interval after the first.
423
+ this.#leftTapCount = 0;
327
424
  this.ctx.lastLeftTapTime = 0;
328
- void this.ctx.unfocusSession();
329
- } else {
330
- this.ctx.lastLeftTapTime = now;
425
+ return true;
331
426
  }
427
+ return false;
332
428
  }
333
429
 
334
430
  #setupEnhancedPaste(): void {
@@ -376,22 +472,16 @@ export class InputController {
376
472
  return;
377
473
  }
378
474
 
379
- // Empty submit while streaming with queued steering: interrupt now and
380
- // immediately resume so the visible `Steer:` entry is sent without
381
- // waiting for the current tool/model boundary.
475
+ // Empty submit while streaming with queued messages: abort the active
476
+ // turn and let the post-unwind drain deliver the agent-core queue.
382
477
  if (!text && this.ctx.session.isStreaming) {
383
- const queuedMessages = this.ctx.session.getQueuedMessages();
384
- if (queuedMessages.steering.length > 0) {
385
- await this.ctx.session.interruptAndFlushQueuedMessages({ reason: USER_INTERRUPT_LABEL });
478
+ if (this.ctx.session.queuedMessageCount > 0) {
479
+ const aborting = this.ctx.session.abort({ reason: USER_INTERRUPT_LABEL });
480
+ await aborting;
386
481
  this.ctx.updatePendingMessagesDisplay();
387
482
  this.ctx.ui.requestRender();
388
- return;
389
- }
390
- if (this.ctx.session.queuedMessageCount > 0) {
391
- // Preserve the existing empty-submit flush for non-steer queues.
392
- await this.ctx.session.abort({ reason: USER_INTERRUPT_LABEL });
393
- return;
394
483
  }
484
+ return;
395
485
  }
396
486
 
397
487
  if (!text) return;
@@ -663,9 +753,9 @@ export class InputController {
663
753
  async #submitToFocusedSession(text: string, streamingBehavior: "steer" | "followUp"): Promise<void> {
664
754
  const target = this.ctx.viewSession;
665
755
  if (!text) {
666
- // Mirror the empty-submit steer flush against the focused session.
667
- if (target.isStreaming && target.getQueuedMessages().steering.length > 0) {
668
- await target.interruptAndFlushQueuedMessages({ reason: USER_INTERRUPT_LABEL });
756
+ if (target.isStreaming && target.queuedMessageCount > 0) {
757
+ const aborting = target.abort({ reason: USER_INTERRUPT_LABEL });
758
+ await aborting;
669
759
  this.ctx.updatePendingMessagesDisplay();
670
760
  this.ctx.ui.requestRender();
671
761
  }
@@ -702,6 +792,18 @@ export class InputController {
702
792
  this.ctx.clearEditor();
703
793
  this.ctx.lastSigintTime = now;
704
794
  }
795
+ // Sync-flush the session JSONL so in-flight writes survive a hard exit.
796
+ // The TUI consumes Ctrl+C as a key event in raw mode, so postmortem's
797
+ // process-level SIGINT handler never fires. The second press still
798
+ // funnels through shutdown() which awaits its own async flush — the
799
+ // sync flush here is a superset that also covers the first-press case.
800
+ try {
801
+ this.ctx.sessionManager.flushSync();
802
+ } catch (err) {
803
+ logger.warn("session-manager sync flush on Ctrl+C failed", {
804
+ error: err instanceof Error ? err.message : String(err),
805
+ });
806
+ }
705
807
  }
706
808
 
707
809
  handleCtrlD(): void {
@@ -798,15 +900,6 @@ export class InputController {
798
900
  args: args || undefined,
799
901
  lineCount: body ? body.split("\n").length : 0,
800
902
  };
801
- // When the agent is streaming, register the compact slash-form text as
802
- // the pending-display twin BEFORE dispatching the CustomMessage. The
803
- // returned tag is embedded in details so AgentSession.#handleAgentEvent
804
- // can remove the matching display entry when the agent consumes this
805
- // message (mirrors the user-message dequeue path).
806
- if (this.ctx.session.isStreaming) {
807
- const tag = this.ctx.session.enqueueCustomMessageDisplay(text, streamingBehavior);
808
- details.__pendingDisplayTag = tag;
809
- }
810
903
  await this.ctx.session.promptCustomMessage(
811
904
  {
812
905
  customType: SKILL_PROMPT_MESSAGE_TYPE,
@@ -815,7 +908,7 @@ export class InputController {
815
908
  details,
816
909
  attribution: "user",
817
910
  },
818
- { streamingBehavior },
911
+ { streamingBehavior, queueChipText: text },
819
912
  );
820
913
  if (this.ctx.session.isStreaming) {
821
914
  this.ctx.updatePendingMessagesDisplay();
@@ -901,22 +994,55 @@ export class InputController {
901
994
  restoreQueuedMessagesToEditor(options?: { abort?: boolean; currentText?: string }): number {
902
995
  this.ctx.locallySubmittedUserSignatures.clear();
903
996
  const { steering, followUp } = this.ctx.session.clearQueue();
904
- const allQueued = [...steering, ...followUp];
997
+ // Messages typed while compacting live in `compactionQueuedMessages`, not the
998
+ // agent queue `clearQueue()` drains — but the pending bar shows the same
999
+ // "Alt+Up to edit" hint for them (ui-helpers `updatePendingMessagesDisplay`).
1000
+ // Drain them here too so the dequeue restores every message the hint
1001
+ // advertises; otherwise a skill/text queued during compaction is stranded and
1002
+ // Alt+Up reports "No queued messages to restore".
1003
+ const compactionQueued = this.ctx.compactionQueuedMessages;
1004
+ this.ctx.compactionQueuedMessages = [];
1005
+ const allQueued = [
1006
+ ...steering,
1007
+ ...compactionQueued.filter(e => e.mode === "steer").map(e => ({ text: e.text, images: e.images })),
1008
+ ...followUp,
1009
+ ...compactionQueued.filter(e => e.mode === "followUp").map(e => ({ text: e.text, images: e.images })),
1010
+ ];
905
1011
  if (allQueued.length === 0) {
906
1012
  this.ctx.updatePendingMessagesDisplay();
907
1013
  if (options?.abort) {
908
- this.ctx.session.abort({ reason: USER_INTERRUPT_LABEL });
1014
+ void this.ctx.session.abort({ reason: USER_INTERRUPT_LABEL });
909
1015
  }
910
1016
  return 0;
911
1017
  }
912
- const queuedText = allQueued.map(e => e.text).join("\n\n");
1018
+ // Image markers are positional: `[Image #N]` ↔ `pendingImages[N-1]`. Each
1019
+ // queued message numbered its markers against its own local image list
1020
+ // (1..K). Because we prepend the queued text but append the queued images
1021
+ // to `pendingImages`, any existing draft images (M of them) — plus images
1022
+ // already pulled in by earlier queued messages — shift the slot index that
1023
+ // every marker must point to. Bumping each message's markers by the
1024
+ // running offset keeps the merged text aligned with the merged
1025
+ // `pendingImages` order; draft markers stay valid because draft images
1026
+ // keep their original positions.
1027
+ const queuedImages = allQueued.flatMap(e => e.images ?? []);
1028
+ let queuedText: string;
1029
+ if (queuedImages.length > 0) {
1030
+ const parts: string[] = [];
1031
+ let imageOffset = this.ctx.pendingImages.length;
1032
+ for (const entry of allQueued) {
1033
+ parts.push(shiftImageMarkers(entry.text, imageOffset));
1034
+ if (entry.images && entry.images.length > 0) imageOffset += entry.images.length;
1035
+ }
1036
+ queuedText = parts.join("\n\n");
1037
+ } else {
1038
+ queuedText = allQueued.map(e => e.text).join("\n\n");
1039
+ }
913
1040
  const currentText = options?.currentText ?? this.ctx.editor.getText();
914
1041
  const combinedText = [queuedText, currentText].filter(t => t.trim()).join("\n\n");
915
1042
  this.ctx.editor.setText(combinedText);
916
1043
  // Hand queued images back to the pending-image buffer (links are
917
1044
  // re-materialized lazily; the restored text already carries the
918
- // `[Image #N, WxH]` markers).
919
- const queuedImages = allQueued.flatMap(e => e.images ?? []);
1045
+ // renumbered `[Image #N, WxH]` markers).
920
1046
  if (queuedImages.length > 0) {
921
1047
  this.ctx.pendingImages.push(...queuedImages);
922
1048
  this.ctx.pendingImageLinks.push(...queuedImages.map(() => undefined));
@@ -924,7 +1050,7 @@ export class InputController {
924
1050
  }
925
1051
  this.ctx.updatePendingMessagesDisplay();
926
1052
  if (options?.abort) {
927
- this.ctx.session.abort({ reason: USER_INTERRUPT_LABEL });
1053
+ void this.ctx.session.abort({ reason: USER_INTERRUPT_LABEL });
928
1054
  }
929
1055
  return allQueued.length;
930
1056
  }
@@ -990,6 +1116,35 @@ export class InputController {
990
1116
  return true;
991
1117
  }
992
1118
 
1119
+ /**
1120
+ * Win+Shift+S on Windows 11 leaves the screenshot bitmap on the clipboard
1121
+ * while the terminal pastes a transient packaged-app TempState path
1122
+ * (…\MicrosoftWindows.Client.Core_*\TempState\…) that is already gone — or
1123
+ * never materialized — by the time we read it. Whenever a pasted image path
1124
+ * can't be turned into an image locally, those clipboard bytes are the real
1125
+ * payload, so prefer them before degrading to a text paste.
1126
+ *
1127
+ * Skipped over SSH: the clipboard read would hit the remote host, not the
1128
+ * terminal that holds the screenshot. Returns true when the clipboard owned
1129
+ * the outcome (image attached, or an unsupported-format status surfaced), so
1130
+ * the caller stops without emitting its own degraded diagnostic.
1131
+ */
1132
+ async #tryPasteClipboardImage(): Promise<boolean> {
1133
+ const env = process.env;
1134
+ if (env.SSH_CONNECTION || env.SSH_TTY || env.SSH_CLIENT) return false;
1135
+ try {
1136
+ const image = await this.clipboard.readImage();
1137
+ if (!image) return false;
1138
+ await this.#normalizeAndInsertPastedImage(
1139
+ { type: "image", data: image.data.toBase64(), mimeType: image.mimeType },
1140
+ `Unsupported clipboard image format: ${image.mimeType}`,
1141
+ );
1142
+ return true;
1143
+ } catch {
1144
+ return false;
1145
+ }
1146
+ }
1147
+
993
1148
  async handleImagePathPaste(path: string): Promise<void> {
994
1149
  try {
995
1150
  const image = await loadImageInput({
@@ -998,6 +1153,9 @@ export class InputController {
998
1153
  autoResize: false,
999
1154
  });
1000
1155
  if (!image) {
1156
+ // Path resolved but is not a readable image (e.g. a zero-byte or
1157
+ // locked transient screenshot file). Prefer the clipboard bytes.
1158
+ if (await this.#tryPasteClipboardImage()) return;
1001
1159
  this.ctx.editor.pasteText(path);
1002
1160
  this.ctx.ui.requestRender();
1003
1161
  this.ctx.showStatus("Pasted path is not a supported image");
@@ -1016,13 +1174,17 @@ export class InputController {
1016
1174
  }
1017
1175
  if (isEnoent(error)) {
1018
1176
  // #2375: the bracketed paste forwarded by a local terminal carries a
1019
- // path on the *local* filesystem. When omp itself runs over SSH, that
1020
- // path is unreachable here; pasting it as text would look like the
1021
- // image was attached when in fact nothing was sent. Refuse the silent
1022
- // degrade and tell the user how to send the bytes for real. The
1023
- // pasted path is untrusted terminal input strip control/ANSI/
1024
- // newlines, collapse home to `~`, and bound the displayed length
1025
- // before splicing it into the status string.
1177
+ // path on the *local* filesystem. The bytes may still be on the
1178
+ // clipboard (Win+Shift+S), so try those before giving up.
1179
+ if (await this.#tryPasteClipboardImage()) return;
1180
+ // Over SSH the clipboard lives on the remote host, so the path is
1181
+ // genuinely unreachable; pasting it as text would look like the
1182
+ // image was attached when nothing was sent. Surface an SSH-aware
1183
+ // diagnostic instead. The pasted path is untrusted terminal input —
1184
+ // strip control/ANSI/newlines, collapse home to `~`, and bound the
1185
+ // displayed length before splicing it into the status string.
1186
+ const env = process.env;
1187
+ const overSsh = Boolean(env.SSH_CONNECTION || env.SSH_TTY || env.SSH_CLIENT);
1026
1188
  const displayPath = truncateToWidth(
1027
1189
  shortenPath(
1028
1190
  sanitizeText(path)
@@ -1031,8 +1193,6 @@ export class InputController {
1031
1193
  ),
1032
1194
  TRUNCATE_LENGTHS.CONTENT,
1033
1195
  );
1034
- const env = process.env;
1035
- const overSsh = Boolean(env.SSH_CONNECTION || env.SSH_TTY || env.SSH_CLIENT);
1036
1196
  this.ctx.showStatus(
1037
1197
  overSsh
1038
1198
  ? `Image not found at ${displayPath}. Over SSH this path is local to your terminal — paste the image directly (clipboard image-paste shortcut) to send its bytes.`
@@ -1040,6 +1200,7 @@ export class InputController {
1040
1200
  );
1041
1201
  return;
1042
1202
  }
1203
+ if (await this.#tryPasteClipboardImage()) return;
1043
1204
  this.ctx.editor.pasteText(path);
1044
1205
  this.ctx.ui.requestRender();
1045
1206
  this.ctx.showStatus("Failed to read pasted image path");
@@ -1096,6 +1257,97 @@ export class InputController {
1096
1257
  }
1097
1258
  }
1098
1259
 
1260
+ /**
1261
+ * Editor `onLargePaste` hook: gate a marker-sized paste behind the large-paste menu. Returns
1262
+ * `true` to intercept (the editor skips its default `[Paste]` marker) once the paste reaches the
1263
+ * configured `paste.largeMenuThreshold` line count; otherwise `false` for default collapse-to-marker
1264
+ * behavior. The async menu is fired and forgotten — the editor only needs the synchronous verdict.
1265
+ */
1266
+ handleLargePaste(text: string, lineCount: number): boolean {
1267
+ const threshold = this.ctx.settings.get("paste.largeMenuThreshold");
1268
+ if (!(threshold > 0) || lineCount < threshold) return false;
1269
+ void this.presentLargePasteMenu(text, lineCount);
1270
+ return true;
1271
+ }
1272
+
1273
+ /**
1274
+ * Present the large-paste menu and apply the chosen action: wrap in a code block or in XML tags
1275
+ * (both collapse to a `[Paste]` marker that expands on submit), or save the text to a file and
1276
+ * reference its path so the agent can `read` it on demand. Cancelling (Esc) falls back to the
1277
+ * default inline paste marker, so the pasted content is never lost.
1278
+ */
1279
+ async presentLargePasteMenu(text: string, lineCount: number): Promise<void> {
1280
+ const CODE_BLOCK = "Wrap in a code block";
1281
+ const XML = "Wrap in XML tags";
1282
+ const FILE = "Attach as a file";
1283
+
1284
+ let choice: string | undefined;
1285
+ try {
1286
+ choice = await this.ctx.showHookSelector(
1287
+ `Pasted ${lineCount} lines`,
1288
+ [
1289
+ { label: CODE_BLOCK, description: "Fence the text in a ``` block, collapsed to a marker" },
1290
+ { label: XML, description: "Wrap the text in <pasted_text> tags, collapsed to a marker" },
1291
+ { label: FILE, description: "Save the text to a file and reference its path" },
1292
+ ],
1293
+ { helpText: "Esc to paste inline" },
1294
+ );
1295
+ } catch (error) {
1296
+ logger.warn("large-paste menu failed", { error: error instanceof Error ? error.message : String(error) });
1297
+ choice = undefined;
1298
+ }
1299
+
1300
+ switch (choice) {
1301
+ case CODE_BLOCK:
1302
+ this.ctx.editor.insertPaste(wrapPasteInCodeBlock(text));
1303
+ break;
1304
+ case XML:
1305
+ this.ctx.editor.insertPaste(wrapPasteInXml(text));
1306
+ break;
1307
+ case FILE:
1308
+ await this.#attachPasteAsFile(text, lineCount);
1309
+ break;
1310
+ default:
1311
+ // Esc / cancel: keep the original behavior — collapse to an inline paste marker.
1312
+ this.ctx.editor.insertPaste(text);
1313
+ break;
1314
+ }
1315
+ this.ctx.ui.requestRender();
1316
+ }
1317
+
1318
+ /**
1319
+ * Save a large paste to the session's `local://` store and insert a clean `local://attachment-N`
1320
+ * reference into the editor so the agent can `read` it on demand — instead of inlining the text or
1321
+ * leaking a raw temp path. Falls back to an inline paste marker when the write fails, so the
1322
+ * content is never lost.
1323
+ */
1324
+ async #attachPasteAsFile(text: string, lineCount: number): Promise<void> {
1325
+ try {
1326
+ // Mirror the exact mapping the read tool's local:// resolver uses so a later
1327
+ // `read local://attachment-N` lands on the file written here.
1328
+ const localRoot = resolveLocalRoot({
1329
+ getArtifactsDir: () => this.ctx.sessionManager.getArtifactsDir(),
1330
+ getSessionId: () => this.ctx.sessionManager.getSessionId(),
1331
+ });
1332
+ let name: string;
1333
+ let filePath: string;
1334
+ do {
1335
+ this.#attachmentCounter++;
1336
+ name = `attachment-${this.#attachmentCounter}`;
1337
+ filePath = path.join(localRoot, name);
1338
+ } while (await Bun.file(filePath).exists());
1339
+ await Bun.write(filePath, text);
1340
+ this.ctx.editor.insertText(`local://${name} `);
1341
+ this.ctx.showStatus(`Saved ${lineCount} pasted lines to local://${name}`);
1342
+ } catch (error) {
1343
+ logger.warn("failed to save large paste to file", {
1344
+ error: error instanceof Error ? error.message : String(error),
1345
+ });
1346
+ this.ctx.editor.insertPaste(text);
1347
+ this.ctx.showError("Failed to save paste to a file — pasted inline instead");
1348
+ }
1349
+ }
1350
+
1099
1351
  createAutocompleteProvider(commands: SlashCommand[], basePath: string): AutocompleteProvider {
1100
1352
  return createPromptActionAutocompleteProvider({
1101
1353
  commands,
@@ -1177,12 +1429,14 @@ export class InputController {
1177
1429
  this.ctx.updateEditorBorderColor();
1178
1430
  // The status line already reports the resolved model + thinking level, so
1179
1431
  // the cycle status is just a status-line-style chip track (active role
1180
- // filled), matching the plan-approval model slider.
1432
+ // filled), matching the plan-approval model slider. It renders into its
1433
+ // own anchored container above the editor (cleared+rebuilt each cycle),
1434
+ // so it updates in place instead of stacking duplicates in the scrollback.
1181
1435
  const track = renderSegmentTrack(
1182
1436
  cycleOrder.map(role => ({ label: role })),
1183
1437
  cycleOrder.indexOf(result.role),
1184
1438
  );
1185
- this.ctx.showStatus(track, { dim: false });
1439
+ this.ctx.showModelCycleTrack(track);
1186
1440
  } catch (error) {
1187
1441
  this.ctx.showError(error instanceof Error ? error.message : String(error));
1188
1442
  }
@@ -813,6 +813,43 @@ export class MCPCommandController {
813
813
  return null;
814
814
  }
815
815
 
816
+ /**
817
+ * Resolve a server for an auth/test operation.
818
+ *
819
+ * Unlike {@link #findConfiguredServer} (which only reads writable OMP config
820
+ * files), this also recognizes runtime-discovered servers that `/mcp list`
821
+ * surfaces but that live in no writable config — e.g. servers from a Claude
822
+ * Code marketplace plugin (`cloudflare:cloudflare-api`), `.cursor/mcp.json`,
823
+ * etc. Without this, `/mcp reauth|test|unauth` reports "not found" for a
824
+ * server the list just showed.
825
+ *
826
+ * For a discovered server, any persisted change is written into the *user*
827
+ * config under the same (namespaced) name; the native provider (priority 100)
828
+ * shadows the discovered entry on the next reload, so an OAuth `auth` block
829
+ * persisted by `/mcp reauth` takes effect. `discovered` lets callers tailor
830
+ * messaging and skip pointless writes when there is nothing to persist.
831
+ */
832
+ async #resolveServerForAuth(name: string): Promise<{
833
+ filePath: string;
834
+ scope: "user" | "project";
835
+ config: MCPServerConfig;
836
+ discovered: boolean;
837
+ } | null> {
838
+ const found = await this.#findConfiguredServer(name);
839
+ if (found) return { ...found, discovered: false };
840
+
841
+ const config = this.ctx.mcpManager?.getServerConfig(name);
842
+ const source = this.ctx.mcpManager?.getSource(name);
843
+ if (!config || !source) return null;
844
+
845
+ return {
846
+ filePath: getMCPConfigPath("user", getProjectDir()),
847
+ scope: "user",
848
+ config,
849
+ discovered: true,
850
+ };
851
+ }
852
+
816
853
  async #removeManagedOAuthCredential(credentialId: string | undefined): Promise<void> {
817
854
  if (!credentialId?.startsWith("mcp_oauth_")) return;
818
855
  await this.ctx.session.modelRegistry.authStorage.remove(credentialId);
@@ -1199,7 +1236,7 @@ export class MCPCommandController {
1199
1236
 
1200
1237
  let connection: MCPServerConnection | undefined;
1201
1238
  try {
1202
- const found = await this.#findConfiguredServer(name);
1239
+ const found = await this.#resolveServerForAuth(name);
1203
1240
 
1204
1241
  if (!found) {
1205
1242
  this.ctx.showError(
@@ -1389,13 +1426,17 @@ export class MCPCommandController {
1389
1426
  }
1390
1427
 
1391
1428
  try {
1392
- const found = await this.#findConfiguredServer(name);
1429
+ const found = await this.#resolveServerForAuth(name);
1393
1430
  if (!found) {
1394
1431
  this.ctx.showError(`Server "${name}" not found.`);
1395
1432
  return;
1396
1433
  }
1397
1434
 
1398
1435
  const currentAuth = (found.config as MCPServerConfig & { auth?: MCPAuthConfig }).auth;
1436
+ if (found.discovered && currentAuth?.type !== "oauth") {
1437
+ this.#showMessage(["", theme.fg("muted", `No stored OAuth auth to remove for "${name}".`), ""].join("\n"));
1438
+ return;
1439
+ }
1399
1440
  if (currentAuth?.type === "oauth") {
1400
1441
  await this.#removeManagedOAuthCredential(currentAuth.credentialId);
1401
1442
  }
@@ -1419,7 +1460,7 @@ export class MCPCommandController {
1419
1460
  }
1420
1461
 
1421
1462
  try {
1422
- const found = await this.#findConfiguredServer(name);
1463
+ const found = await this.#resolveServerForAuth(name);
1423
1464
  if (!found) {
1424
1465
  this.ctx.showError(`Server "${name}" not found.`);
1425
1466
  return;