@kolisachint/hoocode-agent 0.4.106 → 0.4.107

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 (289) hide show
  1. package/CHANGELOG.md +2 -0
  2. package/dist/cli/args.d.ts.map +1 -1
  3. package/dist/cli/args.js +4 -4
  4. package/dist/cli/args.js.map +1 -1
  5. package/dist/cli.d.ts.map +1 -1
  6. package/dist/cli.js +0 -1
  7. package/dist/cli.js.map +1 -1
  8. package/dist/config.d.ts.map +1 -1
  9. package/dist/config.js +2 -2
  10. package/dist/config.js.map +1 -1
  11. package/dist/core/agent-session.d.ts +1 -3
  12. package/dist/core/agent-session.d.ts.map +1 -1
  13. package/dist/core/agent-session.js +2 -2
  14. package/dist/core/agent-session.js.map +1 -1
  15. package/dist/core/bash-executor.d.ts.map +1 -1
  16. package/dist/core/bash-executor.js +1 -1
  17. package/dist/core/bash-executor.js.map +1 -1
  18. package/dist/core/extension-mcp-servers.d.ts +1 -1
  19. package/dist/core/extension-mcp-servers.d.ts.map +1 -1
  20. package/dist/core/extension-mcp-servers.js +1 -1
  21. package/dist/core/extension-mcp-servers.js.map +1 -1
  22. package/dist/core/extensions/plugins/index.d.ts.map +1 -1
  23. package/dist/core/extensions/plugins/index.js +1 -1
  24. package/dist/core/extensions/plugins/index.js.map +1 -1
  25. package/dist/core/extensions/types.d.ts +1 -3
  26. package/dist/core/extensions/types.d.ts.map +1 -1
  27. package/dist/core/extensions/types.js.map +1 -1
  28. package/dist/core/index.d.ts +1 -1
  29. package/dist/core/index.d.ts.map +1 -1
  30. package/dist/core/index.js.map +1 -1
  31. package/dist/core/mode-prompts.d.ts +1 -1
  32. package/dist/core/mode-prompts.d.ts.map +1 -1
  33. package/dist/core/mode-prompts.js +1 -1
  34. package/dist/core/mode-prompts.js.map +1 -1
  35. package/dist/core/package-manager.d.ts.map +1 -1
  36. package/dist/core/package-manager.js +1 -1
  37. package/dist/core/package-manager.js.map +1 -1
  38. package/dist/core/sdk.d.ts.map +1 -1
  39. package/dist/core/sdk.js +1 -2
  40. package/dist/core/sdk.js.map +1 -1
  41. package/dist/core/session-manager.d.ts +2 -97
  42. package/dist/core/session-manager.d.ts.map +1 -1
  43. package/dist/core/session-manager.js +4 -65
  44. package/dist/core/session-manager.js.map +1 -1
  45. package/dist/core/settings-manager.d.ts.map +1 -1
  46. package/dist/core/settings-manager.js +2 -3
  47. package/dist/core/settings-manager.js.map +1 -1
  48. package/dist/core/telemetry.d.ts.map +1 -1
  49. package/dist/core/telemetry.js +1 -1
  50. package/dist/core/telemetry.js.map +1 -1
  51. package/dist/core/timings.d.ts +1 -1
  52. package/dist/core/timings.d.ts.map +1 -1
  53. package/dist/core/timings.js +2 -2
  54. package/dist/core/timings.js.map +1 -1
  55. package/dist/core/tools/{browser-continue.d.ts → browser/browser-continue.d.ts} +1 -1
  56. package/dist/core/tools/browser/browser-continue.d.ts.map +1 -0
  57. package/dist/core/tools/{browser-continue.js → browser/browser-continue.js} +2 -2
  58. package/dist/core/tools/browser/browser-continue.js.map +1 -0
  59. package/dist/core/tools/{browser-run.d.ts → browser/browser-run.d.ts} +2 -2
  60. package/dist/core/tools/browser/browser-run.d.ts.map +1 -0
  61. package/dist/core/tools/{browser-run.js → browser/browser-run.js} +2 -2
  62. package/dist/core/tools/browser/browser-run.js.map +1 -0
  63. package/dist/core/tools/browser/browsertools-shared.d.ts.map +1 -0
  64. package/dist/core/tools/{browsertools-shared.js → browser/browsertools-shared.js} +1 -1
  65. package/dist/core/tools/browser/browsertools-shared.js.map +1 -0
  66. package/dist/core/tools/browser/index.d.ts +8 -0
  67. package/dist/core/tools/browser/index.d.ts.map +1 -0
  68. package/dist/core/tools/browser/index.js +8 -0
  69. package/dist/core/tools/browser/index.js.map +1 -0
  70. package/dist/core/tools/{docedit.d.ts → doc/docedit.d.ts} +2 -2
  71. package/dist/core/tools/doc/docedit.d.ts.map +1 -0
  72. package/dist/core/tools/{docedit.js → doc/docedit.js} +7 -7
  73. package/dist/core/tools/doc/docedit.js.map +1 -0
  74. package/dist/core/tools/{docgrep.d.ts → doc/docgrep.d.ts} +2 -2
  75. package/dist/core/tools/doc/docgrep.d.ts.map +1 -0
  76. package/dist/core/tools/{docgrep.js → doc/docgrep.js} +6 -6
  77. package/dist/core/tools/doc/docgrep.js.map +1 -0
  78. package/dist/core/tools/{docpeek.d.ts → doc/docpeek.d.ts} +2 -2
  79. package/dist/core/tools/{docpeek.js.map → doc/docpeek.d.ts.map} +1 -1
  80. package/dist/core/tools/{docpeek.js → doc/docpeek.js} +6 -6
  81. package/dist/core/tools/doc/docpeek.js.map +1 -0
  82. package/dist/core/tools/{docread.d.ts → doc/docread.d.ts} +2 -2
  83. package/dist/core/tools/doc/docread.d.ts.map +1 -0
  84. package/dist/core/tools/{docread.js → doc/docread.js} +6 -6
  85. package/dist/core/tools/doc/docread.js.map +1 -0
  86. package/dist/core/tools/{docscan.d.ts → doc/docscan.d.ts} +2 -2
  87. package/dist/core/tools/doc/docscan.d.ts.map +1 -0
  88. package/dist/core/tools/{docscan.js → doc/docscan.js} +6 -6
  89. package/dist/core/tools/doc/docscan.js.map +1 -0
  90. package/dist/core/tools/{docwrite.d.ts → doc/docwrite.d.ts} +1 -1
  91. package/dist/core/tools/doc/docwrite.d.ts.map +1 -0
  92. package/dist/core/tools/{docwrite.js → doc/docwrite.js} +6 -6
  93. package/dist/core/tools/doc/docwrite.js.map +1 -0
  94. package/dist/core/tools/doc/index.d.ts +11 -0
  95. package/dist/core/tools/doc/index.d.ts.map +1 -0
  96. package/dist/core/tools/doc/index.js +11 -0
  97. package/dist/core/tools/doc/index.js.map +1 -0
  98. package/dist/core/tools/grep.d.ts.map +1 -1
  99. package/dist/core/tools/grep.js +1 -1
  100. package/dist/core/tools/grep.js.map +1 -1
  101. package/dist/core/tools/index.d.ts +42 -28
  102. package/dist/core/tools/index.d.ts.map +1 -1
  103. package/dist/core/tools/index.js +61 -199
  104. package/dist/core/tools/index.js.map +1 -1
  105. package/dist/core/tools/output-accumulator.d.ts.map +1 -1
  106. package/dist/core/tools/output-accumulator.js +1 -1
  107. package/dist/core/tools/output-accumulator.js.map +1 -1
  108. package/dist/core/tools/read.d.ts.map +1 -1
  109. package/dist/core/tools/read.js +1 -1
  110. package/dist/core/tools/read.js.map +1 -1
  111. package/dist/extensions/core/ask-options.d.ts +10 -0
  112. package/dist/extensions/core/ask-options.d.ts.map +1 -0
  113. package/dist/extensions/core/ask-options.js +83 -0
  114. package/dist/extensions/core/ask-options.js.map +1 -0
  115. package/dist/extensions/core/config.d.ts +80 -0
  116. package/dist/extensions/core/config.d.ts.map +1 -0
  117. package/dist/extensions/core/config.js +110 -0
  118. package/dist/extensions/core/config.js.map +1 -0
  119. package/dist/extensions/core/cost.d.ts +11 -0
  120. package/dist/extensions/core/cost.d.ts.map +1 -0
  121. package/dist/extensions/core/cost.js +64 -0
  122. package/dist/extensions/core/cost.js.map +1 -0
  123. package/dist/extensions/core/hoo-core.d.ts +16 -179
  124. package/dist/extensions/core/hoo-core.d.ts.map +1 -1
  125. package/dist/extensions/core/hoo-core.js +27 -1663
  126. package/dist/extensions/core/hoo-core.js.map +1 -1
  127. package/dist/extensions/core/loop.d.ts +15 -0
  128. package/dist/extensions/core/loop.d.ts.map +1 -0
  129. package/dist/extensions/core/loop.js +235 -0
  130. package/dist/extensions/core/loop.js.map +1 -0
  131. package/dist/extensions/core/marketplace.d.ts +17 -0
  132. package/dist/extensions/core/marketplace.d.ts.map +1 -0
  133. package/dist/extensions/core/marketplace.js +167 -0
  134. package/dist/extensions/core/marketplace.js.map +1 -0
  135. package/dist/extensions/core/mcp-loader.d.ts +26 -0
  136. package/dist/extensions/core/mcp-loader.d.ts.map +1 -0
  137. package/dist/extensions/core/mcp-loader.js +391 -0
  138. package/dist/extensions/core/mcp-loader.js.map +1 -0
  139. package/dist/extensions/core/modes.d.ts +56 -0
  140. package/dist/extensions/core/modes.d.ts.map +1 -0
  141. package/dist/extensions/core/modes.js +268 -0
  142. package/dist/extensions/core/modes.js.map +1 -0
  143. package/dist/extensions/core/permission-gate.d.ts +10 -0
  144. package/dist/extensions/core/permission-gate.d.ts.map +1 -0
  145. package/dist/extensions/core/permission-gate.js +167 -0
  146. package/dist/extensions/core/permission-gate.js.map +1 -0
  147. package/dist/extensions/core/scaffold.d.ts +9 -0
  148. package/dist/extensions/core/scaffold.d.ts.map +1 -0
  149. package/dist/extensions/core/scaffold.js +146 -0
  150. package/dist/extensions/core/scaffold.js.map +1 -0
  151. package/dist/extensions/core/thinking-escalation.d.ts +18 -0
  152. package/dist/extensions/core/thinking-escalation.d.ts.map +1 -0
  153. package/dist/extensions/core/thinking-escalation.js +70 -0
  154. package/dist/extensions/core/thinking-escalation.js.map +1 -0
  155. package/dist/index.d.ts +2 -3
  156. package/dist/index.d.ts.map +1 -1
  157. package/dist/index.js +3 -4
  158. package/dist/index.js.map +1 -1
  159. package/dist/main.d.ts.map +1 -1
  160. package/dist/main.js +2 -2
  161. package/dist/main.js.map +1 -1
  162. package/dist/modes/interactive/command-executor.d.ts +0 -5
  163. package/dist/modes/interactive/command-executor.d.ts.map +1 -1
  164. package/dist/modes/interactive/command-executor.js +0 -7
  165. package/dist/modes/interactive/command-executor.js.map +1 -1
  166. package/dist/modes/interactive/components/branch-summary-message.d.ts +1 -1
  167. package/dist/modes/interactive/components/branch-summary-message.d.ts.map +1 -1
  168. package/dist/modes/interactive/components/branch-summary-message.js.map +1 -1
  169. package/dist/modes/interactive/components/compaction-summary-message.d.ts +1 -1
  170. package/dist/modes/interactive/components/compaction-summary-message.d.ts.map +1 -1
  171. package/dist/modes/interactive/components/compaction-summary-message.js.map +1 -1
  172. package/dist/modes/interactive/components/custom-message.d.ts +1 -1
  173. package/dist/modes/interactive/components/custom-message.d.ts.map +1 -1
  174. package/dist/modes/interactive/components/custom-message.js.map +1 -1
  175. package/dist/modes/interactive/components/index.d.ts +0 -2
  176. package/dist/modes/interactive/components/index.d.ts.map +1 -1
  177. package/dist/modes/interactive/components/index.js +0 -2
  178. package/dist/modes/interactive/components/index.js.map +1 -1
  179. package/dist/modes/interactive/extension-chrome.d.ts +56 -0
  180. package/dist/modes/interactive/extension-chrome.d.ts.map +1 -0
  181. package/dist/modes/interactive/extension-chrome.js +172 -0
  182. package/dist/modes/interactive/extension-chrome.js.map +1 -0
  183. package/dist/modes/interactive/extension-dialogs.d.ts +82 -0
  184. package/dist/modes/interactive/extension-dialogs.d.ts.map +1 -0
  185. package/dist/modes/interactive/extension-dialogs.js +259 -0
  186. package/dist/modes/interactive/extension-dialogs.js.map +1 -0
  187. package/dist/modes/interactive/interactive-mode.d.ts +11 -161
  188. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  189. package/dist/modes/interactive/interactive-mode.js +261 -1584
  190. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  191. package/dist/modes/interactive/resource-display.d.ts +59 -0
  192. package/dist/modes/interactive/resource-display.d.ts.map +1 -0
  193. package/dist/modes/interactive/resource-display.js +499 -0
  194. package/dist/modes/interactive/resource-display.js.map +1 -0
  195. package/dist/modes/interactive/startup-checks.d.ts +17 -0
  196. package/dist/modes/interactive/startup-checks.d.ts.map +1 -0
  197. package/dist/modes/interactive/startup-checks.js +102 -0
  198. package/dist/modes/interactive/startup-checks.js.map +1 -0
  199. package/dist/modes/interactive/team-focus.d.ts +56 -0
  200. package/dist/modes/interactive/team-focus.d.ts.map +1 -0
  201. package/dist/modes/interactive/team-focus.js +167 -0
  202. package/dist/modes/interactive/team-focus.js.map +1 -0
  203. package/dist/modes/interactive/voice/voice-controller.d.ts +66 -0
  204. package/dist/modes/interactive/voice/voice-controller.d.ts.map +1 -0
  205. package/dist/modes/interactive/voice/voice-controller.js +312 -0
  206. package/dist/modes/interactive/voice/voice-controller.js.map +1 -0
  207. package/dist/modes/interactive/voice/voice-panel.d.ts.map +1 -0
  208. package/dist/modes/interactive/voice/voice-panel.js.map +1 -0
  209. package/dist/modes/interactive/voice/voice-transcribe.d.ts.map +1 -0
  210. package/dist/modes/interactive/voice/voice-transcribe.js.map +1 -0
  211. package/dist/modes/rpc/rpc-client.d.ts +1 -2
  212. package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
  213. package/dist/modes/rpc/rpc-client.js.map +1 -1
  214. package/dist/modes/rpc/rpc-types.d.ts +1 -2
  215. package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
  216. package/dist/modes/rpc/rpc-types.js.map +1 -1
  217. package/dist/resources-cli.d.ts.map +1 -1
  218. package/dist/resources-cli.js +1 -1
  219. package/dist/resources-cli.js.map +1 -1
  220. package/dist/utils/tools-manager.d.ts.map +1 -1
  221. package/dist/utils/tools-manager.js +1 -1
  222. package/dist/utils/tools-manager.js.map +1 -1
  223. package/dist/utils/version-check.d.ts.map +1 -1
  224. package/dist/utils/version-check.js +2 -2
  225. package/dist/utils/version-check.js.map +1 -1
  226. package/docs/project-local-resources.md +1 -1
  227. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  228. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  229. package/examples/extensions/sandbox/package.json +1 -1
  230. package/examples/extensions/with-deps/package.json +1 -1
  231. package/package.json +4 -4
  232. package/dist/core/compaction/branch-summarization.d.ts +0 -88
  233. package/dist/core/compaction/branch-summarization.d.ts.map +0 -1
  234. package/dist/core/compaction/branch-summarization.js +0 -243
  235. package/dist/core/compaction/branch-summarization.js.map +0 -1
  236. package/dist/core/compaction/compaction.d.ts +0 -123
  237. package/dist/core/compaction/compaction.d.ts.map +0 -1
  238. package/dist/core/compaction/compaction.js +0 -635
  239. package/dist/core/compaction/compaction.js.map +0 -1
  240. package/dist/core/compaction/index.d.ts +0 -7
  241. package/dist/core/compaction/index.d.ts.map +0 -1
  242. package/dist/core/compaction/index.js +0 -7
  243. package/dist/core/compaction/index.js.map +0 -1
  244. package/dist/core/compaction/utils.d.ts +0 -38
  245. package/dist/core/compaction/utils.d.ts.map +0 -1
  246. package/dist/core/compaction/utils.js +0 -156
  247. package/dist/core/compaction/utils.js.map +0 -1
  248. package/dist/core/messages.d.ts +0 -124
  249. package/dist/core/messages.d.ts.map +0 -1
  250. package/dist/core/messages.js +0 -221
  251. package/dist/core/messages.js.map +0 -1
  252. package/dist/core/tools/browser-continue.d.ts.map +0 -1
  253. package/dist/core/tools/browser-continue.js.map +0 -1
  254. package/dist/core/tools/browser-run.d.ts.map +0 -1
  255. package/dist/core/tools/browser-run.js.map +0 -1
  256. package/dist/core/tools/browsertools-shared.d.ts.map +0 -1
  257. package/dist/core/tools/browsertools-shared.js.map +0 -1
  258. package/dist/core/tools/docedit.d.ts.map +0 -1
  259. package/dist/core/tools/docedit.js.map +0 -1
  260. package/dist/core/tools/docgrep.d.ts.map +0 -1
  261. package/dist/core/tools/docgrep.js.map +0 -1
  262. package/dist/core/tools/docpeek.d.ts.map +0 -1
  263. package/dist/core/tools/docread.d.ts.map +0 -1
  264. package/dist/core/tools/docread.js.map +0 -1
  265. package/dist/core/tools/docscan.d.ts.map +0 -1
  266. package/dist/core/tools/docscan.js.map +0 -1
  267. package/dist/core/tools/docwrite.d.ts.map +0 -1
  268. package/dist/core/tools/docwrite.js.map +0 -1
  269. package/dist/core/tools/output-compression.d.ts +0 -53
  270. package/dist/core/tools/output-compression.d.ts.map +0 -1
  271. package/dist/core/tools/output-compression.js +0 -392
  272. package/dist/core/tools/output-compression.js.map +0 -1
  273. package/dist/modes/interactive/components/armin.d.ts +0 -34
  274. package/dist/modes/interactive/components/armin.d.ts.map +0 -1
  275. package/dist/modes/interactive/components/armin.js +0 -333
  276. package/dist/modes/interactive/components/armin.js.map +0 -1
  277. package/dist/modes/interactive/components/daxnuts.d.ts +0 -23
  278. package/dist/modes/interactive/components/daxnuts.d.ts.map +0 -1
  279. package/dist/modes/interactive/components/daxnuts.js +0 -140
  280. package/dist/modes/interactive/components/daxnuts.js.map +0 -1
  281. package/dist/modes/interactive/components/voice-panel.d.ts.map +0 -1
  282. package/dist/modes/interactive/components/voice-panel.js.map +0 -1
  283. package/dist/modes/interactive/voice-transcribe.d.ts.map +0 -1
  284. package/dist/modes/interactive/voice-transcribe.js.map +0 -1
  285. /package/dist/core/tools/{browsertools-shared.d.ts → browser/browsertools-shared.d.ts} +0 -0
  286. /package/dist/modes/interactive/{components → voice}/voice-panel.d.ts +0 -0
  287. /package/dist/modes/interactive/{components → voice}/voice-panel.js +0 -0
  288. /package/dist/modes/interactive/{voice-transcribe.d.ts → voice/voice-transcribe.d.ts} +0 -0
  289. /package/dist/modes/interactive/{voice-transcribe.js → voice/voice-transcribe.js} +0 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"voice-controller.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/voice/voice-controller.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,GAAG,EAAE,MAAM,0BAA0B,CAAC;AAoB/D,iEAAiE;AACjE,MAAM,WAAW,mBAAmB;IACnC,EAAE,EAAE,GAAG,CAAC;IACR,mEAAmE;IACnE,eAAe,EAAE,SAAS,CAAC;IAC3B,6DAA6D;IAC7D,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpC,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CACjC;AAED,qBAAa,eAAe;IAef,OAAO,CAAC,QAAQ,CAAC,IAAI;IAdjC,0DAA0D;IAC1D,OAAO,CAAC,OAAO,CAA2B;IAG1C,OAAO,CAAC,MAAM,CAA0B;IACxC,gFAAgF;IAChF,OAAO,CAAC,iBAAiB,CAAS;IAClC,6FAA6F;IAC7F,OAAO,CAAC,QAAQ,CAAS;IACzB,kEAAkE;IAClE,OAAO,CAAC,MAAM,CAAS;IACvB,oDAAoD;IACpD,OAAO,CAAC,KAAK,CAAyB;IAEtC,YAA6B,IAAI,EAAE,mBAAmB,EAAI;IAE1D,oCAAoC;IACpC,MAAM,IAAI,IAAI,CAmGb;IAED,kEAAkE;IAClE,OAAO,IAAI,IAAI,CAad;IAED;;;;;;;;;;;;;;OAcG;IACH,OAAO,CAAC,mBAAmB;IA2D3B,uFAAuF;IACvF,OAAO,CAAC,UAAU;IAIlB,OAAO,CAAC,kBAAkB;IAO1B,OAAO,CAAC,kBAAkB;YAgCZ,UAAU;IAMxB,8EAA8E;IAC9E,OAAO,CAAC,SAAS;IAUjB,OAAO,CAAC,WAAW;IAInB,8EAA8E;IAC9E,OAAO,CAAC,OAAO;CAQf","sourcesContent":["/**\n * Voice-to-text capture control (ctrl+r / app.input.voiceTranscribe).\n *\n * Prefers a persistent `voicetools serve` daemon: the first press probes for\n * support and (if found) loads models once, showing a \"warming up\" spinner;\n * every later press reuses the already-warm daemon and jumps straight to\n * Listening. Binaries without `serve` support fall back to spawning\n * `voicetools transcribe` per press. Pressing the shortcut again while\n * listening (or while still warming up) cancels.\n */\n\nimport type { Container, TUI } from \"@kolisachint/hoocode-tui\";\nimport { ensureTool } from \"../../../utils/tools-manager.js\";\nimport { keyHint } from \"../components/keybinding-hints.js\";\nimport { VoicePanel } from \"./voice-panel.js\";\nimport { startVoiceTranscribe, VoiceDaemon, type VoiceDaemonHandlers, type VoiceSession } from \"./voice-transcribe.js\";\n\n// Trailing-silence window: how long a pause while speaking lasts before the\n// capture auto-stops. Passed to `voicetools serve` via `--silence-ms` (see\n// VoiceDaemon.spawn) so the binary's real cutoff matches the on-screen\n// countdown this same value drives. Kept generous so a thinking pause mid-\n// sentence doesn't cut the user off (the binary's own default is 600ms).\nconst VOICE_SILENCE_MS = 3000;\n/** How long to keep the warm voice model in memory after the last capture\n * completes. The daemon auto-shuts down after this window, releasing the\n * ~900 MB resident model; the next ctrl+r pays a cold-start respawn cost. */\nconst VOICE_IDLE_TIMEOUT_MS = 60_000;\nconst VOICE_UNAVAILABLE_MESSAGE =\n\t\"Voice input failed: voicetools binary unavailable and could not be downloaded. \" +\n\t\"Install it, set VOICETOOLS_BIN, or ensure a published release exists for this platform.\";\n\n/** The slice of the interactive mode the voice feature needs. */\nexport interface VoiceControllerDeps {\n\tui: TUI;\n\t/** The status area below the chat; the voice panel mounts here. */\n\tstatusContainer: Container;\n\t/** Feed raw input (bracketed paste) to the prompt editor. */\n\tsendEditorInput(data: string): void;\n\tshowError(message: string): void;\n}\n\nexport class VoiceController {\n\t/** In-flight legacy (`voicetools transcribe`) capture. */\n\tprivate session: VoiceSession | undefined;\n\t// Persistent `voicetools serve` process, once probed successfully. Stays alive\n\t// between captures so the model stays warm.\n\tprivate daemon: VoiceDaemon | undefined;\n\t/** Set when the binary rejected `serve`; future presses use the legacy path. */\n\tprivate daemonUnsupported = false;\n\t/** True while the voicetools binary is being resolved/downloaded before a session starts. */\n\tprivate starting = false;\n\t/** True while a capture is active (listening or transcribing). */\n\tprivate active = false;\n\t/** The status panel while voice input is active. */\n\tprivate panel: VoicePanel | undefined;\n\n\tconstructor(private readonly deps: VoiceControllerDeps) {}\n\n\t/** Toggle voice-to-text capture. */\n\ttoggle(): void {\n\t\tif (this.active) {\n\t\t\tif (this.daemon?.isReady) {\n\t\t\t\tthis.daemon.cancel();\n\t\t\t} else {\n\t\t\t\tthis.session?.stop();\n\t\t\t}\n\t\t\tthis.session = undefined;\n\t\t\tthis.active = false;\n\t\t\tthis.resetUI();\n\t\t\treturn;\n\t\t}\n\n\t\tif (this.starting) {\n\t\t\t// A second press while resolving/warming up: honour the cancel. Any\n\t\t\t// daemon that finishes loading afterwards is kept warm for next time.\n\t\t\tthis.starting = false;\n\t\t\tthis.resetUI();\n\t\t\treturn;\n\t\t}\n\n\t\tif (this.daemonUnsupported) {\n\t\t\tthis.starting = true;\n\t\t\tthis.showWarming(\"Starting voice input...\");\n\t\t\tvoid this.resolveBin()\n\t\t\t\t.then((bin) => {\n\t\t\t\t\tif (!this.starting) return;\n\t\t\t\t\tthis.starting = false;\n\t\t\t\t\tif (!bin) {\n\t\t\t\t\t\tthis.resetUI();\n\t\t\t\t\t\tthis.deps.showError(VOICE_UNAVAILABLE_MESSAGE);\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tthis.beginLegacyCapture(bin);\n\t\t\t\t})\n\t\t\t\t.catch((err: unknown) => {\n\t\t\t\t\tthis.starting = false;\n\t\t\t\t\tthis.resetUI();\n\t\t\t\t\tthis.deps.showError(`Voice input failed: ${err instanceof Error ? err.message : String(err)}`);\n\t\t\t\t});\n\t\t\treturn;\n\t\t}\n\n\t\tif (this.daemon?.isReady) {\n\t\t\tthis.beginDaemonCapture();\n\t\t\treturn;\n\t\t}\n\n\t\t// No daemon yet: resolve the binary, then probe for `serve` support by\n\t\t// spawning it. `VoiceDaemon.spawn` doubles as the probe: it resolves to a\n\t\t// live daemon once READY arrives, to \"unsupported\" if the process exits\n\t\t// with no output at all (an old binary rejecting the unrecognized `serve`\n\t\t// subcommand), or to \"error\" if it printed a real ERROR first (e.g. no\n\t\t// model installed yet) — already surfaced via onError, so that case skips\n\t\t// the legacy fallback (it would just hit the same error) but leaves\n\t\t// daemon mode available to retry on the next press.\n\t\tthis.starting = true;\n\t\tthis.showWarming(\"Warming up voice input...\");\n\t\tvoid this.resolveBin()\n\t\t\t.then(async (bin) => {\n\t\t\t\tif (!bin) {\n\t\t\t\t\tthis.starting = false;\n\t\t\t\t\tthis.resetUI();\n\t\t\t\t\tthis.deps.showError(VOICE_UNAVAILABLE_MESSAGE);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst result = await VoiceDaemon.spawn(bin, this.buildDaemonHandlers(), {\n\t\t\t\t\tsilenceMs: VOICE_SILENCE_MS,\n\t\t\t\t\tidleTimeoutMs: VOICE_IDLE_TIMEOUT_MS,\n\t\t\t\t});\n\t\t\t\tif (!result.ok) {\n\t\t\t\t\tif (result.reason === \"unsupported\") {\n\t\t\t\t\t\tthis.daemonUnsupported = true;\n\t\t\t\t\t\tif (!this.starting) {\n\t\t\t\t\t\t\tthis.resetUI();\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tthis.starting = false;\n\t\t\t\t\t\tthis.beginLegacyCapture(bin);\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tthis.starting = false;\n\t\t\t\t\tthis.resetUI();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tthis.daemon = result.daemon;\n\t\t\t\tif (!this.starting) {\n\t\t\t\t\t// Cancelled while warming up: keep the loaded daemon warm for next time.\n\t\t\t\t\tthis.resetUI();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tthis.starting = false;\n\t\t\t\tthis.beginDaemonCapture();\n\t\t\t})\n\t\t\t.catch((err: unknown) => {\n\t\t\t\tthis.starting = false;\n\t\t\t\tthis.resetUI();\n\t\t\t\tthis.deps.showError(`Voice input failed: ${err instanceof Error ? err.message : String(err)}`);\n\t\t\t});\n\t}\n\n\t/** Stop any capture, shut down the daemon, and drop the panel. */\n\tdispose(): void {\n\t\tif (this.session?.running) {\n\t\t\tthis.session.stop();\n\t\t\tthis.session = undefined;\n\t\t}\n\t\tthis.daemon?.shutdown();\n\t\tthis.daemon = undefined;\n\t\tif (this.panel) {\n\t\t\tthis.panel.dispose();\n\t\t\tthis.panel = undefined;\n\t\t}\n\t\tthis.active = false;\n\t\tthis.starting = false;\n\t}\n\n\t/**\n\t * Build the (stable, reused-across-captures) handlers for the daemon.\n\t *\n\t * Every handler bails out once `active` is false: CANCEL is a soft\n\t * request (unlike the legacy path's `proc.kill()`, it doesn't sever the\n\t * pipe), so the daemon can still have a trailing PARTIAL/FINAL/DONE for the\n\t * just-cancelled capture in flight when the user presses cancel. Without\n\t * this guard that stale text would land in the editor after the panel had\n\t * already collapsed.\n\t *\n\t * v0.1.4 serve streams `PARTIAL <full growing hypothesis>` (live preview,\n\t * never committed) and ends with a single `FINAL <complete text>` (the one\n\t * commit to the editor). `SEGMENT` is still handled for the legacy\n\t * transcribe path and any binary that streams committed chunks directly.\n\t */\n\tprivate buildDaemonHandlers(): VoiceDaemonHandlers {\n\t\treturn {\n\t\t\tonSegment: (text) => {\n\t\t\t\tif (!this.active) return;\n\t\t\t\tthis.commitText(text);\n\t\t\t},\n\t\t\tonPartial: (text) => {\n\t\t\t\tif (!this.active) return;\n\t\t\t\tthis.panel?.setPartial(text);\n\t\t\t},\n\t\t\tonFinal: (text) => {\n\t\t\t\tif (!this.active) return;\n\t\t\t\tthis.commitText(text);\n\t\t\t},\n\t\t\tonStatus: (status) => {\n\t\t\t\tif (!this.active) return;\n\t\t\t\tif (status === \"done\") {\n\t\t\t\t\tthis.active = false;\n\t\t\t\t\tthis.resetUI();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (status === \"transcribing\") {\n\t\t\t\t\tthis.panel?.setTranscribing();\n\t\t\t\t} else if (status === \"listening\") {\n\t\t\t\t\tthis.panel?.startListening();\n\t\t\t\t}\n\t\t\t},\n\t\t\tonLevel: (rms) => {\n\t\t\t\tif (!this.active) return;\n\t\t\t\tthis.panel?.pushLevel(rms);\n\t\t\t},\n\t\t\tonPhase: (phase) => {\n\t\t\t\tif (!this.active) return;\n\t\t\t\tif (phase === \"silence\") {\n\t\t\t\t\tthis.panel?.beginSilence(VOICE_SILENCE_MS);\n\t\t\t\t} else {\n\t\t\t\t\tthis.panel?.endSilence();\n\t\t\t\t}\n\t\t\t},\n\t\t\tonError: (message) => {\n\t\t\t\tthis.active = false;\n\t\t\t\tthis.resetUI();\n\t\t\t\tthis.deps.showError(`Voice input failed: ${message}`);\n\t\t\t},\n\t\t\tonCrash: (message) => {\n\t\t\t\tthis.active = false;\n\t\t\t\tthis.daemon = undefined;\n\t\t\t\tthis.resetUI();\n\t\t\t\tthis.deps.showError(`Voice input daemon crashed: ${message}. It will restart on next use.`);\n\t\t\t},\n\t\t\tonIdle: () => {\n\t\t\t\t// Daemon auto-shut down after idle timeout: drop the reference so the\n\t\t\t\t// next ctrl+r respawns (cold start). No user-facing message — this is\n\t\t\t\t// an expected memory-reclamation event, not an error.\n\t\t\t\tthis.daemon = undefined;\n\t\t\t},\n\t\t};\n\t}\n\n\t/** Inject decoded text into the editor via bracketed paste (with a trailing space). */\n\tprivate commitText(text: string): void {\n\t\tthis.deps.sendEditorInput(`\\x1b[200~${text} \\x1b[201~`);\n\t}\n\n\tprivate beginDaemonCapture(): void {\n\t\tif (!this.daemon?.isReady) return;\n\t\tthis.active = true;\n\t\tthis.showPanel().startListening();\n\t\tthis.daemon.startCapture();\n\t}\n\n\tprivate beginLegacyCapture(bin: string): void {\n\t\tthis.active = true;\n\t\tconst panel = this.showPanel();\n\t\tpanel.startListening();\n\t\tthis.session = startVoiceTranscribe(bin, {\n\t\t\tonStatus: (status) => {\n\t\t\t\tif (status === \"done\") {\n\t\t\t\t\tthis.active = false;\n\t\t\t\t\tthis.session = undefined;\n\t\t\t\t\tthis.resetUI();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\t// Old binaries emit no LEVEL/PARTIAL, so the panel shows a spinner\n\t\t\t\t// for the batch phases; committed words still stream into the editor.\n\t\t\t\tif (status === \"transcribing\") panel.setTranscribing();\n\t\t\t},\n\t\t\tonSegment: (text) => {\n\t\t\t\tthis.commitText(text);\n\t\t\t},\n\t\t\tonError: (message) => {\n\t\t\t\tthis.active = false;\n\t\t\t\tthis.session = undefined;\n\t\t\t\tthis.resetUI();\n\t\t\t\tthis.deps.showError(`Voice input failed: ${message}`);\n\t\t\t},\n\t\t});\n\t}\n\n\t/** Resolve the `voicetools` binary path. Prefers an explicit VOICETOOLS_BIN\n\t * override, otherwise resolves via the managed tools manager (bin dir / PATH /\n\t * download from the published release). Returns undefined when unavailable.\n\t */\n\tprivate async resolveBin(): Promise<string | undefined> {\n\t\tconst override = process.env.VOICETOOLS_BIN?.trim();\n\t\tif (override) return override;\n\t\treturn ensureTool(\"voicetools\", true);\n\t}\n\n\t/** Create (or reuse) the voice panel and mount it in the status container. */\n\tprivate showPanel(): VoicePanel {\n\t\tif (!this.panel) {\n\t\t\tthis.deps.statusContainer.clear();\n\t\t\tthis.panel = new VoicePanel(this.deps.ui, keyHint(\"app.input.voiceTranscribe\", \"cancel\"));\n\t\t\tthis.deps.statusContainer.addChild(this.panel);\n\t\t}\n\t\tthis.deps.ui.requestRender();\n\t\treturn this.panel;\n\t}\n\n\tprivate showWarming(message: string): void {\n\t\tthis.showPanel().setWarming(message);\n\t}\n\n\t/** Collapse the voice panel back to nothing (idle) and stop its animation. */\n\tprivate resetUI(): void {\n\t\tif (this.panel) {\n\t\t\tthis.panel.dispose();\n\t\t\tthis.panel = undefined;\n\t\t}\n\t\tthis.deps.statusContainer.clear();\n\t\tthis.deps.ui.requestRender();\n\t}\n}\n"]}
@@ -0,0 +1,312 @@
1
+ /**
2
+ * Voice-to-text capture control (ctrl+r / app.input.voiceTranscribe).
3
+ *
4
+ * Prefers a persistent `voicetools serve` daemon: the first press probes for
5
+ * support and (if found) loads models once, showing a "warming up" spinner;
6
+ * every later press reuses the already-warm daemon and jumps straight to
7
+ * Listening. Binaries without `serve` support fall back to spawning
8
+ * `voicetools transcribe` per press. Pressing the shortcut again while
9
+ * listening (or while still warming up) cancels.
10
+ */
11
+ import { ensureTool } from "../../../utils/tools-manager.js";
12
+ import { keyHint } from "../components/keybinding-hints.js";
13
+ import { VoicePanel } from "./voice-panel.js";
14
+ import { startVoiceTranscribe, VoiceDaemon } from "./voice-transcribe.js";
15
+ // Trailing-silence window: how long a pause while speaking lasts before the
16
+ // capture auto-stops. Passed to `voicetools serve` via `--silence-ms` (see
17
+ // VoiceDaemon.spawn) so the binary's real cutoff matches the on-screen
18
+ // countdown this same value drives. Kept generous so a thinking pause mid-
19
+ // sentence doesn't cut the user off (the binary's own default is 600ms).
20
+ const VOICE_SILENCE_MS = 3000;
21
+ /** How long to keep the warm voice model in memory after the last capture
22
+ * completes. The daemon auto-shuts down after this window, releasing the
23
+ * ~900 MB resident model; the next ctrl+r pays a cold-start respawn cost. */
24
+ const VOICE_IDLE_TIMEOUT_MS = 60_000;
25
+ const VOICE_UNAVAILABLE_MESSAGE = "Voice input failed: voicetools binary unavailable and could not be downloaded. " +
26
+ "Install it, set VOICETOOLS_BIN, or ensure a published release exists for this platform.";
27
+ export class VoiceController {
28
+ deps;
29
+ /** In-flight legacy (`voicetools transcribe`) capture. */
30
+ session;
31
+ // Persistent `voicetools serve` process, once probed successfully. Stays alive
32
+ // between captures so the model stays warm.
33
+ daemon;
34
+ /** Set when the binary rejected `serve`; future presses use the legacy path. */
35
+ daemonUnsupported = false;
36
+ /** True while the voicetools binary is being resolved/downloaded before a session starts. */
37
+ starting = false;
38
+ /** True while a capture is active (listening or transcribing). */
39
+ active = false;
40
+ /** The status panel while voice input is active. */
41
+ panel;
42
+ constructor(deps) {
43
+ this.deps = deps;
44
+ }
45
+ /** Toggle voice-to-text capture. */
46
+ toggle() {
47
+ if (this.active) {
48
+ if (this.daemon?.isReady) {
49
+ this.daemon.cancel();
50
+ }
51
+ else {
52
+ this.session?.stop();
53
+ }
54
+ this.session = undefined;
55
+ this.active = false;
56
+ this.resetUI();
57
+ return;
58
+ }
59
+ if (this.starting) {
60
+ // A second press while resolving/warming up: honour the cancel. Any
61
+ // daemon that finishes loading afterwards is kept warm for next time.
62
+ this.starting = false;
63
+ this.resetUI();
64
+ return;
65
+ }
66
+ if (this.daemonUnsupported) {
67
+ this.starting = true;
68
+ this.showWarming("Starting voice input...");
69
+ void this.resolveBin()
70
+ .then((bin) => {
71
+ if (!this.starting)
72
+ return;
73
+ this.starting = false;
74
+ if (!bin) {
75
+ this.resetUI();
76
+ this.deps.showError(VOICE_UNAVAILABLE_MESSAGE);
77
+ return;
78
+ }
79
+ this.beginLegacyCapture(bin);
80
+ })
81
+ .catch((err) => {
82
+ this.starting = false;
83
+ this.resetUI();
84
+ this.deps.showError(`Voice input failed: ${err instanceof Error ? err.message : String(err)}`);
85
+ });
86
+ return;
87
+ }
88
+ if (this.daemon?.isReady) {
89
+ this.beginDaemonCapture();
90
+ return;
91
+ }
92
+ // No daemon yet: resolve the binary, then probe for `serve` support by
93
+ // spawning it. `VoiceDaemon.spawn` doubles as the probe: it resolves to a
94
+ // live daemon once READY arrives, to "unsupported" if the process exits
95
+ // with no output at all (an old binary rejecting the unrecognized `serve`
96
+ // subcommand), or to "error" if it printed a real ERROR first (e.g. no
97
+ // model installed yet) — already surfaced via onError, so that case skips
98
+ // the legacy fallback (it would just hit the same error) but leaves
99
+ // daemon mode available to retry on the next press.
100
+ this.starting = true;
101
+ this.showWarming("Warming up voice input...");
102
+ void this.resolveBin()
103
+ .then(async (bin) => {
104
+ if (!bin) {
105
+ this.starting = false;
106
+ this.resetUI();
107
+ this.deps.showError(VOICE_UNAVAILABLE_MESSAGE);
108
+ return;
109
+ }
110
+ const result = await VoiceDaemon.spawn(bin, this.buildDaemonHandlers(), {
111
+ silenceMs: VOICE_SILENCE_MS,
112
+ idleTimeoutMs: VOICE_IDLE_TIMEOUT_MS,
113
+ });
114
+ if (!result.ok) {
115
+ if (result.reason === "unsupported") {
116
+ this.daemonUnsupported = true;
117
+ if (!this.starting) {
118
+ this.resetUI();
119
+ return;
120
+ }
121
+ this.starting = false;
122
+ this.beginLegacyCapture(bin);
123
+ return;
124
+ }
125
+ this.starting = false;
126
+ this.resetUI();
127
+ return;
128
+ }
129
+ this.daemon = result.daemon;
130
+ if (!this.starting) {
131
+ // Cancelled while warming up: keep the loaded daemon warm for next time.
132
+ this.resetUI();
133
+ return;
134
+ }
135
+ this.starting = false;
136
+ this.beginDaemonCapture();
137
+ })
138
+ .catch((err) => {
139
+ this.starting = false;
140
+ this.resetUI();
141
+ this.deps.showError(`Voice input failed: ${err instanceof Error ? err.message : String(err)}`);
142
+ });
143
+ }
144
+ /** Stop any capture, shut down the daemon, and drop the panel. */
145
+ dispose() {
146
+ if (this.session?.running) {
147
+ this.session.stop();
148
+ this.session = undefined;
149
+ }
150
+ this.daemon?.shutdown();
151
+ this.daemon = undefined;
152
+ if (this.panel) {
153
+ this.panel.dispose();
154
+ this.panel = undefined;
155
+ }
156
+ this.active = false;
157
+ this.starting = false;
158
+ }
159
+ /**
160
+ * Build the (stable, reused-across-captures) handlers for the daemon.
161
+ *
162
+ * Every handler bails out once `active` is false: CANCEL is a soft
163
+ * request (unlike the legacy path's `proc.kill()`, it doesn't sever the
164
+ * pipe), so the daemon can still have a trailing PARTIAL/FINAL/DONE for the
165
+ * just-cancelled capture in flight when the user presses cancel. Without
166
+ * this guard that stale text would land in the editor after the panel had
167
+ * already collapsed.
168
+ *
169
+ * v0.1.4 serve streams `PARTIAL <full growing hypothesis>` (live preview,
170
+ * never committed) and ends with a single `FINAL <complete text>` (the one
171
+ * commit to the editor). `SEGMENT` is still handled for the legacy
172
+ * transcribe path and any binary that streams committed chunks directly.
173
+ */
174
+ buildDaemonHandlers() {
175
+ return {
176
+ onSegment: (text) => {
177
+ if (!this.active)
178
+ return;
179
+ this.commitText(text);
180
+ },
181
+ onPartial: (text) => {
182
+ if (!this.active)
183
+ return;
184
+ this.panel?.setPartial(text);
185
+ },
186
+ onFinal: (text) => {
187
+ if (!this.active)
188
+ return;
189
+ this.commitText(text);
190
+ },
191
+ onStatus: (status) => {
192
+ if (!this.active)
193
+ return;
194
+ if (status === "done") {
195
+ this.active = false;
196
+ this.resetUI();
197
+ return;
198
+ }
199
+ if (status === "transcribing") {
200
+ this.panel?.setTranscribing();
201
+ }
202
+ else if (status === "listening") {
203
+ this.panel?.startListening();
204
+ }
205
+ },
206
+ onLevel: (rms) => {
207
+ if (!this.active)
208
+ return;
209
+ this.panel?.pushLevel(rms);
210
+ },
211
+ onPhase: (phase) => {
212
+ if (!this.active)
213
+ return;
214
+ if (phase === "silence") {
215
+ this.panel?.beginSilence(VOICE_SILENCE_MS);
216
+ }
217
+ else {
218
+ this.panel?.endSilence();
219
+ }
220
+ },
221
+ onError: (message) => {
222
+ this.active = false;
223
+ this.resetUI();
224
+ this.deps.showError(`Voice input failed: ${message}`);
225
+ },
226
+ onCrash: (message) => {
227
+ this.active = false;
228
+ this.daemon = undefined;
229
+ this.resetUI();
230
+ this.deps.showError(`Voice input daemon crashed: ${message}. It will restart on next use.`);
231
+ },
232
+ onIdle: () => {
233
+ // Daemon auto-shut down after idle timeout: drop the reference so the
234
+ // next ctrl+r respawns (cold start). No user-facing message — this is
235
+ // an expected memory-reclamation event, not an error.
236
+ this.daemon = undefined;
237
+ },
238
+ };
239
+ }
240
+ /** Inject decoded text into the editor via bracketed paste (with a trailing space). */
241
+ commitText(text) {
242
+ this.deps.sendEditorInput(`\x1b[200~${text} \x1b[201~`);
243
+ }
244
+ beginDaemonCapture() {
245
+ if (!this.daemon?.isReady)
246
+ return;
247
+ this.active = true;
248
+ this.showPanel().startListening();
249
+ this.daemon.startCapture();
250
+ }
251
+ beginLegacyCapture(bin) {
252
+ this.active = true;
253
+ const panel = this.showPanel();
254
+ panel.startListening();
255
+ this.session = startVoiceTranscribe(bin, {
256
+ onStatus: (status) => {
257
+ if (status === "done") {
258
+ this.active = false;
259
+ this.session = undefined;
260
+ this.resetUI();
261
+ return;
262
+ }
263
+ // Old binaries emit no LEVEL/PARTIAL, so the panel shows a spinner
264
+ // for the batch phases; committed words still stream into the editor.
265
+ if (status === "transcribing")
266
+ panel.setTranscribing();
267
+ },
268
+ onSegment: (text) => {
269
+ this.commitText(text);
270
+ },
271
+ onError: (message) => {
272
+ this.active = false;
273
+ this.session = undefined;
274
+ this.resetUI();
275
+ this.deps.showError(`Voice input failed: ${message}`);
276
+ },
277
+ });
278
+ }
279
+ /** Resolve the `voicetools` binary path. Prefers an explicit VOICETOOLS_BIN
280
+ * override, otherwise resolves via the managed tools manager (bin dir / PATH /
281
+ * download from the published release). Returns undefined when unavailable.
282
+ */
283
+ async resolveBin() {
284
+ const override = process.env.VOICETOOLS_BIN?.trim();
285
+ if (override)
286
+ return override;
287
+ return ensureTool("voicetools", true);
288
+ }
289
+ /** Create (or reuse) the voice panel and mount it in the status container. */
290
+ showPanel() {
291
+ if (!this.panel) {
292
+ this.deps.statusContainer.clear();
293
+ this.panel = new VoicePanel(this.deps.ui, keyHint("app.input.voiceTranscribe", "cancel"));
294
+ this.deps.statusContainer.addChild(this.panel);
295
+ }
296
+ this.deps.ui.requestRender();
297
+ return this.panel;
298
+ }
299
+ showWarming(message) {
300
+ this.showPanel().setWarming(message);
301
+ }
302
+ /** Collapse the voice panel back to nothing (idle) and stop its animation. */
303
+ resetUI() {
304
+ if (this.panel) {
305
+ this.panel.dispose();
306
+ this.panel = undefined;
307
+ }
308
+ this.deps.statusContainer.clear();
309
+ this.deps.ui.requestRender();
310
+ }
311
+ }
312
+ //# sourceMappingURL=voice-controller.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"voice-controller.js","sourceRoot":"","sources":["../../../../src/modes/interactive/voice/voice-controller.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Voice-to-text capture control (ctrl+r / app.input.voiceTranscribe).\n *\n * Prefers a persistent `voicetools serve` daemon: the first press probes for\n * support and (if found) loads models once, showing a \"warming up\" spinner;\n * every later press reuses the already-warm daemon and jumps straight to\n * Listening. Binaries without `serve` support fall back to spawning\n * `voicetools transcribe` per press. Pressing the shortcut again while\n * listening (or while still warming up) cancels.\n */\n\nimport type { Container, TUI } from \"@kolisachint/hoocode-tui\";\nimport { ensureTool } from \"../../../utils/tools-manager.js\";\nimport { keyHint } from \"../components/keybinding-hints.js\";\nimport { VoicePanel } from \"./voice-panel.js\";\nimport { startVoiceTranscribe, VoiceDaemon, type VoiceDaemonHandlers, type VoiceSession } from \"./voice-transcribe.js\";\n\n// Trailing-silence window: how long a pause while speaking lasts before the\n// capture auto-stops. Passed to `voicetools serve` via `--silence-ms` (see\n// VoiceDaemon.spawn) so the binary's real cutoff matches the on-screen\n// countdown this same value drives. Kept generous so a thinking pause mid-\n// sentence doesn't cut the user off (the binary's own default is 600ms).\nconst VOICE_SILENCE_MS = 3000;\n/** How long to keep the warm voice model in memory after the last capture\n * completes. The daemon auto-shuts down after this window, releasing the\n * ~900 MB resident model; the next ctrl+r pays a cold-start respawn cost. */\nconst VOICE_IDLE_TIMEOUT_MS = 60_000;\nconst VOICE_UNAVAILABLE_MESSAGE =\n\t\"Voice input failed: voicetools binary unavailable and could not be downloaded. \" +\n\t\"Install it, set VOICETOOLS_BIN, or ensure a published release exists for this platform.\";\n\n/** The slice of the interactive mode the voice feature needs. */\nexport interface VoiceControllerDeps {\n\tui: TUI;\n\t/** The status area below the chat; the voice panel mounts here. */\n\tstatusContainer: Container;\n\t/** Feed raw input (bracketed paste) to the prompt editor. */\n\tsendEditorInput(data: string): void;\n\tshowError(message: string): void;\n}\n\nexport class VoiceController {\n\t/** In-flight legacy (`voicetools transcribe`) capture. */\n\tprivate session: VoiceSession | undefined;\n\t// Persistent `voicetools serve` process, once probed successfully. Stays alive\n\t// between captures so the model stays warm.\n\tprivate daemon: VoiceDaemon | undefined;\n\t/** Set when the binary rejected `serve`; future presses use the legacy path. */\n\tprivate daemonUnsupported = false;\n\t/** True while the voicetools binary is being resolved/downloaded before a session starts. */\n\tprivate starting = false;\n\t/** True while a capture is active (listening or transcribing). */\n\tprivate active = false;\n\t/** The status panel while voice input is active. */\n\tprivate panel: VoicePanel | undefined;\n\n\tconstructor(private readonly deps: VoiceControllerDeps) {}\n\n\t/** Toggle voice-to-text capture. */\n\ttoggle(): void {\n\t\tif (this.active) {\n\t\t\tif (this.daemon?.isReady) {\n\t\t\t\tthis.daemon.cancel();\n\t\t\t} else {\n\t\t\t\tthis.session?.stop();\n\t\t\t}\n\t\t\tthis.session = undefined;\n\t\t\tthis.active = false;\n\t\t\tthis.resetUI();\n\t\t\treturn;\n\t\t}\n\n\t\tif (this.starting) {\n\t\t\t// A second press while resolving/warming up: honour the cancel. Any\n\t\t\t// daemon that finishes loading afterwards is kept warm for next time.\n\t\t\tthis.starting = false;\n\t\t\tthis.resetUI();\n\t\t\treturn;\n\t\t}\n\n\t\tif (this.daemonUnsupported) {\n\t\t\tthis.starting = true;\n\t\t\tthis.showWarming(\"Starting voice input...\");\n\t\t\tvoid this.resolveBin()\n\t\t\t\t.then((bin) => {\n\t\t\t\t\tif (!this.starting) return;\n\t\t\t\t\tthis.starting = false;\n\t\t\t\t\tif (!bin) {\n\t\t\t\t\t\tthis.resetUI();\n\t\t\t\t\t\tthis.deps.showError(VOICE_UNAVAILABLE_MESSAGE);\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tthis.beginLegacyCapture(bin);\n\t\t\t\t})\n\t\t\t\t.catch((err: unknown) => {\n\t\t\t\t\tthis.starting = false;\n\t\t\t\t\tthis.resetUI();\n\t\t\t\t\tthis.deps.showError(`Voice input failed: ${err instanceof Error ? err.message : String(err)}`);\n\t\t\t\t});\n\t\t\treturn;\n\t\t}\n\n\t\tif (this.daemon?.isReady) {\n\t\t\tthis.beginDaemonCapture();\n\t\t\treturn;\n\t\t}\n\n\t\t// No daemon yet: resolve the binary, then probe for `serve` support by\n\t\t// spawning it. `VoiceDaemon.spawn` doubles as the probe: it resolves to a\n\t\t// live daemon once READY arrives, to \"unsupported\" if the process exits\n\t\t// with no output at all (an old binary rejecting the unrecognized `serve`\n\t\t// subcommand), or to \"error\" if it printed a real ERROR first (e.g. no\n\t\t// model installed yet) — already surfaced via onError, so that case skips\n\t\t// the legacy fallback (it would just hit the same error) but leaves\n\t\t// daemon mode available to retry on the next press.\n\t\tthis.starting = true;\n\t\tthis.showWarming(\"Warming up voice input...\");\n\t\tvoid this.resolveBin()\n\t\t\t.then(async (bin) => {\n\t\t\t\tif (!bin) {\n\t\t\t\t\tthis.starting = false;\n\t\t\t\t\tthis.resetUI();\n\t\t\t\t\tthis.deps.showError(VOICE_UNAVAILABLE_MESSAGE);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst result = await VoiceDaemon.spawn(bin, this.buildDaemonHandlers(), {\n\t\t\t\t\tsilenceMs: VOICE_SILENCE_MS,\n\t\t\t\t\tidleTimeoutMs: VOICE_IDLE_TIMEOUT_MS,\n\t\t\t\t});\n\t\t\t\tif (!result.ok) {\n\t\t\t\t\tif (result.reason === \"unsupported\") {\n\t\t\t\t\t\tthis.daemonUnsupported = true;\n\t\t\t\t\t\tif (!this.starting) {\n\t\t\t\t\t\t\tthis.resetUI();\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tthis.starting = false;\n\t\t\t\t\t\tthis.beginLegacyCapture(bin);\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tthis.starting = false;\n\t\t\t\t\tthis.resetUI();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tthis.daemon = result.daemon;\n\t\t\t\tif (!this.starting) {\n\t\t\t\t\t// Cancelled while warming up: keep the loaded daemon warm for next time.\n\t\t\t\t\tthis.resetUI();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tthis.starting = false;\n\t\t\t\tthis.beginDaemonCapture();\n\t\t\t})\n\t\t\t.catch((err: unknown) => {\n\t\t\t\tthis.starting = false;\n\t\t\t\tthis.resetUI();\n\t\t\t\tthis.deps.showError(`Voice input failed: ${err instanceof Error ? err.message : String(err)}`);\n\t\t\t});\n\t}\n\n\t/** Stop any capture, shut down the daemon, and drop the panel. */\n\tdispose(): void {\n\t\tif (this.session?.running) {\n\t\t\tthis.session.stop();\n\t\t\tthis.session = undefined;\n\t\t}\n\t\tthis.daemon?.shutdown();\n\t\tthis.daemon = undefined;\n\t\tif (this.panel) {\n\t\t\tthis.panel.dispose();\n\t\t\tthis.panel = undefined;\n\t\t}\n\t\tthis.active = false;\n\t\tthis.starting = false;\n\t}\n\n\t/**\n\t * Build the (stable, reused-across-captures) handlers for the daemon.\n\t *\n\t * Every handler bails out once `active` is false: CANCEL is a soft\n\t * request (unlike the legacy path's `proc.kill()`, it doesn't sever the\n\t * pipe), so the daemon can still have a trailing PARTIAL/FINAL/DONE for the\n\t * just-cancelled capture in flight when the user presses cancel. Without\n\t * this guard that stale text would land in the editor after the panel had\n\t * already collapsed.\n\t *\n\t * v0.1.4 serve streams `PARTIAL <full growing hypothesis>` (live preview,\n\t * never committed) and ends with a single `FINAL <complete text>` (the one\n\t * commit to the editor). `SEGMENT` is still handled for the legacy\n\t * transcribe path and any binary that streams committed chunks directly.\n\t */\n\tprivate buildDaemonHandlers(): VoiceDaemonHandlers {\n\t\treturn {\n\t\t\tonSegment: (text) => {\n\t\t\t\tif (!this.active) return;\n\t\t\t\tthis.commitText(text);\n\t\t\t},\n\t\t\tonPartial: (text) => {\n\t\t\t\tif (!this.active) return;\n\t\t\t\tthis.panel?.setPartial(text);\n\t\t\t},\n\t\t\tonFinal: (text) => {\n\t\t\t\tif (!this.active) return;\n\t\t\t\tthis.commitText(text);\n\t\t\t},\n\t\t\tonStatus: (status) => {\n\t\t\t\tif (!this.active) return;\n\t\t\t\tif (status === \"done\") {\n\t\t\t\t\tthis.active = false;\n\t\t\t\t\tthis.resetUI();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (status === \"transcribing\") {\n\t\t\t\t\tthis.panel?.setTranscribing();\n\t\t\t\t} else if (status === \"listening\") {\n\t\t\t\t\tthis.panel?.startListening();\n\t\t\t\t}\n\t\t\t},\n\t\t\tonLevel: (rms) => {\n\t\t\t\tif (!this.active) return;\n\t\t\t\tthis.panel?.pushLevel(rms);\n\t\t\t},\n\t\t\tonPhase: (phase) => {\n\t\t\t\tif (!this.active) return;\n\t\t\t\tif (phase === \"silence\") {\n\t\t\t\t\tthis.panel?.beginSilence(VOICE_SILENCE_MS);\n\t\t\t\t} else {\n\t\t\t\t\tthis.panel?.endSilence();\n\t\t\t\t}\n\t\t\t},\n\t\t\tonError: (message) => {\n\t\t\t\tthis.active = false;\n\t\t\t\tthis.resetUI();\n\t\t\t\tthis.deps.showError(`Voice input failed: ${message}`);\n\t\t\t},\n\t\t\tonCrash: (message) => {\n\t\t\t\tthis.active = false;\n\t\t\t\tthis.daemon = undefined;\n\t\t\t\tthis.resetUI();\n\t\t\t\tthis.deps.showError(`Voice input daemon crashed: ${message}. It will restart on next use.`);\n\t\t\t},\n\t\t\tonIdle: () => {\n\t\t\t\t// Daemon auto-shut down after idle timeout: drop the reference so the\n\t\t\t\t// next ctrl+r respawns (cold start). No user-facing message — this is\n\t\t\t\t// an expected memory-reclamation event, not an error.\n\t\t\t\tthis.daemon = undefined;\n\t\t\t},\n\t\t};\n\t}\n\n\t/** Inject decoded text into the editor via bracketed paste (with a trailing space). */\n\tprivate commitText(text: string): void {\n\t\tthis.deps.sendEditorInput(`\\x1b[200~${text} \\x1b[201~`);\n\t}\n\n\tprivate beginDaemonCapture(): void {\n\t\tif (!this.daemon?.isReady) return;\n\t\tthis.active = true;\n\t\tthis.showPanel().startListening();\n\t\tthis.daemon.startCapture();\n\t}\n\n\tprivate beginLegacyCapture(bin: string): void {\n\t\tthis.active = true;\n\t\tconst panel = this.showPanel();\n\t\tpanel.startListening();\n\t\tthis.session = startVoiceTranscribe(bin, {\n\t\t\tonStatus: (status) => {\n\t\t\t\tif (status === \"done\") {\n\t\t\t\t\tthis.active = false;\n\t\t\t\t\tthis.session = undefined;\n\t\t\t\t\tthis.resetUI();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\t// Old binaries emit no LEVEL/PARTIAL, so the panel shows a spinner\n\t\t\t\t// for the batch phases; committed words still stream into the editor.\n\t\t\t\tif (status === \"transcribing\") panel.setTranscribing();\n\t\t\t},\n\t\t\tonSegment: (text) => {\n\t\t\t\tthis.commitText(text);\n\t\t\t},\n\t\t\tonError: (message) => {\n\t\t\t\tthis.active = false;\n\t\t\t\tthis.session = undefined;\n\t\t\t\tthis.resetUI();\n\t\t\t\tthis.deps.showError(`Voice input failed: ${message}`);\n\t\t\t},\n\t\t});\n\t}\n\n\t/** Resolve the `voicetools` binary path. Prefers an explicit VOICETOOLS_BIN\n\t * override, otherwise resolves via the managed tools manager (bin dir / PATH /\n\t * download from the published release). Returns undefined when unavailable.\n\t */\n\tprivate async resolveBin(): Promise<string | undefined> {\n\t\tconst override = process.env.VOICETOOLS_BIN?.trim();\n\t\tif (override) return override;\n\t\treturn ensureTool(\"voicetools\", true);\n\t}\n\n\t/** Create (or reuse) the voice panel and mount it in the status container. */\n\tprivate showPanel(): VoicePanel {\n\t\tif (!this.panel) {\n\t\t\tthis.deps.statusContainer.clear();\n\t\t\tthis.panel = new VoicePanel(this.deps.ui, keyHint(\"app.input.voiceTranscribe\", \"cancel\"));\n\t\t\tthis.deps.statusContainer.addChild(this.panel);\n\t\t}\n\t\tthis.deps.ui.requestRender();\n\t\treturn this.panel;\n\t}\n\n\tprivate showWarming(message: string): void {\n\t\tthis.showPanel().setWarming(message);\n\t}\n\n\t/** Collapse the voice panel back to nothing (idle) and stop its animation. */\n\tprivate resetUI(): void {\n\t\tif (this.panel) {\n\t\t\tthis.panel.dispose();\n\t\t\tthis.panel = undefined;\n\t\t}\n\t\tthis.deps.statusContainer.clear();\n\t\tthis.deps.ui.requestRender();\n\t}\n}\n"]}
@@ -0,0 +1 @@
1
+ {"version":3,"file":"voice-panel.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/voice/voice-panel.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,GAAG,EAAE,MAAM,0BAA0B,CAAC;AAI/D,MAAM,MAAM,eAAe,GAAG,SAAS,GAAG,WAAW,GAAG,SAAS,GAAG,cAAc,CAAC;AAgCnF,qBAAa,UAAW,YAAW,SAAS;IAW1C,OAAO,CAAC,QAAQ,CAAC,EAAE;IACnB,OAAO,CAAC,QAAQ,CAAC,UAAU;IAX5B,OAAO,CAAC,KAAK,CAA8B;IAC3C,OAAO,CAAC,KAAK,CAAK;IAClB,OAAO,CAAC,MAAM,CAAgB;IAC9B,OAAO,CAAC,OAAO,CAAM;IACrB,OAAO,CAAC,gBAAgB,CAAK;IAC7B,OAAO,CAAC,kBAAkB,CAAK;IAC/B,OAAO,CAAC,cAAc,CAA6B;IACnD,OAAO,CAAC,KAAK,CAA6C;IAE1D,YACkB,EAAE,EAAE,GAAG,GAAG,SAAS,EACnB,UAAU,EAAE,MAAM,EAChC;IAIJ,OAAO,CAAC,eAAe;IAWvB,kFAAkF;IAClF,OAAO,IAAI,IAAI,CAKd;IAED,oFAAoF;IACpF,UAAU,IAAI,IAAI,CAAG;IAIrB,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAKhC;IAED,cAAc,IAAI,IAAI,CAOrB;IAED,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAKlC;IAED,mEAAmE;IACnE,UAAU,IAAI,IAAI,CAKjB;IAED,eAAe,IAAI,IAAI,CAItB;IAID,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAO3B;IAED,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAI7B;IAID,OAAO,CAAC,cAAc;IAQtB,OAAO,CAAC,aAAa;IAOrB,OAAO,CAAC,YAAY;IAOpB,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAgC9B;CACD","sourcesContent":["/**\n * Multi-line voice-input status panel.\n *\n * Renders the live state of a `voicetools serve` capture across its phases so\n * the user always sees what's happening:\n *\n * ```text\n * 🎙 Listening · 0:03\n * ▁▂▃▅▇▆▄▂▁▂▄▆▇▅▃▂▁ ← scrolling level history from LEVEL events\n * › and so my fellow ameri▏ ← live partial transcript (PARTIAL), dim/tentative\n * ctrl+r cancel ← dim key footer\n * ```\n *\n * Phases:\n * - `warming` spinner while the model loads (first press only)\n * - `listening` mic glyph + elapsed timer + waveform + growing partial text\n * - `silence` trailing-silence countdown; waveform dims\n * - `transcribing` spinner while the final decode runs\n *\n * The component self-animates (spinner, timer, countdown) on an internal timer\n * and calls `ui.requestRender()`; the owner feeds it protocol events via\n * `pushLevel` / `setPartial` / `beginSilence` etc. and disposes it on collapse.\n */\n\nimport type { Component, TUI } from \"@kolisachint/hoocode-tui\";\nimport { visibleWidth } from \"@kolisachint/hoocode-tui\";\nimport { theme } from \"../theme/theme.js\";\n\nexport type VoicePanelPhase = \"warming\" | \"listening\" | \"silence\" | \"transcribing\";\n\nconst SPINNER_FRAMES = [\"⠋\", \"⠙\", \"⠹\", \"⠸\", \"⠼\", \"⠴\", \"⠦\", \"⠧\", \"⠇\", \"⠏\"];\n// 8 filled levels; index 0 renders as a baseline dot so an empty history still\n// reads as \"a track\", not a blank line.\nconst WAVE_LEVELS = [\"▁\", \"▁\", \"▂\", \"▃\", \"▄\", \"▅\", \"▆\", \"▇\", \"█\"];\nconst TICK_MS = 100;\nconst HISTORY_CAP = 64;\n// RMS from voicetools' vad::rms sits well below 1.0 for speech; this gain maps a\n// normal speaking level to roughly the top of the meter without clipping constantly.\nconst LEVEL_GAIN = 6;\n// During trailing silence the binary keeps emitting low LEVELs; only an RMS above\n// this counts as the speaker resuming, so we don't cancel the countdown on noise.\nconst RESUME_RMS = 0.02;\n\n/** Map an RMS energy to a waveform block glyph. */\nfunction levelGlyph(rms: number): string {\n\tconst norm = Math.max(0, Math.min(1, rms * LEVEL_GAIN));\n\tconst idx = Math.round(norm * (WAVE_LEVELS.length - 1));\n\treturn WAVE_LEVELS[idx] ?? WAVE_LEVELS[0]!;\n}\n\n/** Keep the tail (most recent chars) of `text` within `max` visible columns. */\nfunction clampTail(text: string, max: number): string {\n\tif (max <= 0) return \"\";\n\tif (visibleWidth(text) <= max) return text;\n\t// Partial transcripts are effectively plain text; slice by chars from the end\n\t// and prefix an ellipsis so the newest words stay visible as it grows.\n\tconst tail = text.slice(-(max - 1));\n\treturn `…${tail}`;\n}\n\nexport class VoicePanel implements Component {\n\tprivate phase: VoicePanelPhase = \"warming\";\n\tprivate frame = 0;\n\tprivate levels: number[] = [];\n\tprivate partial = \"\";\n\tprivate listeningStartMs = 0;\n\tprivate silenceRemainingMs = 0;\n\tprivate warmingMessage = \"Warming up voice input…\";\n\tprivate timer: ReturnType<typeof setInterval> | undefined;\n\n\tconstructor(\n\t\tprivate readonly ui: TUI | undefined,\n\t\tprivate readonly cancelHint: string,\n\t) {}\n\n\t// ---- lifecycle -------------------------------------------------------\n\n\tprivate ensureAnimating(): void {\n\t\tif (this.timer) return;\n\t\tthis.timer = setInterval(() => {\n\t\t\tthis.frame = (this.frame + 1) % SPINNER_FRAMES.length;\n\t\t\tif (this.phase === \"silence\" && this.silenceRemainingMs > 0) {\n\t\t\t\tthis.silenceRemainingMs = Math.max(0, this.silenceRemainingMs - TICK_MS);\n\t\t\t}\n\t\t\tthis.ui?.requestRender();\n\t\t}, TICK_MS);\n\t}\n\n\t/** Stop timers. The owner removes the panel from its container to collapse it. */\n\tdispose(): void {\n\t\tif (this.timer) {\n\t\t\tclearInterval(this.timer);\n\t\t\tthis.timer = undefined;\n\t\t}\n\t}\n\n\t/** No cached render state to clear; every `render()` recomputes from live state. */\n\tinvalidate(): void {}\n\n\t// ---- phase transitions ----------------------------------------------\n\n\tsetWarming(message: string): void {\n\t\tthis.phase = \"warming\";\n\t\tthis.warmingMessage = message;\n\t\tthis.ensureAnimating();\n\t\tthis.ui?.requestRender();\n\t}\n\n\tstartListening(): void {\n\t\tthis.phase = \"listening\";\n\t\tthis.levels = [];\n\t\tthis.partial = \"\";\n\t\tthis.listeningStartMs = Date.now();\n\t\tthis.ensureAnimating();\n\t\tthis.ui?.requestRender();\n\t}\n\n\tbeginSilence(totalMs: number): void {\n\t\tthis.phase = \"silence\";\n\t\tthis.silenceRemainingMs = totalMs;\n\t\tthis.ensureAnimating();\n\t\tthis.ui?.requestRender();\n\t}\n\n\t/** Speech resumed before cutoff: return to the listening phase. */\n\tendSilence(): void {\n\t\tif (this.phase === \"silence\") {\n\t\t\tthis.phase = \"listening\";\n\t\t\tthis.ui?.requestRender();\n\t\t}\n\t}\n\n\tsetTranscribing(): void {\n\t\tthis.phase = \"transcribing\";\n\t\tthis.ensureAnimating();\n\t\tthis.ui?.requestRender();\n\t}\n\n\t// ---- data feed -------------------------------------------------------\n\n\tpushLevel(rms: number): void {\n\t\tthis.levels.push(rms);\n\t\tif (this.levels.length > HISTORY_CAP) this.levels.shift();\n\t\t// Low levels keep flowing during the silence window; only a loud one means\n\t\t// the speaker resumed and the countdown should be abandoned.\n\t\tif (this.phase === \"silence\" && rms > RESUME_RMS) this.endSilence();\n\t\tthis.ui?.requestRender();\n\t}\n\n\tsetPartial(text: string): void {\n\t\tthis.partial = text;\n\t\tif (this.phase === \"silence\") this.endSilence();\n\t\tthis.ui?.requestRender();\n\t}\n\n\t// ---- rendering -------------------------------------------------------\n\n\tprivate renderWaveform(width: number): string {\n\t\tconst cells = Math.max(1, width - 2);\n\t\tconst recent = this.levels.slice(-cells);\n\t\tconst pad = cells - recent.length;\n\t\tconst glyphs = \" \".repeat(Math.max(0, pad)) + recent.map(levelGlyph).join(\"\");\n\t\treturn this.phase === \"silence\" ? theme.fg(\"dim\", glyphs) : theme.fg(\"accent\", glyphs);\n\t}\n\n\tprivate renderPartial(width: number): string | undefined {\n\t\tif (!this.partial) return undefined;\n\t\tconst body = clampTail(this.partial, Math.max(1, width - 4));\n\t\tconst cursor = this.phase === \"transcribing\" ? \"\" : theme.blink(theme.fg(\"accent\", \"▏\"));\n\t\treturn ` ${theme.fg(\"muted\", \"›\")} ${theme.fg(\"dim\", body)}${cursor}`;\n\t}\n\n\tprivate elapsedLabel(): string {\n\t\tconst secs = Math.floor((Date.now() - this.listeningStartMs) / 1000);\n\t\tconst m = Math.floor(secs / 60);\n\t\tconst s = secs % 60;\n\t\treturn `${m}:${s.toString().padStart(2, \"0\")}`;\n\t}\n\n\trender(width: number): string[] {\n\t\tconst spinner = SPINNER_FRAMES[this.frame] ?? SPINNER_FRAMES[0]!;\n\t\tconst footer = theme.fg(\"dim\", ` ${this.cancelHint}`);\n\t\tconst lines: string[] = [\"\"];\n\n\t\tif (this.phase === \"warming\") {\n\t\t\tlines.push(` ${theme.fg(\"accent\", spinner)} ${theme.fg(\"muted\", this.warmingMessage)}`);\n\t\t\tlines.push(footer);\n\t\t\treturn lines;\n\t\t}\n\n\t\tif (this.phase === \"transcribing\") {\n\t\t\tlines.push(` ${theme.fg(\"accent\", spinner)} ${theme.fg(\"accent\", \"Transcribing…\")}`);\n\t\t\tconst partial = this.renderPartial(width);\n\t\t\tif (partial) lines.push(partial);\n\t\t\tlines.push(footer);\n\t\t\treturn lines;\n\t\t}\n\n\t\t// listening / silence\n\t\tconst mic = theme.fg(\"error\", \"🎙\");\n\t\tlet header = `${mic} ${theme.fg(\"accent\", \"Listening\")} ${theme.fg(\"dim\", `· ${this.elapsedLabel()}`)}`;\n\t\tif (this.phase === \"silence\") {\n\t\t\tconst remaining = (this.silenceRemainingMs / 1000).toFixed(1);\n\t\t\theader += ` ${theme.fg(\"warning\", `· cutting off in ${remaining}s`)}`;\n\t\t}\n\t\tlines.push(` ${header}`);\n\t\tlines.push(` ${this.renderWaveform(width)}`);\n\t\tconst partial = this.renderPartial(width);\n\t\tif (partial) lines.push(partial);\n\t\tlines.push(footer);\n\t\treturn lines;\n\t}\n}\n"]}
@@ -0,0 +1 @@
1
+ {"version":3,"file":"voice-panel.js","sourceRoot":"","sources":["../../../../src/modes/interactive/voice/voice-panel.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAGH,OAAO,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AACxD,OAAO,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAI1C,MAAM,cAAc,GAAG,CAAC,KAAG,EAAE,KAAG,EAAE,KAAG,EAAE,KAAG,EAAE,KAAG,EAAE,KAAG,EAAE,KAAG,EAAE,KAAG,EAAE,KAAG,EAAE,KAAG,CAAC,CAAC;AAC1E,+EAA+E;AAC/E,wCAAwC;AACxC,MAAM,WAAW,GAAG,CAAC,KAAG,EAAE,KAAG,EAAE,KAAG,EAAE,KAAG,EAAE,KAAG,EAAE,KAAG,EAAE,KAAG,EAAE,KAAG,EAAE,KAAG,CAAC,CAAC;AAClE,MAAM,OAAO,GAAG,GAAG,CAAC;AACpB,MAAM,WAAW,GAAG,EAAE,CAAC;AACvB,iFAAiF;AACjF,qFAAqF;AACrF,MAAM,UAAU,GAAG,CAAC,CAAC;AACrB,kFAAkF;AAClF,kFAAkF;AAClF,MAAM,UAAU,GAAG,IAAI,CAAC;AAExB,mDAAmD;AACnD,SAAS,UAAU,CAAC,GAAW,EAAU;IACxC,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,GAAG,GAAG,UAAU,CAAC,CAAC,CAAC;IACxD,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,GAAG,CAAC,WAAW,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC;IACxD,OAAO,WAAW,CAAC,GAAG,CAAC,IAAI,WAAW,CAAC,CAAC,CAAE,CAAC;AAAA,CAC3C;AAED,gFAAgF;AAChF,SAAS,SAAS,CAAC,IAAY,EAAE,GAAW,EAAU;IACrD,IAAI,GAAG,IAAI,CAAC;QAAE,OAAO,EAAE,CAAC;IACxB,IAAI,YAAY,CAAC,IAAI,CAAC,IAAI,GAAG;QAAE,OAAO,IAAI,CAAC;IAC3C,8EAA8E;IAC9E,uEAAuE;IACvE,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC;IACpC,OAAO,MAAI,IAAI,EAAE,CAAC;AAAA,CAClB;AAED,MAAM,OAAO,UAAU;IAWJ,EAAE;IACF,UAAU;IAXpB,KAAK,GAAoB,SAAS,CAAC;IACnC,KAAK,GAAG,CAAC,CAAC;IACV,MAAM,GAAa,EAAE,CAAC;IACtB,OAAO,GAAG,EAAE,CAAC;IACb,gBAAgB,GAAG,CAAC,CAAC;IACrB,kBAAkB,GAAG,CAAC,CAAC;IACvB,cAAc,GAAG,2BAAyB,CAAC;IAC3C,KAAK,CAA6C;IAE1D,YACkB,EAAmB,EACnB,UAAkB,EAClC;kBAFgB,EAAE;0BACF,UAAU;IACzB,CAAC;IAEJ,yEAAyE;IAEjE,eAAe,GAAS;QAC/B,IAAI,IAAI,CAAC,KAAK;YAAE,OAAO;QACvB,IAAI,CAAC,KAAK,GAAG,WAAW,CAAC,GAAG,EAAE,CAAC;YAC9B,IAAI,CAAC,KAAK,GAAG,CAAC,IAAI,CAAC,KAAK,GAAG,CAAC,CAAC,GAAG,cAAc,CAAC,MAAM,CAAC;YACtD,IAAI,IAAI,CAAC,KAAK,KAAK,SAAS,IAAI,IAAI,CAAC,kBAAkB,GAAG,CAAC,EAAE,CAAC;gBAC7D,IAAI,CAAC,kBAAkB,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,kBAAkB,GAAG,OAAO,CAAC,CAAC;YAC1E,CAAC;YACD,IAAI,CAAC,EAAE,EAAE,aAAa,EAAE,CAAC;QAAA,CACzB,EAAE,OAAO,CAAC,CAAC;IAAA,CACZ;IAED,kFAAkF;IAClF,OAAO,GAAS;QACf,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;YAChB,aAAa,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YAC1B,IAAI,CAAC,KAAK,GAAG,SAAS,CAAC;QACxB,CAAC;IAAA,CACD;IAED,oFAAoF;IACpF,UAAU,GAAS,EAAC,CAAC;IAErB,wEAAwE;IAExE,UAAU,CAAC,OAAe,EAAQ;QACjC,IAAI,CAAC,KAAK,GAAG,SAAS,CAAC;QACvB,IAAI,CAAC,cAAc,GAAG,OAAO,CAAC;QAC9B,IAAI,CAAC,eAAe,EAAE,CAAC;QACvB,IAAI,CAAC,EAAE,EAAE,aAAa,EAAE,CAAC;IAAA,CACzB;IAED,cAAc,GAAS;QACtB,IAAI,CAAC,KAAK,GAAG,WAAW,CAAC;QACzB,IAAI,CAAC,MAAM,GAAG,EAAE,CAAC;QACjB,IAAI,CAAC,OAAO,GAAG,EAAE,CAAC;QAClB,IAAI,CAAC,gBAAgB,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACnC,IAAI,CAAC,eAAe,EAAE,CAAC;QACvB,IAAI,CAAC,EAAE,EAAE,aAAa,EAAE,CAAC;IAAA,CACzB;IAED,YAAY,CAAC,OAAe,EAAQ;QACnC,IAAI,CAAC,KAAK,GAAG,SAAS,CAAC;QACvB,IAAI,CAAC,kBAAkB,GAAG,OAAO,CAAC;QAClC,IAAI,CAAC,eAAe,EAAE,CAAC;QACvB,IAAI,CAAC,EAAE,EAAE,aAAa,EAAE,CAAC;IAAA,CACzB;IAED,mEAAmE;IACnE,UAAU,GAAS;QAClB,IAAI,IAAI,CAAC,KAAK,KAAK,SAAS,EAAE,CAAC;YAC9B,IAAI,CAAC,KAAK,GAAG,WAAW,CAAC;YACzB,IAAI,CAAC,EAAE,EAAE,aAAa,EAAE,CAAC;QAC1B,CAAC;IAAA,CACD;IAED,eAAe,GAAS;QACvB,IAAI,CAAC,KAAK,GAAG,cAAc,CAAC;QAC5B,IAAI,CAAC,eAAe,EAAE,CAAC;QACvB,IAAI,CAAC,EAAE,EAAE,aAAa,EAAE,CAAC;IAAA,CACzB;IAED,yEAAyE;IAEzE,SAAS,CAAC,GAAW,EAAQ;QAC5B,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACtB,IAAI,IAAI,CAAC,MAAM,CAAC,MAAM,GAAG,WAAW;YAAE,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC;QAC1D,2EAA2E;QAC3E,6DAA6D;QAC7D,IAAI,IAAI,CAAC,KAAK,KAAK,SAAS,IAAI,GAAG,GAAG,UAAU;YAAE,IAAI,CAAC,UAAU,EAAE,CAAC;QACpE,IAAI,CAAC,EAAE,EAAE,aAAa,EAAE,CAAC;IAAA,CACzB;IAED,UAAU,CAAC,IAAY,EAAQ;QAC9B,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC;QACpB,IAAI,IAAI,CAAC,KAAK,KAAK,SAAS;YAAE,IAAI,CAAC,UAAU,EAAE,CAAC;QAChD,IAAI,CAAC,EAAE,EAAE,aAAa,EAAE,CAAC;IAAA,CACzB;IAED,yEAAyE;IAEjE,cAAc,CAAC,KAAa,EAAU;QAC7C,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,GAAG,CAAC,CAAC,CAAC;QACrC,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC;QACzC,MAAM,GAAG,GAAG,KAAK,GAAG,MAAM,CAAC,MAAM,CAAC;QAClC,MAAM,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,GAAG,MAAM,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QAC9E,OAAO,IAAI,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;IAAA,CACvF;IAEO,aAAa,CAAC,KAAa,EAAsB;QACxD,IAAI,CAAC,IAAI,CAAC,OAAO;YAAE,OAAO,SAAS,CAAC;QACpC,MAAM,IAAI,GAAG,SAAS,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC;QAC7D,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,KAAK,cAAc,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE,CAAC,QAAQ,EAAE,KAAG,CAAC,CAAC,CAAC;QACzF,OAAO,IAAI,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,KAAG,CAAC,IAAI,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,IAAI,CAAC,GAAG,MAAM,EAAE,CAAC;IAAA,CACtE;IAEO,YAAY,GAAW;QAC9B,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,gBAAgB,CAAC,GAAG,IAAI,CAAC,CAAC;QACrE,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,GAAG,EAAE,CAAC,CAAC;QAChC,MAAM,CAAC,GAAG,IAAI,GAAG,EAAE,CAAC;QACpB,OAAO,GAAG,CAAC,IAAI,CAAC,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAAC;IAAA,CAC/C;IAED,MAAM,CAAC,KAAa,EAAY;QAC/B,MAAM,OAAO,GAAG,cAAc,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,cAAc,CAAC,CAAC,CAAE,CAAC;QACjE,MAAM,MAAM,GAAG,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,IAAI,IAAI,CAAC,UAAU,EAAE,CAAC,CAAC;QACtD,MAAM,KAAK,GAAa,CAAC,EAAE,CAAC,CAAC;QAE7B,IAAI,IAAI,CAAC,KAAK,KAAK,SAAS,EAAE,CAAC;YAC9B,KAAK,CAAC,IAAI,CAAC,IAAI,KAAK,CAAC,EAAE,CAAC,QAAQ,EAAE,OAAO,CAAC,IAAI,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,IAAI,CAAC,cAAc,CAAC,EAAE,CAAC,CAAC;YACxF,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;YACnB,OAAO,KAAK,CAAC;QACd,CAAC;QAED,IAAI,IAAI,CAAC,KAAK,KAAK,cAAc,EAAE,CAAC;YACnC,KAAK,CAAC,IAAI,CAAC,IAAI,KAAK,CAAC,EAAE,CAAC,QAAQ,EAAE,OAAO,CAAC,IAAI,KAAK,CAAC,EAAE,CAAC,QAAQ,EAAE,iBAAe,CAAC,EAAE,CAAC,CAAC;YACrF,MAAM,OAAO,GAAG,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;YAC1C,IAAI,OAAO;gBAAE,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;YACjC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;YACnB,OAAO,KAAK,CAAC;QACd,CAAC;QAED,sBAAsB;QACtB,MAAM,GAAG,GAAG,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,MAAG,CAAC,CAAC;QACnC,IAAI,MAAM,GAAG,GAAG,GAAG,KAAK,KAAK,CAAC,EAAE,CAAC,QAAQ,EAAE,WAAW,CAAC,IAAI,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,MAAK,IAAI,CAAC,YAAY,EAAE,EAAE,CAAC,EAAE,CAAC;QACzG,IAAI,IAAI,CAAC,KAAK,KAAK,SAAS,EAAE,CAAC;YAC9B,MAAM,SAAS,GAAG,CAAC,IAAI,CAAC,kBAAkB,GAAG,IAAI,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;YAC9D,MAAM,IAAI,IAAI,KAAK,CAAC,EAAE,CAAC,SAAS,EAAE,qBAAoB,SAAS,GAAG,CAAC,EAAE,CAAC;QACvE,CAAC;QACD,KAAK,CAAC,IAAI,CAAC,IAAI,MAAM,EAAE,CAAC,CAAC;QACzB,KAAK,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,cAAc,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;QAC7C,MAAM,OAAO,GAAG,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;QAC1C,IAAI,OAAO;YAAE,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QACjC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QACnB,OAAO,KAAK,CAAC;IAAA,CACb;CACD","sourcesContent":["/**\n * Multi-line voice-input status panel.\n *\n * Renders the live state of a `voicetools serve` capture across its phases so\n * the user always sees what's happening:\n *\n * ```text\n * 🎙 Listening · 0:03\n * ▁▂▃▅▇▆▄▂▁▂▄▆▇▅▃▂▁ ← scrolling level history from LEVEL events\n * › and so my fellow ameri▏ ← live partial transcript (PARTIAL), dim/tentative\n * ctrl+r cancel ← dim key footer\n * ```\n *\n * Phases:\n * - `warming` spinner while the model loads (first press only)\n * - `listening` mic glyph + elapsed timer + waveform + growing partial text\n * - `silence` trailing-silence countdown; waveform dims\n * - `transcribing` spinner while the final decode runs\n *\n * The component self-animates (spinner, timer, countdown) on an internal timer\n * and calls `ui.requestRender()`; the owner feeds it protocol events via\n * `pushLevel` / `setPartial` / `beginSilence` etc. and disposes it on collapse.\n */\n\nimport type { Component, TUI } from \"@kolisachint/hoocode-tui\";\nimport { visibleWidth } from \"@kolisachint/hoocode-tui\";\nimport { theme } from \"../theme/theme.js\";\n\nexport type VoicePanelPhase = \"warming\" | \"listening\" | \"silence\" | \"transcribing\";\n\nconst SPINNER_FRAMES = [\"⠋\", \"⠙\", \"⠹\", \"⠸\", \"⠼\", \"⠴\", \"⠦\", \"⠧\", \"⠇\", \"⠏\"];\n// 8 filled levels; index 0 renders as a baseline dot so an empty history still\n// reads as \"a track\", not a blank line.\nconst WAVE_LEVELS = [\"▁\", \"▁\", \"▂\", \"▃\", \"▄\", \"▅\", \"▆\", \"▇\", \"█\"];\nconst TICK_MS = 100;\nconst HISTORY_CAP = 64;\n// RMS from voicetools' vad::rms sits well below 1.0 for speech; this gain maps a\n// normal speaking level to roughly the top of the meter without clipping constantly.\nconst LEVEL_GAIN = 6;\n// During trailing silence the binary keeps emitting low LEVELs; only an RMS above\n// this counts as the speaker resuming, so we don't cancel the countdown on noise.\nconst RESUME_RMS = 0.02;\n\n/** Map an RMS energy to a waveform block glyph. */\nfunction levelGlyph(rms: number): string {\n\tconst norm = Math.max(0, Math.min(1, rms * LEVEL_GAIN));\n\tconst idx = Math.round(norm * (WAVE_LEVELS.length - 1));\n\treturn WAVE_LEVELS[idx] ?? WAVE_LEVELS[0]!;\n}\n\n/** Keep the tail (most recent chars) of `text` within `max` visible columns. */\nfunction clampTail(text: string, max: number): string {\n\tif (max <= 0) return \"\";\n\tif (visibleWidth(text) <= max) return text;\n\t// Partial transcripts are effectively plain text; slice by chars from the end\n\t// and prefix an ellipsis so the newest words stay visible as it grows.\n\tconst tail = text.slice(-(max - 1));\n\treturn `…${tail}`;\n}\n\nexport class VoicePanel implements Component {\n\tprivate phase: VoicePanelPhase = \"warming\";\n\tprivate frame = 0;\n\tprivate levels: number[] = [];\n\tprivate partial = \"\";\n\tprivate listeningStartMs = 0;\n\tprivate silenceRemainingMs = 0;\n\tprivate warmingMessage = \"Warming up voice input…\";\n\tprivate timer: ReturnType<typeof setInterval> | undefined;\n\n\tconstructor(\n\t\tprivate readonly ui: TUI | undefined,\n\t\tprivate readonly cancelHint: string,\n\t) {}\n\n\t// ---- lifecycle -------------------------------------------------------\n\n\tprivate ensureAnimating(): void {\n\t\tif (this.timer) return;\n\t\tthis.timer = setInterval(() => {\n\t\t\tthis.frame = (this.frame + 1) % SPINNER_FRAMES.length;\n\t\t\tif (this.phase === \"silence\" && this.silenceRemainingMs > 0) {\n\t\t\t\tthis.silenceRemainingMs = Math.max(0, this.silenceRemainingMs - TICK_MS);\n\t\t\t}\n\t\t\tthis.ui?.requestRender();\n\t\t}, TICK_MS);\n\t}\n\n\t/** Stop timers. The owner removes the panel from its container to collapse it. */\n\tdispose(): void {\n\t\tif (this.timer) {\n\t\t\tclearInterval(this.timer);\n\t\t\tthis.timer = undefined;\n\t\t}\n\t}\n\n\t/** No cached render state to clear; every `render()` recomputes from live state. */\n\tinvalidate(): void {}\n\n\t// ---- phase transitions ----------------------------------------------\n\n\tsetWarming(message: string): void {\n\t\tthis.phase = \"warming\";\n\t\tthis.warmingMessage = message;\n\t\tthis.ensureAnimating();\n\t\tthis.ui?.requestRender();\n\t}\n\n\tstartListening(): void {\n\t\tthis.phase = \"listening\";\n\t\tthis.levels = [];\n\t\tthis.partial = \"\";\n\t\tthis.listeningStartMs = Date.now();\n\t\tthis.ensureAnimating();\n\t\tthis.ui?.requestRender();\n\t}\n\n\tbeginSilence(totalMs: number): void {\n\t\tthis.phase = \"silence\";\n\t\tthis.silenceRemainingMs = totalMs;\n\t\tthis.ensureAnimating();\n\t\tthis.ui?.requestRender();\n\t}\n\n\t/** Speech resumed before cutoff: return to the listening phase. */\n\tendSilence(): void {\n\t\tif (this.phase === \"silence\") {\n\t\t\tthis.phase = \"listening\";\n\t\t\tthis.ui?.requestRender();\n\t\t}\n\t}\n\n\tsetTranscribing(): void {\n\t\tthis.phase = \"transcribing\";\n\t\tthis.ensureAnimating();\n\t\tthis.ui?.requestRender();\n\t}\n\n\t// ---- data feed -------------------------------------------------------\n\n\tpushLevel(rms: number): void {\n\t\tthis.levels.push(rms);\n\t\tif (this.levels.length > HISTORY_CAP) this.levels.shift();\n\t\t// Low levels keep flowing during the silence window; only a loud one means\n\t\t// the speaker resumed and the countdown should be abandoned.\n\t\tif (this.phase === \"silence\" && rms > RESUME_RMS) this.endSilence();\n\t\tthis.ui?.requestRender();\n\t}\n\n\tsetPartial(text: string): void {\n\t\tthis.partial = text;\n\t\tif (this.phase === \"silence\") this.endSilence();\n\t\tthis.ui?.requestRender();\n\t}\n\n\t// ---- rendering -------------------------------------------------------\n\n\tprivate renderWaveform(width: number): string {\n\t\tconst cells = Math.max(1, width - 2);\n\t\tconst recent = this.levels.slice(-cells);\n\t\tconst pad = cells - recent.length;\n\t\tconst glyphs = \" \".repeat(Math.max(0, pad)) + recent.map(levelGlyph).join(\"\");\n\t\treturn this.phase === \"silence\" ? theme.fg(\"dim\", glyphs) : theme.fg(\"accent\", glyphs);\n\t}\n\n\tprivate renderPartial(width: number): string | undefined {\n\t\tif (!this.partial) return undefined;\n\t\tconst body = clampTail(this.partial, Math.max(1, width - 4));\n\t\tconst cursor = this.phase === \"transcribing\" ? \"\" : theme.blink(theme.fg(\"accent\", \"▏\"));\n\t\treturn ` ${theme.fg(\"muted\", \"›\")} ${theme.fg(\"dim\", body)}${cursor}`;\n\t}\n\n\tprivate elapsedLabel(): string {\n\t\tconst secs = Math.floor((Date.now() - this.listeningStartMs) / 1000);\n\t\tconst m = Math.floor(secs / 60);\n\t\tconst s = secs % 60;\n\t\treturn `${m}:${s.toString().padStart(2, \"0\")}`;\n\t}\n\n\trender(width: number): string[] {\n\t\tconst spinner = SPINNER_FRAMES[this.frame] ?? SPINNER_FRAMES[0]!;\n\t\tconst footer = theme.fg(\"dim\", ` ${this.cancelHint}`);\n\t\tconst lines: string[] = [\"\"];\n\n\t\tif (this.phase === \"warming\") {\n\t\t\tlines.push(` ${theme.fg(\"accent\", spinner)} ${theme.fg(\"muted\", this.warmingMessage)}`);\n\t\t\tlines.push(footer);\n\t\t\treturn lines;\n\t\t}\n\n\t\tif (this.phase === \"transcribing\") {\n\t\t\tlines.push(` ${theme.fg(\"accent\", spinner)} ${theme.fg(\"accent\", \"Transcribing…\")}`);\n\t\t\tconst partial = this.renderPartial(width);\n\t\t\tif (partial) lines.push(partial);\n\t\t\tlines.push(footer);\n\t\t\treturn lines;\n\t\t}\n\n\t\t// listening / silence\n\t\tconst mic = theme.fg(\"error\", \"🎙\");\n\t\tlet header = `${mic} ${theme.fg(\"accent\", \"Listening\")} ${theme.fg(\"dim\", `· ${this.elapsedLabel()}`)}`;\n\t\tif (this.phase === \"silence\") {\n\t\t\tconst remaining = (this.silenceRemainingMs / 1000).toFixed(1);\n\t\t\theader += ` ${theme.fg(\"warning\", `· cutting off in ${remaining}s`)}`;\n\t\t}\n\t\tlines.push(` ${header}`);\n\t\tlines.push(` ${this.renderWaveform(width)}`);\n\t\tconst partial = this.renderPartial(width);\n\t\tif (partial) lines.push(partial);\n\t\tlines.push(footer);\n\t\treturn lines;\n\t}\n}\n"]}
@@ -0,0 +1 @@
1
+ {"version":3,"file":"voice-transcribe.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/voice/voice-transcribe.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;;;GAgBG;AAEH,MAAM,MAAM,WAAW,GAAG,WAAW,GAAG,cAAc,GAAG,MAAM,GAAG,WAAW,GAAG,MAAM,CAAC;AAEvF,MAAM,WAAW,uBAAuB;IACvC,uEAAuE;IACvE,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,4EAA4E;IAC5E,QAAQ,EAAE,CAAC,MAAM,EAAE,WAAW,KAAK,IAAI,CAAC;IACxC,2EAA2E;IAC3E,OAAO,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;CACnC;AAED;;;GAGG;AACH,MAAM,WAAW,YAAY;IAC5B,IAAI,IAAI,IAAI,CAAC;IACb,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;CAC1B;AAUD;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,uBAAuB,GAAG,YAAY,CAoEjG;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,mBAAoB,SAAQ,uBAAuB;IACnE,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IAChC,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC,OAAO,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACjC,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACpC,4EAA4E;IAC5E,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;CACpB;AAED;;;;;;;;;GASG;AACH,MAAM,MAAM,sBAAsB,GAAG;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,WAAW,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,aAAa,GAAG,OAAO,CAAA;CAAE,CAAC;AAExH,gGAAgG;AAChG,MAAM,WAAW,kBAAkB;IAClC,gGAAgG;IAChG,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;yEAEqE;IACrE,aAAa,CAAC,EAAE,MAAM,CAAC;CACvB;AAED;;;;;GAKG;AACH,qBAAa,WAAW;IAMtB,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,QAAQ;IAN1B,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,SAAS,CAA4C;IAC7D,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAS;IAEvC,OAAO,eAMN;IAED,IAAI,OAAO,IAAI,OAAO,CAErB;IAED,MAAM,CAAC,KAAK,CACX,GAAG,EAAE,MAAM,EACX,QAAQ,EAAE,mBAAmB,EAC7B,OAAO,CAAC,EAAE,kBAAkB,GAC1B,OAAO,CAAC,sBAAsB,CAAC,CA8EjC;IAED,OAAO,CAAC,cAAc;IAUtB,OAAO,CAAC,aAAa;IAOrB,OAAO,CAAC,UAAU;IAsBlB,0FAA0F;IAC1F,YAAY,IAAI,IAAI,CAInB;IAED,wDAAwD;IACxD,MAAM,IAAI,IAAI,CAGb;IAED,gFAAgF;IAChF,QAAQ,IAAI,IAAI,CAcf;CACD","sourcesContent":["import { createInterface, type Interface } from \"node:readline\";\nimport { type ChildProcess, spawn } from \"child_process\";\n\n/**\n * Drives the external `voicetools` binary and streams its stdout line protocol\n * into callbacks. One line per event on stdout (stderr is free for debug logs):\n *\n * ```text\n * STATUS recording # state transition (recording | transcribing | ...)\n * SEGMENT hello world # a chunk of decoded text\n * DONE # finished successfully\n * ERROR no model found # fatal error; process exits non-zero\n * ```\n *\n * `voicetools serve` (see `VoiceDaemon` below) reuses this same line protocol\n * plus a few daemon-only events (READY, LEVEL, PHASE).\n *\n * The caller wires `onSegment` to inject text into the editor (via bracketed\n * paste) and `onStatus` / `onError` to surface feedback.\n */\n\nexport type VoiceStatus = \"recording\" | \"transcribing\" | \"done\" | \"listening\" | string;\n\nexport interface VoiceTranscribeHandlers {\n\t/** A decoded chunk of text. Injected into the editor by the caller. */\n\tonSegment: (text: string) => void;\n\t/** A state transition reported by the binary, or `\"done\"` on completion. */\n\tonStatus: (status: VoiceStatus) => void;\n\t/** A fatal error: spawn failure, protocol ERROR line, or non-zero exit. */\n\tonError: (message: string) => void;\n}\n\n/**\n * A running voice-transcribe session. Call `stop()` to cancel early (e.g. the\n * user pressing the shortcut again). `stop()` is idempotent.\n */\nexport interface VoiceSession {\n\tstop(): void;\n\treadonly running: boolean;\n}\n\n/** Build a friendly message for a spawn failure, calling out a missing binary. */\nfunction describeSpawnError(err: unknown, bin: string): string {\n\tif (err && typeof err === \"object\" && (err as NodeJS.ErrnoException).code === \"ENOENT\") {\n\t\treturn `voicetools binary not found (tried \"${bin}\"). Install it or set VOICETOOLS_BIN to its path.`;\n\t}\n\treturn err instanceof Error ? err.message : String(err);\n}\n\n/**\n * Spawn `voicetools transcribe` for a single capture. This is the fallback\n * path for binaries that don't support `serve` (see `VoiceDaemon`): every\n * push-to-talk press pays the model load cold start.\n */\nexport function startVoiceTranscribe(bin: string, handlers: VoiceTranscribeHandlers): VoiceSession {\n\tlet proc: ChildProcess;\n\ttry {\n\t\tproc = spawn(bin, [\"transcribe\"], {\n\t\t\tstdio: [\"ignore\", \"pipe\", \"ignore\"],\n\t\t});\n\t} catch (err) {\n\t\thandlers.onError(describeSpawnError(err, bin));\n\t\treturn { stop: () => {}, running: false };\n\t}\n\n\tlet stopped = false;\n\tlet finished = false;\n\tlet rl: Interface | undefined;\n\n\tconst finish = (): void => {\n\t\tif (finished) return;\n\t\tfinished = true;\n\t\trl?.close();\n\t};\n\n\tif (!proc.stdout) {\n\t\tproc.kill();\n\t\thandlers.onError(\"failed to capture voicetools stdout\");\n\t\treturn { stop: () => {}, running: false };\n\t}\n\n\trl = createInterface({ input: proc.stdout });\n\trl.on(\"line\", (line) => {\n\t\tif (line.startsWith(\"STATUS \")) {\n\t\t\thandlers.onStatus(line.slice(7).trim());\n\t\t} else if (line.startsWith(\"SEGMENT \")) {\n\t\t\thandlers.onSegment(line.slice(8));\n\t\t} else if (line === \"DONE\") {\n\t\t\thandlers.onStatus(\"done\");\n\t\t\tfinish();\n\t\t} else if (line.startsWith(\"ERROR \")) {\n\t\t\thandlers.onError(line.slice(6).trim());\n\t\t\tfinish();\n\t\t}\n\t});\n\n\tproc.on(\"error\", (err) => {\n\t\tif (stopped || finished) return;\n\t\tfinish();\n\t\thandlers.onError(describeSpawnError(err, bin));\n\t});\n\n\tproc.on(\"close\", (code) => {\n\t\tconst wasFinished = finished;\n\t\tfinish();\n\t\tif (stopped || wasFinished) return;\n\t\tif (code && code !== 0) {\n\t\t\thandlers.onError(`voicetools exited with code ${code}`);\n\t\t}\n\t});\n\n\treturn {\n\t\tstop: () => {\n\t\t\tif (stopped) return;\n\t\t\tstopped = true;\n\t\t\tfinish();\n\t\t\tproc.kill();\n\t\t},\n\t\tget running() {\n\t\t\treturn !finished && !stopped;\n\t\t},\n\t};\n}\n\n/**\n * Handlers for a persistent `voicetools serve` daemon. Extends the base\n * transcribe handlers with the daemon-only events:\n * - `onReady` fires once after models finish loading.\n * - `onLevel` per-audio-chunk RMS, for a live meter/waveform.\n * - `onPhase` phase markers (e.g. `\"silence\"` when trailing silence begins).\n * - `onPartial` interim transcript while the user speaks — the FULL growing\n * hypothesis each time (supersedes the previous), never committed.\n * - `onFinal` the complete committed transcript for the utterance, emitted\n * once before DONE — this is the text to inject into the editor.\n * - `onCrash` the process died after having been ready (caller should drop\n * the reference and respawn lazily on the next push-to-talk).\n */\nexport interface VoiceDaemonHandlers extends VoiceTranscribeHandlers {\n\tonReady?: () => void;\n\tonLevel?: (rms: number) => void;\n\tonPhase?: (phase: string) => void;\n\tonPartial?: (text: string) => void;\n\tonFinal?: (text: string) => void;\n\tonCrash?: (message: string) => void;\n\t/** Fired when the daemon has been idle (no capture) for `idleTimeoutMs`. */\n\tonIdle?: () => void;\n}\n\n/**\n * Outcome of {@link VoiceDaemon.spawn}. `reason: \"unsupported\"` means the\n * process exited before READY with no ERROR line at all — the signature of\n * an old binary rejecting the unrecognized `serve` subcommand — and the\n * caller should silently fall back to `startVoiceTranscribe`. `reason:\n * \"error\"` means a genuine ERROR line (or OS-level spawn failure) was seen;\n * `handlers.onError` has already been called with it, and the caller should\n * surface that (not retry with the legacy path, which would just hit the\n * same failure) while leaving daemon mode available to retry next press.\n */\nexport type VoiceDaemonSpawnResult = { ok: true; daemon: VoiceDaemon } | { ok: false; reason: \"unsupported\" | \"error\" };\n\n/** Tunables passed to `voicetools serve` on spawn. Omitted fields use the binary's defaults. */\nexport interface VoiceDaemonOptions {\n\t/** Trailing-silence timeout, in ms, before the binary auto-stops a capture (`--silence-ms`). */\n\tsilenceMs?: number;\n\t/** Idle timeout, in ms. After a capture completes, the daemon auto-shuts down\n\t * if no new capture starts within this window, releasing the warm model from\n\t * memory. `0` disables idle shutdown (daemon stays warm forever). */\n\tidleTimeoutMs?: number;\n}\n\n/**\n * A persistent `voicetools serve` process: models are loaded once and stay\n * warm across captures. Only one capture runs at a time; call `startCapture`\n * to open the mic and `cancel` to stop early. `spawn` doubles as the support\n * probe for old binaries (see {@link VoiceDaemonSpawnResult}).\n */\nexport class VoiceDaemon {\n\tprivate closed = false;\n\tprivate idleTimer: ReturnType<typeof setTimeout> | undefined;\n\tprivate readonly idleTimeoutMs: number;\n\n\tprivate constructor(\n\t\tprivate readonly proc: ChildProcess,\n\t\tprivate readonly handlers: VoiceDaemonHandlers,\n\t\tidleTimeoutMs: number,\n\t) {\n\t\tthis.idleTimeoutMs = idleTimeoutMs;\n\t}\n\n\tget isReady(): boolean {\n\t\treturn !this.closed;\n\t}\n\n\tstatic spawn(\n\t\tbin: string,\n\t\thandlers: VoiceDaemonHandlers,\n\t\toptions?: VoiceDaemonOptions,\n\t): Promise<VoiceDaemonSpawnResult> {\n\t\treturn new Promise((resolve) => {\n\t\t\tconst args = [\"serve\"];\n\t\t\tif (options?.silenceMs !== undefined) {\n\t\t\t\targs.push(\"--silence-ms\", String(Math.max(0, Math.round(options.silenceMs))));\n\t\t\t}\n\t\t\tlet proc: ChildProcess;\n\t\t\ttry {\n\t\t\t\tproc = spawn(bin, args, { stdio: [\"pipe\", \"pipe\", \"ignore\"] });\n\t\t\t} catch (err) {\n\t\t\t\thandlers.onError(describeSpawnError(err, bin));\n\t\t\t\tresolve({ ok: false, reason: \"error\" });\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (!proc.stdout || !proc.stdin) {\n\t\t\t\tproc.kill();\n\t\t\t\thandlers.onError(\"failed to open voicetools serve stdio\");\n\t\t\t\tresolve({ ok: false, reason: \"error\" });\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tlet settled = false;\n\t\t\tlet daemon: VoiceDaemon | undefined;\n\t\t\tlet sawPreReadyError = false;\n\t\t\tconst rl = createInterface({ input: proc.stdout });\n\n\t\t\tconst fail = (reason: \"unsupported\" | \"error\"): void => {\n\t\t\t\tif (settled) return;\n\t\t\t\tsettled = true;\n\t\t\t\trl.close();\n\t\t\t\tresolve({ ok: false, reason });\n\t\t\t};\n\n\t\t\trl.on(\"line\", (line) => {\n\t\t\t\tif (daemon) {\n\t\t\t\t\tdaemon.handleLine(line);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (settled) return;\n\t\t\t\tif (line === \"READY\") {\n\t\t\t\t\tsettled = true;\n\t\t\t\t\tdaemon = new VoiceDaemon(proc, handlers, options?.idleTimeoutMs ?? 0);\n\t\t\t\t\thandlers.onReady?.();\n\t\t\t\t\tresolve({ ok: true, daemon });\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (line.startsWith(\"ERROR \")) {\n\t\t\t\t\t// Loading can fail before READY (e.g. no model installed yet).\n\t\t\t\t\t// Surface it now; the process still exits right after.\n\t\t\t\t\tsawPreReadyError = true;\n\t\t\t\t\thandlers.onError(line.slice(6).trim());\n\t\t\t\t}\n\t\t\t});\n\n\t\t\tproc.on(\"error\", (err) => {\n\t\t\t\tif (daemon) {\n\t\t\t\t\tif (!daemon.closed) {\n\t\t\t\t\t\tdaemon.closed = true;\n\t\t\t\t\t\thandlers.onCrash?.(describeSpawnError(err, bin));\n\t\t\t\t\t}\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (!sawPreReadyError) handlers.onError(describeSpawnError(err, bin));\n\t\t\t\tfail(\"error\");\n\t\t\t});\n\n\t\t\tproc.on(\"close\", (code) => {\n\t\t\t\tif (daemon) {\n\t\t\t\t\tif (!daemon.closed) {\n\t\t\t\t\t\tdaemon.closed = true;\n\t\t\t\t\t\thandlers.onCrash?.(`voicetools serve exited with code ${code ?? \"unknown\"}`);\n\t\t\t\t\t}\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tfail(sawPreReadyError ? \"error\" : \"unsupported\");\n\t\t\t});\n\t\t});\n\t}\n\n\tprivate startIdleTimer(): void {\n\t\tthis.stopIdleTimer();\n\t\tif (this.idleTimeoutMs <= 0 || this.closed) return;\n\t\tthis.idleTimer = setTimeout(() => {\n\t\t\tthis.idleTimer = undefined;\n\t\t\tif (this.closed) return;\n\t\t\tthis.handlers.onIdle?.();\n\t\t}, this.idleTimeoutMs);\n\t}\n\n\tprivate stopIdleTimer(): void {\n\t\tif (this.idleTimer) {\n\t\t\tclearTimeout(this.idleTimer);\n\t\t\tthis.idleTimer = undefined;\n\t\t}\n\t}\n\n\tprivate handleLine(line: string): void {\n\t\tif (line.startsWith(\"STATUS \")) {\n\t\t\tthis.handlers.onStatus(line.slice(7).trim());\n\t\t} else if (line.startsWith(\"PARTIAL \")) {\n\t\t\tthis.handlers.onPartial?.(line.slice(8));\n\t\t} else if (line.startsWith(\"FINAL \")) {\n\t\t\tthis.handlers.onFinal?.(line.slice(6));\n\t\t} else if (line.startsWith(\"SEGMENT \")) {\n\t\t\tthis.handlers.onSegment(line.slice(8));\n\t\t} else if (line.startsWith(\"LEVEL \")) {\n\t\t\tconst rms = Number.parseFloat(line.slice(6).trim());\n\t\t\tif (!Number.isNaN(rms)) this.handlers.onLevel?.(rms);\n\t\t} else if (line.startsWith(\"PHASE \")) {\n\t\t\tthis.handlers.onPhase?.(line.slice(6).trim());\n\t\t} else if (line === \"DONE\") {\n\t\t\tthis.handlers.onStatus(\"done\");\n\t\t\tthis.startIdleTimer();\n\t\t} else if (line.startsWith(\"ERROR \")) {\n\t\t\tthis.handlers.onError(line.slice(6).trim());\n\t\t}\n\t}\n\n\t/** Begin a capture: opens the mic, streams PARTIAL/FINAL (or SEGMENT), ends with DONE. */\n\tstartCapture(): void {\n\t\tif (this.closed) return;\n\t\tthis.stopIdleTimer();\n\t\tthis.proc.stdin?.write(\"START\\n\");\n\t}\n\n\t/** Cancel the in-flight capture, if any. Idempotent. */\n\tcancel(): void {\n\t\tif (this.closed) return;\n\t\tthis.proc.stdin?.write(\"CANCEL\\n\");\n\t}\n\n\t/** Ask the daemon to exit gracefully, force-killing if it doesn't within 1s. */\n\tshutdown(): void {\n\t\tif (this.closed) return;\n\t\tthis.closed = true;\n\t\tthis.stopIdleTimer();\n\t\ttry {\n\t\t\tthis.proc.stdin?.write(\"SHUTDOWN\\n\");\n\t\t} catch {\n\t\t\t// stdin may already be gone (process died); force-kill below covers it.\n\t\t}\n\t\tconst proc = this.proc;\n\t\tconst killTimer = setTimeout(() => {\n\t\t\tif (!proc.killed) proc.kill();\n\t\t}, 1000);\n\t\tproc.once(\"close\", () => clearTimeout(killTimer));\n\t}\n}\n"]}
@@ -0,0 +1 @@
1
+ {"version":3,"file":"voice-transcribe.js","sourceRoot":"","sources":["../../../../src/modes/interactive/voice/voice-transcribe.ts"],"names":[],"mappings":"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","sourcesContent":["import { createInterface, type Interface } from \"node:readline\";\nimport { type ChildProcess, spawn } from \"child_process\";\n\n/**\n * Drives the external `voicetools` binary and streams its stdout line protocol\n * into callbacks. One line per event on stdout (stderr is free for debug logs):\n *\n * ```text\n * STATUS recording # state transition (recording | transcribing | ...)\n * SEGMENT hello world # a chunk of decoded text\n * DONE # finished successfully\n * ERROR no model found # fatal error; process exits non-zero\n * ```\n *\n * `voicetools serve` (see `VoiceDaemon` below) reuses this same line protocol\n * plus a few daemon-only events (READY, LEVEL, PHASE).\n *\n * The caller wires `onSegment` to inject text into the editor (via bracketed\n * paste) and `onStatus` / `onError` to surface feedback.\n */\n\nexport type VoiceStatus = \"recording\" | \"transcribing\" | \"done\" | \"listening\" | string;\n\nexport interface VoiceTranscribeHandlers {\n\t/** A decoded chunk of text. Injected into the editor by the caller. */\n\tonSegment: (text: string) => void;\n\t/** A state transition reported by the binary, or `\"done\"` on completion. */\n\tonStatus: (status: VoiceStatus) => void;\n\t/** A fatal error: spawn failure, protocol ERROR line, or non-zero exit. */\n\tonError: (message: string) => void;\n}\n\n/**\n * A running voice-transcribe session. Call `stop()` to cancel early (e.g. the\n * user pressing the shortcut again). `stop()` is idempotent.\n */\nexport interface VoiceSession {\n\tstop(): void;\n\treadonly running: boolean;\n}\n\n/** Build a friendly message for a spawn failure, calling out a missing binary. */\nfunction describeSpawnError(err: unknown, bin: string): string {\n\tif (err && typeof err === \"object\" && (err as NodeJS.ErrnoException).code === \"ENOENT\") {\n\t\treturn `voicetools binary not found (tried \"${bin}\"). Install it or set VOICETOOLS_BIN to its path.`;\n\t}\n\treturn err instanceof Error ? err.message : String(err);\n}\n\n/**\n * Spawn `voicetools transcribe` for a single capture. This is the fallback\n * path for binaries that don't support `serve` (see `VoiceDaemon`): every\n * push-to-talk press pays the model load cold start.\n */\nexport function startVoiceTranscribe(bin: string, handlers: VoiceTranscribeHandlers): VoiceSession {\n\tlet proc: ChildProcess;\n\ttry {\n\t\tproc = spawn(bin, [\"transcribe\"], {\n\t\t\tstdio: [\"ignore\", \"pipe\", \"ignore\"],\n\t\t});\n\t} catch (err) {\n\t\thandlers.onError(describeSpawnError(err, bin));\n\t\treturn { stop: () => {}, running: false };\n\t}\n\n\tlet stopped = false;\n\tlet finished = false;\n\tlet rl: Interface | undefined;\n\n\tconst finish = (): void => {\n\t\tif (finished) return;\n\t\tfinished = true;\n\t\trl?.close();\n\t};\n\n\tif (!proc.stdout) {\n\t\tproc.kill();\n\t\thandlers.onError(\"failed to capture voicetools stdout\");\n\t\treturn { stop: () => {}, running: false };\n\t}\n\n\trl = createInterface({ input: proc.stdout });\n\trl.on(\"line\", (line) => {\n\t\tif (line.startsWith(\"STATUS \")) {\n\t\t\thandlers.onStatus(line.slice(7).trim());\n\t\t} else if (line.startsWith(\"SEGMENT \")) {\n\t\t\thandlers.onSegment(line.slice(8));\n\t\t} else if (line === \"DONE\") {\n\t\t\thandlers.onStatus(\"done\");\n\t\t\tfinish();\n\t\t} else if (line.startsWith(\"ERROR \")) {\n\t\t\thandlers.onError(line.slice(6).trim());\n\t\t\tfinish();\n\t\t}\n\t});\n\n\tproc.on(\"error\", (err) => {\n\t\tif (stopped || finished) return;\n\t\tfinish();\n\t\thandlers.onError(describeSpawnError(err, bin));\n\t});\n\n\tproc.on(\"close\", (code) => {\n\t\tconst wasFinished = finished;\n\t\tfinish();\n\t\tif (stopped || wasFinished) return;\n\t\tif (code && code !== 0) {\n\t\t\thandlers.onError(`voicetools exited with code ${code}`);\n\t\t}\n\t});\n\n\treturn {\n\t\tstop: () => {\n\t\t\tif (stopped) return;\n\t\t\tstopped = true;\n\t\t\tfinish();\n\t\t\tproc.kill();\n\t\t},\n\t\tget running() {\n\t\t\treturn !finished && !stopped;\n\t\t},\n\t};\n}\n\n/**\n * Handlers for a persistent `voicetools serve` daemon. Extends the base\n * transcribe handlers with the daemon-only events:\n * - `onReady` fires once after models finish loading.\n * - `onLevel` per-audio-chunk RMS, for a live meter/waveform.\n * - `onPhase` phase markers (e.g. `\"silence\"` when trailing silence begins).\n * - `onPartial` interim transcript while the user speaks — the FULL growing\n * hypothesis each time (supersedes the previous), never committed.\n * - `onFinal` the complete committed transcript for the utterance, emitted\n * once before DONE — this is the text to inject into the editor.\n * - `onCrash` the process died after having been ready (caller should drop\n * the reference and respawn lazily on the next push-to-talk).\n */\nexport interface VoiceDaemonHandlers extends VoiceTranscribeHandlers {\n\tonReady?: () => void;\n\tonLevel?: (rms: number) => void;\n\tonPhase?: (phase: string) => void;\n\tonPartial?: (text: string) => void;\n\tonFinal?: (text: string) => void;\n\tonCrash?: (message: string) => void;\n\t/** Fired when the daemon has been idle (no capture) for `idleTimeoutMs`. */\n\tonIdle?: () => void;\n}\n\n/**\n * Outcome of {@link VoiceDaemon.spawn}. `reason: \"unsupported\"` means the\n * process exited before READY with no ERROR line at all — the signature of\n * an old binary rejecting the unrecognized `serve` subcommand — and the\n * caller should silently fall back to `startVoiceTranscribe`. `reason:\n * \"error\"` means a genuine ERROR line (or OS-level spawn failure) was seen;\n * `handlers.onError` has already been called with it, and the caller should\n * surface that (not retry with the legacy path, which would just hit the\n * same failure) while leaving daemon mode available to retry next press.\n */\nexport type VoiceDaemonSpawnResult = { ok: true; daemon: VoiceDaemon } | { ok: false; reason: \"unsupported\" | \"error\" };\n\n/** Tunables passed to `voicetools serve` on spawn. Omitted fields use the binary's defaults. */\nexport interface VoiceDaemonOptions {\n\t/** Trailing-silence timeout, in ms, before the binary auto-stops a capture (`--silence-ms`). */\n\tsilenceMs?: number;\n\t/** Idle timeout, in ms. After a capture completes, the daemon auto-shuts down\n\t * if no new capture starts within this window, releasing the warm model from\n\t * memory. `0` disables idle shutdown (daemon stays warm forever). */\n\tidleTimeoutMs?: number;\n}\n\n/**\n * A persistent `voicetools serve` process: models are loaded once and stay\n * warm across captures. Only one capture runs at a time; call `startCapture`\n * to open the mic and `cancel` to stop early. `spawn` doubles as the support\n * probe for old binaries (see {@link VoiceDaemonSpawnResult}).\n */\nexport class VoiceDaemon {\n\tprivate closed = false;\n\tprivate idleTimer: ReturnType<typeof setTimeout> | undefined;\n\tprivate readonly idleTimeoutMs: number;\n\n\tprivate constructor(\n\t\tprivate readonly proc: ChildProcess,\n\t\tprivate readonly handlers: VoiceDaemonHandlers,\n\t\tidleTimeoutMs: number,\n\t) {\n\t\tthis.idleTimeoutMs = idleTimeoutMs;\n\t}\n\n\tget isReady(): boolean {\n\t\treturn !this.closed;\n\t}\n\n\tstatic spawn(\n\t\tbin: string,\n\t\thandlers: VoiceDaemonHandlers,\n\t\toptions?: VoiceDaemonOptions,\n\t): Promise<VoiceDaemonSpawnResult> {\n\t\treturn new Promise((resolve) => {\n\t\t\tconst args = [\"serve\"];\n\t\t\tif (options?.silenceMs !== undefined) {\n\t\t\t\targs.push(\"--silence-ms\", String(Math.max(0, Math.round(options.silenceMs))));\n\t\t\t}\n\t\t\tlet proc: ChildProcess;\n\t\t\ttry {\n\t\t\t\tproc = spawn(bin, args, { stdio: [\"pipe\", \"pipe\", \"ignore\"] });\n\t\t\t} catch (err) {\n\t\t\t\thandlers.onError(describeSpawnError(err, bin));\n\t\t\t\tresolve({ ok: false, reason: \"error\" });\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (!proc.stdout || !proc.stdin) {\n\t\t\t\tproc.kill();\n\t\t\t\thandlers.onError(\"failed to open voicetools serve stdio\");\n\t\t\t\tresolve({ ok: false, reason: \"error\" });\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tlet settled = false;\n\t\t\tlet daemon: VoiceDaemon | undefined;\n\t\t\tlet sawPreReadyError = false;\n\t\t\tconst rl = createInterface({ input: proc.stdout });\n\n\t\t\tconst fail = (reason: \"unsupported\" | \"error\"): void => {\n\t\t\t\tif (settled) return;\n\t\t\t\tsettled = true;\n\t\t\t\trl.close();\n\t\t\t\tresolve({ ok: false, reason });\n\t\t\t};\n\n\t\t\trl.on(\"line\", (line) => {\n\t\t\t\tif (daemon) {\n\t\t\t\t\tdaemon.handleLine(line);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (settled) return;\n\t\t\t\tif (line === \"READY\") {\n\t\t\t\t\tsettled = true;\n\t\t\t\t\tdaemon = new VoiceDaemon(proc, handlers, options?.idleTimeoutMs ?? 0);\n\t\t\t\t\thandlers.onReady?.();\n\t\t\t\t\tresolve({ ok: true, daemon });\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (line.startsWith(\"ERROR \")) {\n\t\t\t\t\t// Loading can fail before READY (e.g. no model installed yet).\n\t\t\t\t\t// Surface it now; the process still exits right after.\n\t\t\t\t\tsawPreReadyError = true;\n\t\t\t\t\thandlers.onError(line.slice(6).trim());\n\t\t\t\t}\n\t\t\t});\n\n\t\t\tproc.on(\"error\", (err) => {\n\t\t\t\tif (daemon) {\n\t\t\t\t\tif (!daemon.closed) {\n\t\t\t\t\t\tdaemon.closed = true;\n\t\t\t\t\t\thandlers.onCrash?.(describeSpawnError(err, bin));\n\t\t\t\t\t}\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (!sawPreReadyError) handlers.onError(describeSpawnError(err, bin));\n\t\t\t\tfail(\"error\");\n\t\t\t});\n\n\t\t\tproc.on(\"close\", (code) => {\n\t\t\t\tif (daemon) {\n\t\t\t\t\tif (!daemon.closed) {\n\t\t\t\t\t\tdaemon.closed = true;\n\t\t\t\t\t\thandlers.onCrash?.(`voicetools serve exited with code ${code ?? \"unknown\"}`);\n\t\t\t\t\t}\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tfail(sawPreReadyError ? \"error\" : \"unsupported\");\n\t\t\t});\n\t\t});\n\t}\n\n\tprivate startIdleTimer(): void {\n\t\tthis.stopIdleTimer();\n\t\tif (this.idleTimeoutMs <= 0 || this.closed) return;\n\t\tthis.idleTimer = setTimeout(() => {\n\t\t\tthis.idleTimer = undefined;\n\t\t\tif (this.closed) return;\n\t\t\tthis.handlers.onIdle?.();\n\t\t}, this.idleTimeoutMs);\n\t}\n\n\tprivate stopIdleTimer(): void {\n\t\tif (this.idleTimer) {\n\t\t\tclearTimeout(this.idleTimer);\n\t\t\tthis.idleTimer = undefined;\n\t\t}\n\t}\n\n\tprivate handleLine(line: string): void {\n\t\tif (line.startsWith(\"STATUS \")) {\n\t\t\tthis.handlers.onStatus(line.slice(7).trim());\n\t\t} else if (line.startsWith(\"PARTIAL \")) {\n\t\t\tthis.handlers.onPartial?.(line.slice(8));\n\t\t} else if (line.startsWith(\"FINAL \")) {\n\t\t\tthis.handlers.onFinal?.(line.slice(6));\n\t\t} else if (line.startsWith(\"SEGMENT \")) {\n\t\t\tthis.handlers.onSegment(line.slice(8));\n\t\t} else if (line.startsWith(\"LEVEL \")) {\n\t\t\tconst rms = Number.parseFloat(line.slice(6).trim());\n\t\t\tif (!Number.isNaN(rms)) this.handlers.onLevel?.(rms);\n\t\t} else if (line.startsWith(\"PHASE \")) {\n\t\t\tthis.handlers.onPhase?.(line.slice(6).trim());\n\t\t} else if (line === \"DONE\") {\n\t\t\tthis.handlers.onStatus(\"done\");\n\t\t\tthis.startIdleTimer();\n\t\t} else if (line.startsWith(\"ERROR \")) {\n\t\t\tthis.handlers.onError(line.slice(6).trim());\n\t\t}\n\t}\n\n\t/** Begin a capture: opens the mic, streams PARTIAL/FINAL (or SEGMENT), ends with DONE. */\n\tstartCapture(): void {\n\t\tif (this.closed) return;\n\t\tthis.stopIdleTimer();\n\t\tthis.proc.stdin?.write(\"START\\n\");\n\t}\n\n\t/** Cancel the in-flight capture, if any. Idempotent. */\n\tcancel(): void {\n\t\tif (this.closed) return;\n\t\tthis.proc.stdin?.write(\"CANCEL\\n\");\n\t}\n\n\t/** Ask the daemon to exit gracefully, force-killing if it doesn't within 1s. */\n\tshutdown(): void {\n\t\tif (this.closed) return;\n\t\tthis.closed = true;\n\t\tthis.stopIdleTimer();\n\t\ttry {\n\t\t\tthis.proc.stdin?.write(\"SHUTDOWN\\n\");\n\t\t} catch {\n\t\t\t// stdin may already be gone (process died); force-kill below covers it.\n\t\t}\n\t\tconst proc = this.proc;\n\t\tconst killTimer = setTimeout(() => {\n\t\t\tif (!proc.killed) proc.kill();\n\t\t}, 1000);\n\t\tproc.once(\"close\", () => clearTimeout(killTimer));\n\t}\n}\n"]}
@@ -3,11 +3,10 @@
3
3
  *
4
4
  * Spawns the agent in RPC mode and provides a typed API for all operations.
5
5
  */
6
- import type { AgentEvent, AgentMessage, ThinkingLevel } from "@kolisachint/hoocode-agent-core";
6
+ import type { AgentEvent, AgentMessage, CompactionResult, ThinkingLevel } from "@kolisachint/hoocode-agent-core";
7
7
  import type { ImageContent } from "@kolisachint/hoocode-ai";
8
8
  import type { SessionStats } from "../../core/agent-session.js";
9
9
  import type { BashResult } from "../../core/bash-executor.js";
10
- import type { CompactionResult } from "../../core/compaction/index.js";
11
10
  import type { RpcSessionState, RpcSlashCommand } from "./rpc-types.js";
12
11
  export interface RpcClientOptions {
13
12
  /** Path to the CLI entry point (default: searches for dist/cli.js) */