@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,22 +1,18 @@
1
1
  import { Box, type Component, Markdown } from "@oh-my-pi/pi-tui";
2
2
  import { getMarkdownTheme, theme } from "../../modes/theme/theme";
3
- import type { CompactionSummaryMessage } from "../../session/messages";
3
+ import type { CompactionSummaryMessage, CustomMessage } from "../../session/messages";
4
4
 
5
- /**
6
- * Compaction point in the transcript, rendered as a slim horizontal divider:
7
- *
8
- * ──────── 📷 compacted · ctrl+o ────────
9
- *
10
- * The conversation above the divider stays visible (display transcript keeps
11
- * full history); only the LLM context was reset. Expanding (ctrl+o) reveals
12
- * the compaction summary below the divider.
13
- */
14
- export class CompactionSummaryMessageComponent implements Component {
5
+ interface SummaryDividerOptions {
6
+ label: () => string;
7
+ detailMarkdown: () => string;
8
+ }
9
+
10
+ class SummaryDividerComponent implements Component {
15
11
  #expanded = false;
16
12
  #cache?: { width: number; lines: string[] };
17
13
  #detail?: Box;
18
14
 
19
- constructor(private readonly message: CompactionSummaryMessage) {}
15
+ constructor(private readonly options: SummaryDividerOptions) {}
20
16
 
21
17
  setExpanded(expanded: boolean): void {
22
18
  if (this.#expanded === expanded) return;
@@ -44,7 +40,7 @@ export class CompactionSummaryMessageComponent implements Component {
44
40
 
45
41
  #divider(width: number): string {
46
42
  const rule = theme.tree.horizontal;
47
- const label = `${theme.icon.camera} compacted`;
43
+ const label = this.options.label();
48
44
  // sep.dot ships pre-padded (" · "); trim so the hint joins with single spaces.
49
45
  const hint = `${theme.sep.dot.trim()} ctrl+o`;
50
46
  const plainWidth = Bun.stringWidth(`${label} ${hint}`, { countAnsiEscapeCodes: false });
@@ -66,22 +62,125 @@ export class CompactionSummaryMessageComponent implements Component {
66
62
  #detailBox(): Box {
67
63
  if (this.#detail) return this.#detail;
68
64
  const box = new Box(1, 1, t => theme.bg("customMessageBg", t));
69
- const tokenStr = this.message.tokensBefore.toLocaleString();
70
- const frameCount = this.message.images?.length ?? 0;
71
- const frameNote =
72
- frameCount > 0 ? `\n\n_${frameCount} snapcompact frame${frameCount === 1 ? "" : "s"} attached_` : "";
73
65
  box.addChild(
74
- new Markdown(
75
- `**Compacted from ${tokenStr} tokens**\n\n${this.message.summary}${frameNote}`,
76
- 0,
77
- 0,
78
- getMarkdownTheme(),
79
- {
80
- color: (text: string) => theme.fg("customMessageText", text),
81
- },
82
- ),
66
+ new Markdown(this.options.detailMarkdown(), 0, 0, getMarkdownTheme(), {
67
+ color: (text: string) => theme.fg("customMessageText", text),
68
+ }),
83
69
  );
84
70
  this.#detail = box;
85
71
  return box;
86
72
  }
87
73
  }
74
+
75
+ /**
76
+ * Compaction point in the transcript, rendered as a slim horizontal divider:
77
+ *
78
+ * ──────── 📷 compacted · ctrl+o ────────
79
+ *
80
+ * The conversation above the divider stays visible (display transcript keeps
81
+ * full history); only the LLM context was reset. Expanding (ctrl+o) reveals
82
+ * the compaction summary below the divider.
83
+ */
84
+ export class CompactionSummaryMessageComponent implements Component {
85
+ #divider: SummaryDividerComponent;
86
+
87
+ constructor(private readonly message: CompactionSummaryMessage) {
88
+ this.#divider = new SummaryDividerComponent({
89
+ label: () => `${theme.icon.camera} compacted`,
90
+ detailMarkdown: () => this.#detailMarkdown(),
91
+ });
92
+ }
93
+
94
+ setExpanded(expanded: boolean): void {
95
+ this.#divider.setExpanded(expanded);
96
+ }
97
+
98
+ invalidate(): void {
99
+ this.#divider.invalidate();
100
+ }
101
+
102
+ render(width: number): readonly string[] {
103
+ return this.#divider.render(width);
104
+ }
105
+
106
+ #detailMarkdown(): string {
107
+ const tokenStr = this.message.tokensBefore.toLocaleString();
108
+ const frameCount = this.message.images?.length ?? 0;
109
+ const frameNote =
110
+ frameCount > 0 ? `\n\n_${frameCount} snapcompact frame${frameCount === 1 ? "" : "s"} attached_` : "";
111
+ return `**Compacted from ${tokenStr} tokens**\n\n${this.message.summary}${frameNote}`;
112
+ }
113
+ }
114
+
115
+ /**
116
+ * Handoff is a compaction strategy too, but it is persisted as a custom message
117
+ * so the LLM sees the handoff-specific developer context. Render it with the
118
+ * same divider affordance as `/compact` instead of the generic `[handoff]` box.
119
+ */
120
+ export class HandoffSummaryMessageComponent implements Component {
121
+ #divider: SummaryDividerComponent;
122
+
123
+ constructor(private readonly message: CustomMessage<unknown>) {
124
+ this.#divider = new SummaryDividerComponent({
125
+ label: () => `${theme.icon.context} handoff`,
126
+ detailMarkdown: () => this.#detailMarkdown(),
127
+ });
128
+ }
129
+
130
+ setExpanded(expanded: boolean): void {
131
+ this.#divider.setExpanded(expanded);
132
+ }
133
+
134
+ invalidate(): void {
135
+ this.#divider.invalidate();
136
+ }
137
+
138
+ render(width: number): readonly string[] {
139
+ return this.#divider.render(width);
140
+ }
141
+
142
+ #detailMarkdown(): string {
143
+ const document = extractHandoffDocument(getCustomMessageText(this.message));
144
+ return `**Handoff context**\n\n${document || "_No handoff content._"}`;
145
+ }
146
+ }
147
+
148
+ export function createHandoffSummaryMessageComponent(
149
+ message: CustomMessage<unknown>,
150
+ expanded: boolean,
151
+ ): HandoffSummaryMessageComponent | undefined {
152
+ if (message.customType !== "handoff" || !message.display) return undefined;
153
+ const component = new HandoffSummaryMessageComponent(message);
154
+ component.setExpanded(expanded);
155
+ return component;
156
+ }
157
+
158
+ function getCustomMessageText(message: CustomMessage<unknown>): string {
159
+ if (typeof message.content === "string") return message.content;
160
+ let firstText: string | undefined;
161
+ let parts: string[] | undefined;
162
+ for (const content of message.content) {
163
+ if (content.type !== "text") continue;
164
+ if (firstText === undefined) {
165
+ firstText = content.text;
166
+ continue;
167
+ }
168
+ if (parts === undefined) {
169
+ parts = [firstText];
170
+ }
171
+ parts.push(content.text);
172
+ }
173
+ return parts === undefined ? (firstText ?? "") : parts.join("\n");
174
+ }
175
+
176
+ function extractHandoffDocument(text: string): string {
177
+ const openTag = "<handoff-context>";
178
+ const closeTag = "</handoff-context>";
179
+ const openIndex = text.indexOf(openTag);
180
+ if (openIndex === -1) return text.trim();
181
+
182
+ const contentStart = openIndex + openTag.length;
183
+ const closeIndex = text.indexOf(closeTag, contentStart);
184
+ const document = closeIndex === -1 ? text.slice(contentStart) : text.slice(contentStart, closeIndex);
185
+ return document.trim();
186
+ }
@@ -0,0 +1,96 @@
1
+ import { afterEach, beforeAll, describe, expect, it, vi } from "bun:test";
2
+ import { $ } from "bun";
3
+ import { getEditorTheme, initTheme } from "../theme/theme";
4
+ import { CustomEditor, SPACE_HOLD_RELEASE_MS, SPACE_HOLD_THRESHOLD } from "./custom-editor";
5
+
6
+ function makeEditor() {
7
+ const editor = new CustomEditor(getEditorTheme());
8
+ const events: string[] = [];
9
+ editor.sttHoldEnabled = () => true;
10
+ editor.onSpaceHoldStart = () => events.push("start");
11
+ editor.onSpaceHoldEnd = () => events.push("end");
12
+ return { editor, events };
13
+ }
14
+
15
+ function holdSpace(editor: CustomEditor, count: number): void {
16
+ for (let i = 0; i < count; i++) editor.handleInput(" ");
17
+ }
18
+
19
+ async function decorateInFreshProcess(text: string): Promise<string> {
20
+ const customEditorUrl = new URL("./custom-editor.ts", import.meta.url).href;
21
+ const script = `
22
+ import { CustomEditor } from ${JSON.stringify(customEditorUrl)};
23
+ const editor = new CustomEditor({});
24
+ process.stdout.write(editor.decorateText(${JSON.stringify(text)}));
25
+ `;
26
+ const child = await $`bun -e ${script}`.quiet().nothrow();
27
+ const stdout = child.stdout.toString();
28
+ const stderr = child.stderr.toString();
29
+ if (child.exitCode !== 0) throw new Error(stderr || stdout || `decorate subprocess exited with ${child.exitCode}`);
30
+ return stdout;
31
+ }
32
+
33
+ describe("CustomEditor placeholder decoration", () => {
34
+ it("renders paste placeholders before theme initialization", async () => {
35
+ const output = await decorateInFreshProcess("[Paste #1, +30 lines]");
36
+ expect(output).toBe("[Paste #1, +30 lines]");
37
+ });
38
+
39
+ it("renders image placeholders before theme initialization", async () => {
40
+ const output = await decorateInFreshProcess("[Image #1]");
41
+ expect(output).toBe("[Image #1]");
42
+ });
43
+ });
44
+
45
+ describe("CustomEditor space-hold push-to-talk", () => {
46
+ beforeAll(async () => {
47
+ await initTheme();
48
+ });
49
+
50
+ afterEach(() => {
51
+ vi.useRealTimers();
52
+ });
53
+
54
+ it("inserts spaces normally below the hold threshold", () => {
55
+ const { editor, events } = makeEditor();
56
+ holdSpace(editor, SPACE_HOLD_THRESHOLD);
57
+ expect(editor.getText()).toBe(" ".repeat(SPACE_HOLD_THRESHOLD));
58
+ expect(events).toEqual([]);
59
+ });
60
+
61
+ it("tracks back the space burst and drives the hold lifecycle", () => {
62
+ vi.useFakeTimers();
63
+ const { editor, events } = makeEditor();
64
+ editor.handleInput("h");
65
+ editor.handleInput("i");
66
+ // Crossing the threshold deletes the optimistically-inserted spaces and starts recording,
67
+ // leaving only the pre-burst text behind.
68
+ holdSpace(editor, SPACE_HOLD_THRESHOLD + 1);
69
+ expect(editor.getText()).toBe("hi");
70
+ expect(events).toEqual(["start"]);
71
+ // Continued auto-repeat while the bar is held is swallowed: no spam, no re-trigger.
72
+ holdSpace(editor, 5);
73
+ expect(editor.getText()).toBe("hi");
74
+ expect(events).toEqual(["start"]);
75
+ // An idle gap with no further repeats means the bar was released -> stop + transcribe.
76
+ vi.advanceTimersByTime(SPACE_HOLD_RELEASE_MS + 1);
77
+ expect(events).toEqual(["start", "end"]);
78
+ });
79
+
80
+ it("does not trigger when a non-space breaks the run", () => {
81
+ const { editor, events } = makeEditor();
82
+ holdSpace(editor, SPACE_HOLD_THRESHOLD);
83
+ editor.handleInput("x");
84
+ holdSpace(editor, SPACE_HOLD_THRESHOLD);
85
+ expect(events).toEqual([]);
86
+ expect(editor.getText()).toBe(`${" ".repeat(SPACE_HOLD_THRESHOLD)}x${" ".repeat(SPACE_HOLD_THRESHOLD)}`);
87
+ });
88
+
89
+ it("leaves the space bar typing normally when the gesture is disabled", () => {
90
+ const { editor, events } = makeEditor();
91
+ editor.sttHoldEnabled = () => false;
92
+ holdSpace(editor, SPACE_HOLD_THRESHOLD + 5);
93
+ expect(editor.getText()).toBe(" ".repeat(SPACE_HOLD_THRESHOLD + 5));
94
+ expect(events).toEqual([]);
95
+ });
96
+ });
@@ -1,8 +1,9 @@
1
1
  import { addKeyAliases, canonicalKeyId, Editor, type KeyId, parseKey, parseKittySequence } from "@oh-my-pi/pi-tui";
2
2
  import type { AppKeybinding } from "../../config/keybindings";
3
+ import { isSettingsInitialized, settings } from "../../config/settings";
3
4
  import { imageReferenceHyperlink, PLACEHOLDER_REGEX, renderPlaceholders } from "../image-references";
4
- import { highlightMagicKeywords } from "../magic-keywords";
5
- import { theme } from "../theme/theme";
5
+ import { hasMagicKeyword, highlightMagicKeywords } from "../magic-keywords";
6
+ import { fgOrPlain } from "../theme/theme";
6
7
 
7
8
  type ConfigurableEditorAction = Extract<
8
9
  AppKeybinding,
@@ -61,6 +62,14 @@ const BRACKETED_IMAGE_PATH_REGEX = /\.(?:png|jpe?g|gif|webp)$/i;
61
62
  const BRACKETED_IMAGE_PATH_BOUNDARY_REGEX = /\.(?:png|jpe?g|gif|webp)(?=$|["']?\s)/gi;
62
63
  const SHELL_ESCAPED_PATH_CHAR_REGEX = /\\([\\\s'"()[\]{}&;<>|?*!$`])/g;
63
64
 
65
+ /** Plain spaces from one auto-repeat run that trigger the space-hold push-to-talk STT gesture.
66
+ * Holding the space bar makes the terminal emit a burst of spaces; once more than this many land
67
+ * in the editor we treat it as "space held", track them back out, and start recording. */
68
+ export const SPACE_HOLD_THRESHOLD = 5;
69
+ /** Idle gap (ms) after the last repeated space that counts as the space bar being released, ending
70
+ * the push-to-talk recording. Must comfortably exceed the OS key-repeat interval. */
71
+ export const SPACE_HOLD_RELEASE_MS = 250;
72
+
64
73
  function isPastedPathSeparator(char: string | undefined): boolean {
65
74
  return char === undefined || char === " " || char === "\t" || char === "\r" || char === "\n";
66
75
  }
@@ -136,19 +145,85 @@ export class CustomEditor extends Editor {
136
145
  * instead of corrupting `[Paste #1, +30 lines]` into plain text. */
137
146
  override atomicTokenPattern = PLACEHOLDER_REGEX;
138
147
 
148
+ /** Magic-keyword shimmer cadence — drives one editor repaint every 70 ms while
149
+ * a keyword is on screen and the prompt is focused. ~14 frames/s is smooth
150
+ * without flooding the renderer. */
151
+ static readonly SHIMMER_FRAME_MS = 70;
152
+ /** Time for the gradient to sweep one full cycle across each keyword. */
153
+ static readonly SHIMMER_PERIOD_MS = 1800;
154
+
155
+ /** Per-render scratch flag: did any layout line in this render contain a magic
156
+ * keyword that should shimmer? Reset by {@link #scheduleShimmerIfNeeded} each
157
+ * time a frame is queued. */
158
+ #shimmerTimer: ReturnType<typeof setTimeout> | undefined;
159
+ /** Repaint hook the host wires once at construction. Called from the shimmer
160
+ * timer to request the next animation frame. Undefined when nobody is
161
+ * listening (tests, headless callers); the timer chain still self-cleans. */
162
+ #requestShimmerRepaint: (() => void) | undefined;
163
+
139
164
  /** Gradient-highlight the "ultrathink" / "orchestrate" / "workflowz" keywords as the user types
140
165
  * them, skipping any occurrence inside code spans, fenced blocks, or XML sections. Also make
141
- * pasted image placeholders visually distinct and hyperlink them once their blob file exists. */
142
- decorateText = (text: string): string =>
143
- renderPlaceholders(text, {
144
- renderText: value => highlightMagicKeywords(value),
166
+ * pasted image placeholders visually distinct and hyperlink them once their blob file exists.
167
+ * When the editor is focused, the buffer contains a magic keyword, and `magicKeywords.enabled`
168
+ * is on, the gradient shifts every frame to produce a Claude-Code-style shimmer; each render
169
+ * schedules the next frame, so losing focus, deleting the keyword, or flipping the setting
170
+ * stops the animation on its own. The static glow itself runs even when shimmering is gated
171
+ * off, matching existing behavior for the editor and sent bubbles. */
172
+ decorateText = (text: string): string => {
173
+ const animated = this.focused && this.#shimmerEnabled() && hasMagicKeyword(this.getText());
174
+ const phase = animated ? (Date.now() % CustomEditor.SHIMMER_PERIOD_MS) / CustomEditor.SHIMMER_PERIOD_MS : 0;
175
+ if (animated) this.#scheduleShimmerFrame();
176
+ return renderPlaceholders(text, {
177
+ renderText: value => highlightMagicKeywords(value, undefined, phase),
145
178
  renderReference: (value, kind, index) =>
146
179
  kind === "image"
147
180
  ? imageReferenceHyperlink(value, index, this.imageLinks, label =>
148
- theme.fg("accent", `\x1b[1m\x1b[4m${label}\x1b[24m\x1b[22m`),
181
+ fgOrPlain("accent", label, `\x1b[1m\x1b[4m${label}\x1b[24m\x1b[22m`),
149
182
  )
150
- : theme.fg("accent", `\x1b[1m${value}\x1b[22m`),
183
+ : fgOrPlain("accent", value, `\x1b[1m${value}\x1b[22m`),
151
184
  });
185
+ };
186
+
187
+ /** Optional test/host override for the magic-keyword shimmer gate. When
188
+ * defined, takes precedence over the global `magicKeywords.enabled` setting,
189
+ * letting tests assert the gating behaviour without mutating the
190
+ * process-wide Settings singleton (which races with parallel test files —
191
+ * see issue #2582). Production wires this through the host's Settings
192
+ * reader and updates it on the relevant setting change. */
193
+ magicKeywordsEnabledOverride: boolean | undefined;
194
+
195
+ /** Whether the shimmer should advance this frame. Defaults to "on" before
196
+ * settings have initialised (tests, early boot) so the animation does not
197
+ * silently disappear during a race; settings disabling the feature wins
198
+ * once they are loaded. An explicit `magicKeywordsEnabledOverride` overrides
199
+ * both paths. */
200
+ #shimmerEnabled(): boolean {
201
+ if (this.magicKeywordsEnabledOverride !== undefined) return this.magicKeywordsEnabledOverride;
202
+ return isSettingsInitialized() ? settings.get("magicKeywords.enabled") : true;
203
+ }
204
+
205
+ /** Bind the host's render request callback. Idempotent — the host wires this
206
+ * once after construction (and again after `setEditorComponent` swaps the
207
+ * editor). Passing `undefined` clears any pending frame. */
208
+ setShimmerRepaintHandler(handler: (() => void) | undefined): void {
209
+ this.#requestShimmerRepaint = handler;
210
+ if (!handler && this.#shimmerTimer) {
211
+ clearTimeout(this.#shimmerTimer);
212
+ this.#shimmerTimer = undefined;
213
+ }
214
+ }
215
+
216
+ /** Schedule one shimmer frame if none is already pending. The next render
217
+ * decides whether to schedule another, so the chain stops by itself when
218
+ * `focused` flips off or the keyword leaves the buffer. */
219
+ #scheduleShimmerFrame(): void {
220
+ if (this.#shimmerTimer || !this.#requestShimmerRepaint) return;
221
+ this.#shimmerTimer = setTimeout(() => {
222
+ this.#shimmerTimer = undefined;
223
+ this.#requestShimmerRepaint?.();
224
+ }, CustomEditor.SHIMMER_FRAME_MS);
225
+ this.#shimmerTimer.unref?.();
226
+ }
152
227
  onEscape?: () => void;
153
228
  onClear?: () => void;
154
229
  onExit?: () => void;
@@ -178,9 +253,25 @@ export class CustomEditor extends Editor {
178
253
  /** Called when left-arrow is pressed while the editor is empty (cursor necessarily at start). */
179
254
  onLeftAtStart?: () => void;
180
255
 
256
+ /** Fired when a sustained space-bar hold is recognized — the push-to-talk STT start. The
257
+ * optimistically-typed spaces have already been deleted by the time this runs. */
258
+ onSpaceHoldStart?: () => void;
259
+ /** Fired when the held space bar is released (detected as an idle gap with no further repeated
260
+ * spaces) — the push-to-talk STT stop. */
261
+ onSpaceHoldEnd?: () => void;
262
+ /** Gate for the space-hold gesture. Returns false to keep the space bar inserting spaces
263
+ * normally; wired to `stt.enabled` so disabling STT restores plain space behavior. */
264
+ sttHoldEnabled?: () => boolean;
265
+
181
266
  /** Custom key handlers from extensions and non-built-in app actions. */
182
267
  #customKeyHandlers = new Map<KeyId, () => void>();
183
268
  #customMatchKeys = new Map<string, () => void>();
269
+ /** Consecutive plain spaces inserted in the current run; any other key resets it. */
270
+ #spaceRunInserted = 0;
271
+ /** True while a recognized space-hold push-to-talk recording is in progress. */
272
+ #spaceHoldActive = false;
273
+ /** Idle timer that fires `onSpaceHoldEnd` once repeated spaces stop arriving. */
274
+ #spaceHoldTimer: NodeJS.Timeout | undefined;
184
275
  #actionKeys = new Map<ConfigurableEditorAction, KeyId[]>(
185
276
  Object.entries(DEFAULT_ACTION_KEYS).map(([action, keys]) => [action as ConfigurableEditorAction, [...keys]]),
186
277
  );
@@ -238,6 +329,68 @@ export class CustomEditor extends Editor {
238
329
  this.#rebuildCustomMatchKeys();
239
330
  }
240
331
 
332
+ #spaceHoldGestureEnabled(): boolean {
333
+ return this.onSpaceHoldStart !== undefined && (this.sttHoldEnabled?.() ?? false) && !this.isShowingAutocomplete();
334
+ }
335
+
336
+ /** Drive the space-hold push-to-talk state machine. Returns true when the gesture consumed the
337
+ * input so it must not reach normal editing. Holding the space bar makes the terminal emit a
338
+ * burst of auto-repeat spaces; once more than {@link SPACE_HOLD_THRESHOLD} of them land we treat
339
+ * it as a hold, delete the spam, and start recording until the repeats stop. */
340
+ #handleSpaceHold(data: string, canonical: string | undefined): boolean {
341
+ const isSpace = canonical === "space";
342
+ if (this.#spaceHoldActive) {
343
+ if (isSpace) {
344
+ // Auto-repeat while held: swallow it and keep the release timer alive.
345
+ this.#armSpaceHoldReleaseTimer();
346
+ return true;
347
+ }
348
+ // Any non-space means the bar was released — stop recording, then let the key through.
349
+ this.#endSpaceHold();
350
+ return false;
351
+ }
352
+ if (!isSpace) {
353
+ this.#spaceRunInserted = 0;
354
+ return false;
355
+ }
356
+ if (!this.#spaceHoldGestureEnabled()) return false;
357
+ // A short tap should still type a normal space, so insert optimistically and count the run.
358
+ super.handleInput(data);
359
+ this.#spaceRunInserted++;
360
+ if (this.#spaceRunInserted > SPACE_HOLD_THRESHOLD) {
361
+ this.deleteBeforeCursor(this.#spaceRunInserted);
362
+ this.#spaceRunInserted = 0;
363
+ this.#beginSpaceHold();
364
+ }
365
+ return true;
366
+ }
367
+
368
+ #beginSpaceHold(): void {
369
+ this.#spaceHoldActive = true;
370
+ this.#armSpaceHoldReleaseTimer();
371
+ this.onSpaceHoldStart?.();
372
+ }
373
+
374
+ #armSpaceHoldReleaseTimer(): void {
375
+ if (this.#spaceHoldTimer) clearTimeout(this.#spaceHoldTimer);
376
+ this.#spaceHoldTimer = setTimeout(() => {
377
+ this.#spaceHoldTimer = undefined;
378
+ this.#endSpaceHold();
379
+ }, SPACE_HOLD_RELEASE_MS);
380
+ this.#spaceHoldTimer.unref?.();
381
+ }
382
+
383
+ #endSpaceHold(): void {
384
+ if (!this.#spaceHoldActive) return;
385
+ this.#spaceHoldActive = false;
386
+ this.#spaceRunInserted = 0;
387
+ if (this.#spaceHoldTimer) {
388
+ clearTimeout(this.#spaceHoldTimer);
389
+ this.#spaceHoldTimer = undefined;
390
+ }
391
+ this.onSpaceHoldEnd?.();
392
+ }
393
+
241
394
  handleInput(data: string): void {
242
395
  const kittyParsed = parseKittySequence(data);
243
396
  if (kittyParsed && (kittyParsed.modifier & 64) !== 0 && this.onCapsLock) {
@@ -267,6 +420,9 @@ export class CustomEditor extends Editor {
267
420
  return;
268
421
  }
269
422
 
423
+ // Space-hold push-to-talk: a sustained space bar starts/stops STT instead of typing spaces.
424
+ if (this.#handleSpaceHold(data, canonical)) return;
425
+
270
426
  if (canonical !== undefined) {
271
427
  // Intercept configured image paste (async - fires and handles result)
272
428
  if (this.#matchesAction(canonical, "app.clipboard.pasteImage") && this.onPasteImage) {
@@ -16,6 +16,7 @@ export * from "./hook-message";
16
16
  export * from "./hook-selector";
17
17
  export * from "./keybinding-hints";
18
18
  export * from "./login-dialog";
19
+ export * from "./logout-account-selector";
19
20
  export * from "./model-selector";
20
21
  export * from "./oauth-selector";
21
22
  export * from "./queue-mode-selector";
@@ -0,0 +1,130 @@
1
+ import { Container, matchesKey, ScrollView, Spacer, TruncatedText } from "@oh-my-pi/pi-tui";
2
+ import { theme } from "../../modes/theme/theme";
3
+ import { matchesSelectCancel, matchesSelectDown, matchesSelectUp } from "../../modes/utils/keybinding-matchers";
4
+ import type { LogoutAccount } from "../../slash-commands/helpers/logout";
5
+ import { DynamicBorder } from "./dynamic-border";
6
+
7
+ const LOGOUT_SELECTOR_MAX_VISIBLE = 10;
8
+
9
+ /** Account picker for `/logout` after the provider has been selected. */
10
+ export class LogoutAccountSelectorComponent extends Container {
11
+ #listContainer: Container;
12
+ #accounts: LogoutAccount[];
13
+ #selectedIndex = 0;
14
+ #statusMessage: string | undefined;
15
+ #onSelectCallback: (account: LogoutAccount) => void;
16
+ #onCancelCallback: () => void;
17
+
18
+ constructor(
19
+ providerName: string,
20
+ accounts: LogoutAccount[],
21
+ onSelect: (account: LogoutAccount) => void,
22
+ onCancel: () => void,
23
+ ) {
24
+ super();
25
+ this.#accounts = accounts;
26
+ this.#onSelectCallback = onSelect;
27
+ this.#onCancelCallback = onCancel;
28
+ const activeIndex = accounts.findIndex(account => account.active);
29
+ this.#selectedIndex = activeIndex >= 0 ? activeIndex : 0;
30
+
31
+ this.addChild(new DynamicBorder());
32
+ this.addChild(new Spacer(1));
33
+ this.addChild(new TruncatedText(theme.bold(`Select ${providerName} account to log out:`)));
34
+ this.addChild(new Spacer(1));
35
+ this.#listContainer = new Container();
36
+ this.addChild(this.#listContainer);
37
+ this.addChild(new Spacer(1));
38
+ this.addChild(new DynamicBorder());
39
+ this.#updateList();
40
+ }
41
+
42
+ #updateList(): void {
43
+ this.#listContainer.clear();
44
+
45
+ const total = this.#accounts.length;
46
+ const maxVisible = LOGOUT_SELECTOR_MAX_VISIBLE;
47
+ const startIndex =
48
+ total <= maxVisible
49
+ ? 0
50
+ : Math.max(0, Math.min(this.#selectedIndex - Math.floor(maxVisible / 2), total - maxVisible));
51
+ const endIndex = Math.min(startIndex + maxVisible, total);
52
+
53
+ const rows: string[] = [];
54
+ for (let i = startIndex; i < endIndex; i++) {
55
+ const account = this.#accounts[i];
56
+ if (!account) continue;
57
+ const activeTag = account.active ? theme.fg("muted", " (active)") : "";
58
+ const detail = account.detail ? theme.fg("dim", ` ${account.detail}`) : "";
59
+ if (i === this.#selectedIndex) {
60
+ rows.push(`${theme.fg("accent", `${theme.nav.cursor} ${account.label}`)}${activeTag}${detail}`);
61
+ } else {
62
+ rows.push(` ${account.label}${activeTag}${detail}`);
63
+ }
64
+ }
65
+
66
+ if (rows.length > 0) {
67
+ const sv = new ScrollView(rows, {
68
+ height: rows.length,
69
+ scrollbar: "auto",
70
+ totalRows: total,
71
+ theme: { track: text => theme.fg("muted", text), thumb: text => theme.fg("accent", text) },
72
+ });
73
+ sv.setScrollOffset(startIndex);
74
+ this.#listContainer.addChild(sv);
75
+ }
76
+
77
+ if (total === 0) {
78
+ this.#listContainer.addChild(new TruncatedText(theme.fg("muted", " No stored accounts to log out"), 0, 0));
79
+ }
80
+
81
+ this.#listContainer.addChild(
82
+ new TruncatedText(theme.fg("muted", " ↑/↓ select · ↵ log out account · Esc cancel"), 0, 0),
83
+ );
84
+
85
+ if (this.#statusMessage) {
86
+ this.#listContainer.addChild(new Spacer(1));
87
+ this.#listContainer.addChild(new TruncatedText(theme.fg("warning", ` ${this.#statusMessage}`), 0, 0));
88
+ }
89
+ }
90
+
91
+ handleInput(keyData: string): void {
92
+ if (matchesSelectCancel(keyData)) {
93
+ this.#onCancelCallback();
94
+ return;
95
+ }
96
+
97
+ if (matchesSelectUp(keyData)) {
98
+ if (this.#accounts.length > 0) {
99
+ this.#selectedIndex = this.#selectedIndex === 0 ? this.#accounts.length - 1 : this.#selectedIndex - 1;
100
+ }
101
+ this.#statusMessage = undefined;
102
+ this.#updateList();
103
+ } else if (matchesSelectDown(keyData)) {
104
+ if (this.#accounts.length > 0) {
105
+ this.#selectedIndex = this.#selectedIndex === this.#accounts.length - 1 ? 0 : this.#selectedIndex + 1;
106
+ }
107
+ this.#statusMessage = undefined;
108
+ this.#updateList();
109
+ } else if (matchesKey(keyData, "pageUp")) {
110
+ if (this.#accounts.length > 0) {
111
+ this.#selectedIndex = Math.max(0, this.#selectedIndex - LOGOUT_SELECTOR_MAX_VISIBLE);
112
+ }
113
+ this.#statusMessage = undefined;
114
+ this.#updateList();
115
+ } else if (matchesKey(keyData, "pageDown")) {
116
+ if (this.#accounts.length > 0) {
117
+ this.#selectedIndex = Math.min(
118
+ this.#accounts.length - 1,
119
+ this.#selectedIndex + LOGOUT_SELECTOR_MAX_VISIBLE,
120
+ );
121
+ }
122
+ this.#statusMessage = undefined;
123
+ this.#updateList();
124
+ } else if (matchesKey(keyData, "enter") || matchesKey(keyData, "return") || keyData === "\n") {
125
+ const account = this.#accounts[this.#selectedIndex];
126
+ if (!account) return;
127
+ this.#onSelectCallback(account);
128
+ }
129
+ }
130
+ }
@@ -262,7 +262,7 @@ export class MCPAddWizard extends Container {
262
262
  }
263
263
 
264
264
  this.#contentContainer.addChild(
265
- new Text(theme.fg("muted", "[Only letters, numbers, dash, underscore, dot]"), 0, 0),
265
+ new Text(theme.fg("muted", "[Only letters, numbers, dash, underscore, dot, colon]"), 0, 0),
266
266
  );
267
267
  this.#contentContainer.addChild(new Text(theme.fg("muted", "[Enter to continue, Esc to cancel]"), 0, 0));
268
268
  }
@@ -711,7 +711,7 @@ export class ModelSelectorComponent extends Container {
711
711
  if (!this.#isModelOverContextLimit(model)) {
712
712
  return "";
713
713
  }
714
- return ` ${theme.status.disabled} context>${formatNumber(model.contextWindow).toLowerCase()}`;
714
+ return ` ${theme.status.disabled} context>${formatNumber(model.contextWindow ?? 0).toLowerCase()}`;
715
715
  }
716
716
 
717
717
  #getVisibleItems(): ReadonlyArray<ModelItem | CanonicalModelItem> {
@@ -1016,7 +1016,7 @@ export class ModelSelectorComponent extends Container {
1016
1016
  const limitWarning = this.#isItemDisabled(selected)
1017
1017
  ? theme.fg(
1018
1018
  "dim",
1019
- ` — current context ${formatNumber(this.#currentContextTokens).toLowerCase()} > ${formatNumber(selected.model.contextWindow).toLowerCase()} limit`,
1019
+ ` — current context ${formatNumber(this.#currentContextTokens).toLowerCase()} > ${formatNumber(selected.model.contextWindow ?? 0).toLowerCase()} limit`,
1020
1020
  )
1021
1021
  : "";
1022
1022
  this.#listContainer.addChild(
@@ -15,7 +15,7 @@ import {
15
15
  import { formatBytes } from "@oh-my-pi/pi-utils";
16
16
  import { theme } from "../../modes/theme/theme";
17
17
  import { matchesAppInterrupt, matchesSelectDown, matchesSelectUp } from "../../modes/utils/keybinding-matchers";
18
- import type { SessionInfo, SessionStatus } from "../../session/session-manager";
18
+ import type { SessionInfo, SessionStatus } from "../../session/session-listing";
19
19
  import { shortenPath } from "../../tools/render-utils";
20
20
  import { DynamicBorder } from "./dynamic-border";
21
21
  import { HookSelectorComponent } from "./hook-selector";