gentle-pi 4.2.0 → 4.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (334) hide show
  1. package/README.md +340 -138
  2. package/assets/agents/gentle-ai-explore.md +1 -0
  3. package/assets/agents/gentle-ai-verify.md +6 -4
  4. package/assets/agents/gentle-ai-worker.md +7 -1
  5. package/assets/design-canvas/canvas.css +648 -0
  6. package/assets/design-canvas/canvas.js +1497 -0
  7. package/assets/design-canvas/index.html +88 -0
  8. package/assets/design-canvas/inspector.js +290 -0
  9. package/assets/orchestrator-delegation-reference.md +163 -0
  10. package/assets/orchestrator-delegation.md +25 -176
  11. package/assets/orchestrator-tracking.md +1 -1
  12. package/assets/orchestrator-verification.md +14 -8
  13. package/assets/orchestrator-writer.md +5 -1
  14. package/assets/orchestrator.md +11 -11
  15. package/bin/engram-guard +1 -1
  16. package/bin/gentle-shell.mjs +237 -139
  17. package/contracts/review-provider-contract-mirror/provider-contract.lock.json +8 -7
  18. package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/README.md +7 -0
  19. package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/manifest.json +11 -2
  20. package/contracts/review-provider-contract-mirror/v1.4.0/bundle/routing/pi.md +92 -0
  21. package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/generated/provider-capabilities.baseline.json +8 -1
  22. package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/generated/provider-roles.baseline.json +1 -1
  23. package/docs/assets/diagrams/agent-orchestration.svg +54 -15
  24. package/docs/assets/diagrams/gentleman-workflow.svg +69 -11
  25. package/docs/assets/diagrams/measured-routing.svg +49 -0
  26. package/docs/assets/diagrams/native-review.svg +52 -12
  27. package/docs/assets/diagrams/odd-workflow.svg +128 -66
  28. package/docs/assets/diagrams/permission-modes.svg +51 -0
  29. package/docs/assets/features/audio-notifications.png +0 -0
  30. package/docs/assets/features/command-palette.png +0 -0
  31. package/docs/bundled-install.md +65 -30
  32. package/docs/delegated-verification.md +16 -8
  33. package/docs/gentle-agents-activity.md +16 -6
  34. package/docs/gentle-music.md +61 -0
  35. package/docs/gentle-shell-uninstall.md +8 -1
  36. package/docs/gentle-shell.md +80 -9
  37. package/docs/install-wizard.md +37 -6
  38. package/docs/permission-modes.md +8 -3
  39. package/docs/readme-reference.md +120 -28
  40. package/docs/session-profile-format.md +26 -0
  41. package/docs/voice.md +259 -0
  42. package/docs/yolo-mode.md +38 -10
  43. package/extensions/ask-user-choice.ts +4 -1
  44. package/extensions/codegraph-tools.ts +68 -6
  45. package/extensions/gentle-agents.ts +270 -48
  46. package/extensions/gentle-ai.ts +490 -113
  47. package/extensions/gentle-design.ts +158 -0
  48. package/extensions/gentle-herdr-activity.ts +2 -3
  49. package/extensions/gentle-music.ts +289 -0
  50. package/extensions/gentle-shell.ts +359 -85
  51. package/extensions/gentle-todo.ts +43 -9
  52. package/extensions/gentle-voice.ts +11 -0
  53. package/extensions/quiet-tools.ts +1 -1
  54. package/extensions/session-scratch.ts +384 -0
  55. package/extensions/skill-registry.ts +63 -8
  56. package/extensions/startup-banner.ts +2 -3
  57. package/lib/agent-home.ts +8 -3
  58. package/lib/agent-profiles.ts +26 -0
  59. package/lib/agents-config.ts +35 -0
  60. package/lib/agents-protocol.ts +60 -1
  61. package/lib/agents-runner.ts +107 -9
  62. package/lib/agents-session-transport.ts +101 -6
  63. package/lib/agents-view.ts +3 -1
  64. package/lib/agents-widget.ts +14 -1
  65. package/lib/auto-permission-memory.ts +48 -18
  66. package/lib/auto-permission-runtime.ts +21 -6
  67. package/lib/bounded-writer-admission.ts +51 -0
  68. package/lib/clipboard-image.ts +147 -0
  69. package/lib/command-palette-catalog.ts +2 -4
  70. package/lib/commit-slice-signal.ts +100 -0
  71. package/lib/context-backstop.ts +120 -0
  72. package/lib/delivered-test-lint.ts +80 -0
  73. package/lib/design-canvas-server.ts +947 -0
  74. package/lib/design-comments.ts +437 -0
  75. package/lib/design-handoffs.ts +313 -0
  76. package/lib/design-open-browser.ts +58 -0
  77. package/lib/design-sources.ts +120 -0
  78. package/lib/destructive-command-guard.ts +120 -9
  79. package/lib/external-editor.ts +31 -0
  80. package/lib/gauge-theme.ts +146 -0
  81. package/lib/gentle-ai-binary.ts +28 -7
  82. package/lib/gentle-design.ts +513 -0
  83. package/lib/gentle-shell-bundled.ts +146 -36
  84. package/lib/gentle-shell-import-pi.ts +233 -0
  85. package/lib/gentle-shell-launcher.ts +154 -15
  86. package/lib/gentle-shell-startup-update.ts +61 -9
  87. package/lib/gentle-shell-uninstall.ts +51 -2
  88. package/lib/herdr-activity.ts +24 -0
  89. package/lib/image-attachments.ts +150 -0
  90. package/lib/inprocess-reviewer.ts +33 -2
  91. package/lib/nan-provider.ts +3 -0
  92. package/lib/native-review-cli.ts +20 -5
  93. package/lib/notification-audio-native.ts +149 -13
  94. package/lib/notification-audio.ts +4 -0
  95. package/lib/notification-pulse-worker.ts +82 -18
  96. package/lib/notification-service.ts +5 -1
  97. package/lib/orchestrator-discovery.ts +5 -1
  98. package/lib/orchestrator-presence.ts +87 -8
  99. package/lib/orchestrator-scope.ts +31 -3
  100. package/lib/orchestrator-state.ts +11 -3
  101. package/lib/permission-mode-policy.ts +2 -2
  102. package/lib/provider-contract-bundle.ts +99 -53
  103. package/lib/review-candidate-view-owner.ts +29 -19
  104. package/lib/review-candidate-view.ts +36 -2
  105. package/lib/review-host-relay.ts +11 -2
  106. package/lib/review-reminder-receipt.ts +24 -1
  107. package/lib/review-risk-assessment.ts +43 -38
  108. package/lib/review-session-standing-permission.ts +3 -1
  109. package/lib/session-profile-append-controller.ts +713 -0
  110. package/lib/session-profile-disk-reader.ts +182 -0
  111. package/lib/session-profile-freeze.ts +30 -1
  112. package/lib/session-profile-persistence.ts +15 -7
  113. package/lib/session-scratch.ts +523 -0
  114. package/lib/session-worktree-registry.ts +7 -0
  115. package/lib/shell-bar.ts +29 -28
  116. package/lib/shell-changes-view.ts +80 -13
  117. package/lib/shell-changes.ts +81 -5
  118. package/lib/shell-gauge.ts +49 -6
  119. package/lib/shell-prompt.ts +4 -4
  120. package/lib/shell-sidebar-layout.ts +26 -6
  121. package/lib/shell-todo.ts +24 -5
  122. package/lib/shell-usage-view.ts +5 -1
  123. package/lib/shell-usage.ts +7 -7
  124. package/lib/verifier-admission.ts +30 -0
  125. package/lib/vim-editor-adapter.ts +11 -8
  126. package/lib/voice-artifacts.ts +188 -0
  127. package/lib/voice-command.ts +576 -0
  128. package/lib/voice-install.ts +455 -0
  129. package/lib/voice-platform.ts +99 -0
  130. package/lib/voice-push-to-talk.ts +262 -0
  131. package/lib/voice-session.ts +460 -0
  132. package/lib/voice-settings.ts +50 -0
  133. package/lib/voice-ui.ts +158 -0
  134. package/lib/yolo-session-policy.ts +58 -22
  135. package/lib/yolo-temp-scratch.ts +69 -0
  136. package/package.json +1 -2
  137. package/runtime/gentle-ai-binary.mjs +28 -7
  138. package/runtime/gentle-shell-bundled.mjs +145 -35
  139. package/runtime/gentle-shell-import-pi.mjs +234 -0
  140. package/runtime/gentle-shell-launcher.mjs +152 -13
  141. package/runtime/gentle-shell-startup-update.mjs +61 -9
  142. package/runtime/gentle-shell-uninstall.mjs +51 -2
  143. package/runtime/native-review-cli.mjs +19 -4
  144. package/runtime/review-risk-assessment.mjs +43 -38
  145. package/scripts/build-runtime-modules.mjs +1 -0
  146. package/scripts/bundled-engram-migration.mjs +119 -0
  147. package/scripts/gentle-ai-installer.mjs +10 -10
  148. package/scripts/installer-downloads.mjs +7 -7
  149. package/scripts/installer-preflight.mjs +5 -3
  150. package/scripts/installer-server.mjs +14 -4
  151. package/scripts/installer-windows.mjs +37 -7
  152. package/scripts/main-channel.mjs +94 -19
  153. package/scripts/pi-update.mjs +34 -6
  154. package/scripts/run-test-suite.mjs +5 -0
  155. package/scripts/verify-package-files.mjs +43 -13
  156. package/scripts/voice-worker.cjs +476 -0
  157. package/skills/design/SKILL.md +110 -0
  158. package/skills/gentle-ai/SKILL.md +3 -3
  159. package/skills/work-unit-commits/SKILL.md +5 -3
  160. package/tests/agent-profiles.test.ts +24 -0
  161. package/tests/agent-risk-escalation.test.ts +37 -10
  162. package/tests/agents-config.test.ts +61 -0
  163. package/tests/agents-fake-child.ts +8 -1
  164. package/tests/agents-protocol.test.ts +32 -0
  165. package/tests/agents-runner.test.ts +485 -7
  166. package/tests/agents-session-transport.test.ts +53 -3
  167. package/tests/ask-user-choice.test.ts +30 -0
  168. package/tests/auto-mode.test.ts +17 -0
  169. package/tests/auto-permission-memory-diagnostics.test.ts +56 -0
  170. package/tests/auto-permission-runtime.test.ts +85 -1
  171. package/tests/autonomous-guard.test.ts +142 -0
  172. package/tests/bounded-writer-admission.test.ts +11 -3
  173. package/tests/bundled-install.test.ts +3 -3
  174. package/tests/card-content-policy.test.ts +19 -9
  175. package/tests/child-safety.test.ts +39 -0
  176. package/tests/clipboard-image.test.ts +114 -0
  177. package/tests/codegraph-tools.test.ts +123 -2
  178. package/tests/command-palette.test.ts +15 -5
  179. package/tests/commit-slice-signal.test.ts +162 -0
  180. package/tests/context-backstop.test.ts +225 -0
  181. package/tests/delegate-for-reason-contract.test.ts +92 -24
  182. package/tests/delivered-test-lint.test.ts +105 -0
  183. package/tests/design-canvas-assets.test.ts +710 -0
  184. package/tests/design-canvas-server.test.ts +1096 -0
  185. package/tests/design-comments.test.ts +201 -0
  186. package/tests/design-handoffs.test.ts +282 -0
  187. package/tests/design-open-browser.test.ts +70 -0
  188. package/tests/design-sources.test.ts +132 -0
  189. package/tests/destructive-command-guard.test.ts +85 -1
  190. package/tests/engram-safety.test.ts +132 -57
  191. package/tests/external-editor.test.ts +84 -0
  192. package/tests/fixtures/primary-session-prompt/gentleman.golden.md +143 -0
  193. package/tests/fixtures/provider-contract-bundle/v1.4.0/README.md +29 -0
  194. package/tests/fixtures/provider-contract-bundle/v1.4.0/manifest.json +83 -0
  195. package/tests/fixtures/provider-contract-bundle/v1.4.0/orchestration/pi.md +70 -0
  196. package/tests/fixtures/provider-contract-bundle/v1.4.0/routing/pi.md +92 -0
  197. package/tests/fixtures/provider-contract-bundle/v1.4.0/schemas/lens.schema.json +16 -0
  198. package/tests/fixtures/provider-contract-bundle/v1.4.0/schemas/refuter.schema.json +1 -0
  199. package/tests/fixtures/provider-contract-bundle/v1.4.0/schemas/targeted-validator.schema.json +1 -0
  200. package/tests/fixtures/provider-contract-bundle/v1.4.0/vectors/lens.json +1 -0
  201. package/tests/fixtures/provider-contract-bundle/v1.4.0/vectors/refuter.json +1 -0
  202. package/tests/fixtures/provider-contract-bundle/v1.4.0/vectors/targeted-validator.json +1 -0
  203. package/tests/gauge-theme.test.ts +120 -0
  204. package/tests/gentle-agents.test.ts +645 -21
  205. package/tests/gentle-ai-binary.test.ts +1 -1
  206. package/tests/gentle-ai-child-guards.test.ts +61 -0
  207. package/tests/gentle-ai-doctor-herdr.test.ts +94 -0
  208. package/tests/gentle-ai-installer.test.ts +51 -51
  209. package/tests/gentle-ai.test.ts +292 -13
  210. package/tests/gentle-design.test.ts +1313 -0
  211. package/tests/gentle-music.test.ts +335 -0
  212. package/tests/gentle-shell-bin.test.ts +163 -0
  213. package/tests/gentle-shell-bundled.test.ts +392 -176
  214. package/tests/gentle-shell-import-pi.test.ts +262 -0
  215. package/tests/gentle-shell-launcher.test.ts +107 -2
  216. package/tests/gentle-shell-startup-update.test.ts +77 -1
  217. package/tests/gentle-shell-uninstall.test.ts +89 -0
  218. package/tests/gentle-shell.test.ts +225 -17
  219. package/tests/gentle-todo.test.ts +190 -0
  220. package/tests/headless-delivery-question.test.ts +58 -0
  221. package/tests/herdr-activity.test.ts +34 -1
  222. package/tests/image-attachments.test.ts +119 -0
  223. package/tests/image-paste-chips.test.ts +236 -0
  224. package/tests/inprocess-reviewer.test.ts +55 -2
  225. package/tests/installer-bundles.test.ts +12 -6
  226. package/tests/installer-posix-bootstrap.test.ts +8 -8
  227. package/tests/installer-preflight.test.ts +16 -0
  228. package/tests/installer-server.test.ts +10 -2
  229. package/tests/installer-windows-bootstrap.test.ts +94 -7
  230. package/tests/installer-windows-diagnostics.test.ts +111 -0
  231. package/tests/main-channel-upgrade.test.ts +232 -2
  232. package/tests/main-channel.test.ts +140 -21
  233. package/tests/nan-provider.test.ts +36 -0
  234. package/tests/native-review-capability-contract.test.ts +17 -4
  235. package/tests/native-review-parity.test.ts +23 -2
  236. package/tests/no-self-pause-contract.test.ts +5 -2
  237. package/tests/notification-audio-native.test.ts +128 -9
  238. package/tests/notification-customize.test.ts +17 -5
  239. package/tests/odd-integration.test.ts +5 -1
  240. package/tests/odd-routing-canonical-ratchet.test.ts +128 -162
  241. package/tests/odd-routing-contract.test.ts +115 -30
  242. package/tests/orchestrator-budget.test.ts +22 -8
  243. package/tests/orchestrator-discovery.test.ts +29 -0
  244. package/tests/orchestrator-presence.test.ts +100 -2
  245. package/tests/orchestrator-rdd-ownership.test.ts +3 -1
  246. package/tests/orchestrator-scope.test.ts +80 -0
  247. package/tests/orchestrator-state.test.ts +29 -0
  248. package/tests/orchestrator-status-publication-contract.test.ts +12 -8
  249. package/tests/package-files.test.ts +34 -0
  250. package/tests/package-manifest.test.ts +24 -20
  251. package/tests/permission-mode-policy.test.ts +10 -1
  252. package/tests/pi-update-launcher.test.ts +21 -2
  253. package/tests/pi-update.test.ts +99 -4
  254. package/tests/primary-session-prompt.test.ts +101 -0
  255. package/tests/provider-contract-bundle.test.ts +118 -0
  256. package/tests/provider-contract-mirror.test.ts +24 -0
  257. package/tests/quiet-tool-rendering.test.ts +23 -0
  258. package/tests/rdd-aware-verification-contract.test.ts +29 -11
  259. package/tests/rdd-status-line.test.ts +79 -0
  260. package/tests/review-agent-end-preflight.test.ts +6 -0
  261. package/tests/review-candidate-view.test.ts +49 -0
  262. package/tests/review-contract-prompt.test.ts +5 -2
  263. package/tests/review-controller-native-routing.test.ts +14 -1
  264. package/tests/review-controller.test.ts +1 -1
  265. package/tests/review-host-relay-routing.test.ts +147 -3
  266. package/tests/review-host-relay.test.ts +9 -2
  267. package/tests/review-json-arguments.test.ts +20 -3
  268. package/tests/review-loadout-rdd-off-sdk.test.ts +258 -0
  269. package/tests/review-owner-identity.test.ts +132 -0
  270. package/tests/review-relay-transport-agent.test.ts +26 -0
  271. package/tests/review-reminder-receipt.test.ts +31 -1
  272. package/tests/review-risk-assessment.test.ts +163 -76
  273. package/tests/review-session-standing-permission-controller.test.ts +14 -1
  274. package/tests/review-session-standing-permission.test.ts +27 -1
  275. package/tests/run-test-suite.test.ts +15 -0
  276. package/tests/runtime-harness.mjs +16 -6
  277. package/tests/session-changes-shell.test.ts +1 -1
  278. package/tests/session-changes.test.ts +2 -1
  279. package/tests/session-profile-append-controller-fork.test.ts +458 -0
  280. package/tests/session-profile-append-controller-pi.test.ts +290 -0
  281. package/tests/session-profile-append-controller.test.ts +731 -0
  282. package/tests/session-profile-disk-reader-pi.test.ts +183 -0
  283. package/tests/session-profile-disk-reader.test.ts +311 -0
  284. package/tests/session-profile-freeze.test.ts +23 -0
  285. package/tests/session-scratch.test.ts +1011 -0
  286. package/tests/session-subject-routing-contract.test.ts +2 -1
  287. package/tests/session-worktree-registry.test.ts +13 -1
  288. package/tests/shell-bar.test.ts +21 -2
  289. package/tests/shell-changes-view.test.ts +137 -1
  290. package/tests/shell-changes.test.ts +148 -1
  291. package/tests/shell-gauge.test.ts +42 -0
  292. package/tests/shell-prompt.test.ts +3 -3
  293. package/tests/shell-render-cache.test.ts +134 -1
  294. package/tests/shell-sidebar-layout.test.ts +50 -0
  295. package/tests/short-check-routing-contract.test.ts +1 -1
  296. package/tests/skill-collision-prefixes.test.ts +3 -1
  297. package/tests/skill-registry-watcher.test.ts +17 -5
  298. package/tests/skill-registry.test.ts +47 -3
  299. package/tests/small-task-inline-contract.test.ts +37 -0
  300. package/tests/standalone-config-home.test.ts +324 -0
  301. package/tests/startup-banner.test.ts +5 -4
  302. package/tests/startup-home-repo.test.ts +34 -0
  303. package/tests/support/orchestrator-modules.ts +1 -0
  304. package/tests/task-size-routing-contract.test.ts +70 -10
  305. package/tests/test-scratch-contract.test.ts +102 -0
  306. package/tests/test-strength-contract.test.ts +36 -1
  307. package/tests/tool-batching-contract.test.ts +64 -0
  308. package/tests/verification-proportionality.test.ts +46 -0
  309. package/tests/verifier-admission.test.ts +154 -0
  310. package/tests/verify-agent-contract.test.ts +8 -8
  311. package/tests/voice-command.test.ts +773 -0
  312. package/tests/voice-install.test.ts +323 -0
  313. package/tests/voice-platform.test.ts +176 -0
  314. package/tests/voice-push-to-talk.test.ts +444 -0
  315. package/tests/voice-session.test.ts +434 -0
  316. package/tests/voice-settings.test.ts +49 -0
  317. package/tests/voice-ui.test.ts +97 -0
  318. package/tests/voice-worker.test.ts +445 -0
  319. package/tests/weak-model-economy-prompt.test.ts +32 -0
  320. package/tests/windows-hidden-processes.test.ts +30 -2
  321. package/tests/writer-edit-surface-scope.test.ts +3 -0
  322. package/tests/writer-feature-document-admission.test.ts +156 -0
  323. package/tests/yolo-customize.test.ts +5 -1
  324. package/tests/yolo-mode.test.ts +19 -1
  325. package/tests/yolo-session-policy-capture.test.ts +104 -0
  326. package/tests/yolo-temp-scratch.test.ts +71 -0
  327. package/scripts/mirror-odd-routing.mjs +0 -242
  328. /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/orchestration/pi.md +0 -0
  329. /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/schemas/lens.schema.json +0 -0
  330. /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/schemas/refuter.schema.json +0 -0
  331. /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/schemas/targeted-validator.schema.json +0 -0
  332. /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/vectors/lens.json +0 -0
  333. /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/vectors/refuter.json +0 -0
  334. /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/vectors/targeted-validator.json +0 -0
@@ -0,0 +1,576 @@
1
+ import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import { isKeyRelease, matchesKey, type Component, type TUI } from "@earendil-works/pi-tui";
3
+ import { withOverlayRepaint } from "./overlay-repaint.ts";
4
+ import { PARAKEET_MODEL_MANIFEST, VOICE_NOTICES, voiceRuntimeManifest, type VoiceTarget } from "./voice-artifacts.ts";
5
+ import { installVoiceModel, installVoiceRuntime, readVoiceStatus, VoiceInstallError, type VoiceComponentStatus, type VoiceInstallErrorCode, type VoiceInstallProgress, type VoiceInstallResult } from "./voice-install.ts";
6
+ import { diagnoseVoicePlatform, readVoicePlatformProbe, type VoicePlatformDiagnosis, type VoicePlatformProbe } from "./voice-platform.ts";
7
+ import { SpaceHoldDetector, type PushToTalkOptions } from "./voice-push-to-talk.ts";
8
+ import { createVoiceSession, VoiceSessionError, type VoiceSessionErrorCode, type VoiceSessionState, type VoiceTranscript } from "./voice-session.ts";
9
+ import { readVoicePushToTalk, writeVoicePushToTalk } from "./voice-settings.ts";
10
+ import { pushToTalkStatus, VoiceDictationView, VoiceProgressView, type VoiceViewTheme } from "./voice-ui.ts";
11
+
12
+ // /gentle:voice (S1-S3): the only entry point to optional local dictation. It
13
+ // shows status, downloads the pinned runtime and model after an explicit
14
+ // confirmation, and records one dictation whose transcript is pasted into the
15
+ // editor draft, never submitted. Registration does nothing else: the platform
16
+ // probe, status reads, downloads and the worker all start from the command, in
17
+ // an interactive parent session only. One install or dictation runs at a time.
18
+ //
19
+ // Push-to-talk (hold Space) is the second way to dictate. When its persisted
20
+ // toggle is on, session start arms a raw input listener in an interactive
21
+ // parent session; reading the toggle is the only startup work. Whether voice is
22
+ // installed is checked at the first hold, and a missing install disarms it.
23
+ // A hold shares the single-operation lock with the command.
24
+
25
+ export const VOICE_COMMAND_NAME = "gentle:voice";
26
+
27
+ // The part of VoiceSession the command drives.
28
+ export interface VoiceDictation {
29
+ readonly state: VoiceSessionState;
30
+ start(): Promise<void>;
31
+ stop(): Promise<VoiceTranscript>;
32
+ cancel(): Promise<void>;
33
+ }
34
+
35
+ export interface VoiceStatusSnapshot {
36
+ voiceHome: string;
37
+ model: VoiceComponentStatus;
38
+ runtime?: VoiceComponentStatus;
39
+ }
40
+
41
+ interface VoiceInstallRequest {
42
+ signal: AbortSignal;
43
+ onProgress(progress: VoiceInstallProgress): void;
44
+ }
45
+
46
+ export interface VoiceCommandDeps {
47
+ env?: NodeJS.ProcessEnv;
48
+ probe?: () => VoicePlatformProbe;
49
+ readStatus?: (target: VoiceTarget) => Promise<VoiceStatusSnapshot>;
50
+ installRuntime?: (options: VoiceInstallRequest & { target: VoiceTarget }) => Promise<VoiceInstallResult>;
51
+ installModel?: (options: VoiceInstallRequest) => Promise<VoiceInstallResult>;
52
+ createSession?: (options: { target: VoiceTarget; onStateChange(state: VoiceSessionState): void }) => VoiceDictation;
53
+ // The persisted push-to-talk toggle.
54
+ settings?: { read(): boolean; write(enabled: boolean): void };
55
+ pushToTalk?: PushToTalkOptions;
56
+ }
57
+
58
+ const MB = 1_000_000;
59
+ const OVERLAY = { overlay: true, overlayOptions: { anchor: "center", width: "60%", minWidth: 44 } } as const;
60
+
61
+ const START = "Start dictation";
62
+ const STATUS = "Show status and notices";
63
+ const DISABLE_PUSH_TO_TALK = "Disable push-to-talk (hold Space)";
64
+ const ENABLE_PUSH_TO_TALK = "Enable push-to-talk (hold Space)";
65
+ const STATUS_KEY = "gentle-voice";
66
+
67
+ const INSTALL_MESSAGES: Record<VoiceInstallErrorCode, string> = {
68
+ aborted: "Voice download was cancelled. Unfinished files were removed; completed components are kept.",
69
+ timeout: "Voice download timed out. Check the network connection and try again.",
70
+ http: "The download server answered with an error. Try again later.",
71
+ redirect: "The download was redirected to an unexpected location and was refused.",
72
+ size: "A downloaded file did not match its pinned size or digest and was discarded.",
73
+ integrity: "A downloaded file did not match its pinned size or digest and was discarded.",
74
+ archive: "A downloaded runtime package failed validation and was discarded.",
75
+ unsupported: "Voice dictation has no pinned runtime for this platform.",
76
+ io: "Voice files could not be written to the voice directory. Check its permissions and free space.",
77
+ };
78
+
79
+ type DictationStart = "command" | "hold";
80
+
81
+ // What the user can do about a session failure, by platform where it differs.
82
+ // Gentle AI never runs an installer for any of these.
83
+ function sessionAction(code: VoiceSessionErrorCode, platform: string, start: DictationStart): string | undefined {
84
+ switch (code) {
85
+ case "not-installed":
86
+ case "runtime-invalid":
87
+ case "model-invalid":
88
+ return "Run /gentle:voice and download it again.";
89
+ case "runtime-load":
90
+ if (platform === "linux") return "Install the ALSA library (libasound.so.2, for example the libasound2 or alsa-lib package), then try again.";
91
+ if (platform === "win32") return "Install the Microsoft Visual C++ Redistributable (x64) from Microsoft, then try again.";
92
+ return "Loading has not been verified on hardware for this target.";
93
+ case "no-input-device":
94
+ return "Connect a microphone or choose a default input device, then try again.";
95
+ case "capture":
96
+ return platform === "darwin"
97
+ ? "Allow microphone access for this terminal in System Settings > Privacy & Security > Microphone, then try again."
98
+ : "Check that the default microphone is available, then try again.";
99
+ case "node-version":
100
+ return "Run Gentle AI with Node 22 or newer.";
101
+ case "empty-audio":
102
+ case "no-speech":
103
+ return start === "hold" ? "Keep holding Space until recording starts, then speak." : "Speak after recording starts, then press Enter.";
104
+ default:
105
+ return undefined;
106
+ }
107
+ }
108
+
109
+ function sessionErrorMessage(error: unknown, platform: string, start: DictationStart): string {
110
+ if (!(error instanceof VoiceSessionError)) return "Voice dictation failed.";
111
+ const action = sessionAction(error.code, platform, start);
112
+ return action ? `${error.message} ${action}` : error.message;
113
+ }
114
+
115
+ type Outcome = { kind: "text"; transcript: VoiceTranscript } | { kind: "cancelled" } | { kind: "error"; error: unknown };
116
+ type Say = (message: string, type: "info" | "warning" | "error") => void;
117
+
118
+ // Reports a settled dictation. The transcript goes through the editor's paste
119
+ // pipeline as is; it is never sent.
120
+ function deliver(ctx: ExtensionContext, result: Outcome, platform: string, start: DictationStart, say: Say, afterPaste?: () => void): void {
121
+ if (result.kind === "cancelled") {
122
+ say("Voice dictation cancelled; the draft is unchanged.", "info");
123
+ return;
124
+ }
125
+ if (result.kind === "error") {
126
+ say(sessionErrorMessage(result.error, platform, start), "error");
127
+ return;
128
+ }
129
+ const { text, truncated } = result.transcript;
130
+ if (!text.trim()) {
131
+ say("No speech was recognized.", "warning");
132
+ return;
133
+ }
134
+ ctx.ui.pasteToEditor(text);
135
+ afterPaste?.();
136
+ if (truncated) say("Recording reached the 2-minute limit; the transcript covers what was recorded.", "info");
137
+ }
138
+
139
+ // One dictation session driven to an Outcome. stop() and cancel() settle only
140
+ // after the worker has exited; a transcript that lands while cancelling is
141
+ // dropped. A release before recording starts waits for recording.
142
+ function runDictation(createSession: NonNullable<VoiceCommandDeps["createSession"]>, target: VoiceTarget, onState: (state: VoiceSessionState) => void) {
143
+ let settled = false;
144
+ let stopping = false;
145
+ let cancelled = false;
146
+ let released = false;
147
+ let resolveOutcome!: (outcome: Outcome) => void;
148
+ const outcome = new Promise<Outcome>((resolve) => { resolveOutcome = resolve; });
149
+ const settle = (value: Outcome) => {
150
+ if (settled) return;
151
+ settled = true;
152
+ resolveOutcome(cancelled ? { kind: "cancelled" } : value);
153
+ };
154
+ const collect = () => {
155
+ if (stopping || cancelled || settled) return;
156
+ if (session.state !== "recording" && session.state !== "finished") return;
157
+ stopping = true;
158
+ session.stop().then((transcript) => settle({ kind: "text", transcript }), (error) => settle({ kind: "error", error }));
159
+ };
160
+ const cancel = () => {
161
+ if (cancelled || settled) return;
162
+ cancelled = true;
163
+ session.cancel().catch(() => {}).finally(() => settle({ kind: "cancelled" }));
164
+ };
165
+ // The session calls this synchronously from its own transitions, so it
166
+ // must never throw: a throw there could leave a closing session unsettled.
167
+ const onStateChange = (state: VoiceSessionState) => {
168
+ try {
169
+ onState(state);
170
+ // The recording limit ends capture without a key press.
171
+ if (state === "finished" || (state === "recording" && released)) queueMicrotask(collect);
172
+ } catch {
173
+ /* the observer is advisory */
174
+ }
175
+ };
176
+ const session = createSession({ target, onStateChange });
177
+ return {
178
+ session,
179
+ outcome,
180
+ start: () => { session.start().catch((error) => settle({ kind: "error", error })); },
181
+ stop: () => { released = true; collect(); },
182
+ cancel,
183
+ };
184
+ }
185
+
186
+ const PREREQUISITE_ACTIONS: Partial<Record<string, string>> = {
187
+ linux: "Gentle AI never installs system libraries; install ALSA with your package manager if loading fails.",
188
+ win32: "Gentle AI never installs the Visual C++ runtime; install it from Microsoft if loading fails.",
189
+ };
190
+
191
+ const runtimeBytes = (target: VoiceTarget) => voiceRuntimeManifest(target).packages.reduce((sum, pkg) => sum + pkg.size, 0);
192
+ const modelBytes = () => PARAKEET_MODEL_MANIFEST.files.reduce((sum, file) => sum + file.size, 0);
193
+ const megabytes = (bytes: number) => `${Math.max(1, Math.round(bytes / MB))} MB`;
194
+
195
+ function noticeLines(): string[] {
196
+ return VOICE_NOTICES.flatMap((notice) => [`- ${notice.attribution ?? `${notice.component}: ${notice.license}.`} Source: ${notice.source}`, ...(notice.unresolved ? [` ${notice.unresolved}`] : [])]);
197
+ }
198
+
199
+ function limitationLines(diagnosis: VoicePlatformDiagnosis, platform: string): string[] {
200
+ const action = PREREQUISITE_ACTIONS[platform];
201
+ return [...diagnosis.limitations, ...(action ? [action] : [])].map((line) => `- ${line}`);
202
+ }
203
+
204
+ const describe = (status: VoiceComponentStatus | undefined) => (!status ? "unknown" : status.state === "invalid" && status.reason ? `invalid (${status.reason})` : status.state);
205
+
206
+ function statusReport(target: VoiceTarget, diagnosis: VoicePlatformDiagnosis, status: VoiceStatusSnapshot, platform: string): string {
207
+ return [
208
+ "Voice dictation status",
209
+ `Target: ${target} (${diagnosis.support})`,
210
+ `Voice home: ${status.voiceHome}`,
211
+ `Model: ${describe(status.model)}`,
212
+ `Runtime: ${describe(status.runtime)}`,
213
+ "Limitations:",
214
+ ...limitationLines(diagnosis, platform),
215
+ "Licenses and attribution:",
216
+ ...noticeLines(),
217
+ ].join("\n");
218
+ }
219
+
220
+ interface Overlay<V> {
221
+ view(): V | undefined;
222
+ tui(): TUI | undefined;
223
+ // Closes once, whether or not the factory has run yet; resolves when closed.
224
+ close(): Promise<void>;
225
+ }
226
+
227
+ function openOverlay<V extends Component & { dispose(): void }>(ctx: ExtensionCommandContext, make: (theme: VoiceViewTheme, requestRender: () => void) => V): Overlay<V> {
228
+ let view: V | undefined;
229
+ let tui: TUI | undefined;
230
+ let finish: ((value: undefined) => void) | undefined;
231
+ let closing = false;
232
+ const closed = ctx.ui
233
+ .custom<undefined>((t, theme, _keybindings, done) => {
234
+ tui = t;
235
+ finish = withOverlayRepaint(t, done);
236
+ view = make(theme as VoiceViewTheme, () => t.requestRender());
237
+ if (closing) queueMicrotask(() => finish?.(undefined));
238
+ return view;
239
+ }, OVERLAY)
240
+ .catch(() => undefined)
241
+ .finally(() => view?.dispose());
242
+ return {
243
+ view: () => view,
244
+ tui: () => tui,
245
+ close: () => {
246
+ if (!closing) {
247
+ closing = true;
248
+ view?.dispose();
249
+ finish?.(undefined);
250
+ }
251
+ return closed.then(() => undefined);
252
+ },
253
+ };
254
+ }
255
+
256
+ interface Run {
257
+ generation: number;
258
+ abort(): void;
259
+ }
260
+
261
+ export function registerVoiceCommand(pi: ExtensionAPI, deps: VoiceCommandDeps = {}): void {
262
+ const env = deps.env ?? process.env;
263
+ const probe = deps.probe ?? readVoicePlatformProbe;
264
+ const readStatus = deps.readStatus ?? ((target) => readVoiceStatus({ env, target }));
265
+ // Production always installs and runs the pinned manifests: no manifest or
266
+ // fetch override is ever passed from here.
267
+ const installRuntime = deps.installRuntime ?? (({ target, signal, onProgress }) => installVoiceRuntime({ env, target, signal, onProgress }));
268
+ const installModel = deps.installModel ?? (({ signal, onProgress }) => installVoiceModel({ env, signal, onProgress }));
269
+ const createSession = deps.createSession ?? (({ target, onStateChange }) => createVoiceSession({ env, target, onStateChange }));
270
+ const settings = deps.settings ?? { read: () => readVoicePushToTalk({ env }), write: (enabled: boolean) => { writeVoicePushToTalk(enabled, { env }); } };
271
+ let generation = 0;
272
+ let active: Run | undefined;
273
+
274
+ const command = async (ctx: ExtensionCommandContext): Promise<void> => {
275
+ if (env.GENTLE_PI_AGENTS_CHILD === "1") {
276
+ if (ctx.hasUI) ctx.ui.notify("Voice dictation is not available in delegated sessions.", "warning");
277
+ return;
278
+ }
279
+ if (!ctx.hasUI) return;
280
+ if (ctx.mode !== "tui") {
281
+ ctx.ui.notify("Voice dictation requires TUI mode.", "warning");
282
+ return;
283
+ }
284
+ if (active) {
285
+ ctx.ui.notify("Voice dictation is already running. Finish or cancel it first.", "warning");
286
+ return;
287
+ }
288
+ const run: Run = { generation, abort: () => {} };
289
+ active = run;
290
+ try {
291
+ await workflow(ctx, run);
292
+ } catch {
293
+ if (run.generation === generation) ctx.ui.notify("Voice dictation failed.", "error");
294
+ } finally {
295
+ if (active === run) active = undefined;
296
+ }
297
+ };
298
+
299
+ const workflow = async (ctx: ExtensionCommandContext, run: Run): Promise<void> => {
300
+ // After a session shutdown the old context is stale: say and paste nothing.
301
+ const live = () => run.generation === generation;
302
+ const say = (message: string, type: "info" | "warning" | "error") => {
303
+ if (live()) ctx.ui.notify(message, type);
304
+ };
305
+ const platform = probe();
306
+ const diagnosis = diagnoseVoicePlatform(platform);
307
+ if (diagnosis.support === "unsupported" || !diagnosis.target) {
308
+ say(diagnosis.limitations[0] ?? "Voice dictation is not supported on this platform.", "error");
309
+ return;
310
+ }
311
+ const target = diagnosis.target;
312
+ const status = await readStatus(target);
313
+ if (!live()) return;
314
+ const runtimeMissing = status.runtime?.state !== "installed";
315
+ const modelMissing = status.model.state !== "installed";
316
+ const pending = (runtimeMissing ? runtimeBytes(target) : 0) + (modelMissing ? modelBytes() : 0);
317
+ const download = `Download voice dictation (about ${megabytes(pending)})`;
318
+ const ready = !runtimeMissing && !modelMissing;
319
+ if (ready) installed = { target, platform: platform.platform };
320
+ else if (installed !== "checking") installed = undefined;
321
+ // Enable is offered only once voice is installed; Disable whenever it is on.
322
+ const pushToTalk = readToggle();
323
+ const toggle = pushToTalk ? [DISABLE_PUSH_TO_TALK] : ready ? [ENABLE_PUSH_TO_TALK] : [];
324
+ const choice = await ctx.ui.select(ready ? "Voice dictation · ready" : "Voice dictation · not installed", ready ? [START, ...toggle, STATUS] : [download, ...toggle, STATUS]);
325
+ if (!live() || !choice) return;
326
+ if (choice === STATUS) say(statusReport(target, diagnosis, status, platform.platform), "info");
327
+ else if (choice === DISABLE_PUSH_TO_TALK || choice === ENABLE_PUSH_TO_TALK) setPushToTalk(ctx, choice === ENABLE_PUSH_TO_TALK, say);
328
+ else if (choice === download) await install(ctx, run, { target, diagnosis, status, platform: platform.platform, runtimeMissing, modelMissing }, say);
329
+ else if (choice === START) await dictate(ctx, run, target, platform.platform, say);
330
+ };
331
+
332
+ const install = async (
333
+ ctx: ExtensionCommandContext,
334
+ run: Run,
335
+ plan: { target: VoiceTarget; diagnosis: VoicePlatformDiagnosis; status: VoiceStatusSnapshot; platform: string; runtimeMissing: boolean; modelMissing: boolean },
336
+ say: (message: string, type: "info" | "warning" | "error") => void,
337
+ ): Promise<void> => {
338
+ const items = [
339
+ ...(plan.runtimeMissing ? [`- Voice runtime for ${plan.target} (sherpa-onnx, node-cpal) from the npm registry: ${megabytes(runtimeBytes(plan.target))}`] : []),
340
+ ...(plan.modelMissing ? [`- Parakeet TDT 0.6B v3 int8 model from Hugging Face: ${megabytes(modelBytes())}`] : []),
341
+ ];
342
+ const message = [
343
+ `Downloads into ${plan.status.voiceHome}:`,
344
+ ...items,
345
+ "Every file is checked against its pinned size and digest. Nothing downloaded is loaded or run during the download.",
346
+ "Licenses and attribution:",
347
+ ...noticeLines(),
348
+ "Limitations:",
349
+ ...limitationLines(plan.diagnosis, plan.platform),
350
+ ].join("\n");
351
+ if (!(await ctx.ui.confirm("Download voice dictation?", message))) {
352
+ say("Voice download skipped; nothing was downloaded.", "info");
353
+ return;
354
+ }
355
+ if (run.generation !== generation) return;
356
+ const controller = new AbortController();
357
+ run.abort = () => controller.abort();
358
+ const overlay = openOverlay(ctx, (theme, requestRender) => new VoiceProgressView({ theme, requestRender, onCancel: () => controller.abort() }));
359
+ const onProgress = (progress: VoiceInstallProgress) => overlay.view()?.update(progress);
360
+ let failure: unknown;
361
+ try {
362
+ if (plan.runtimeMissing) await installRuntime({ target: plan.target, signal: controller.signal, onProgress });
363
+ if (plan.modelMissing && !controller.signal.aborted) await installModel({ signal: controller.signal, onProgress });
364
+ } catch (error) {
365
+ failure = error;
366
+ } finally {
367
+ run.abort = () => {};
368
+ await overlay.close();
369
+ }
370
+ if (controller.signal.aborted) say(INSTALL_MESSAGES.aborted, "info");
371
+ else if (failure !== undefined) say(failure instanceof VoiceInstallError ? INSTALL_MESSAGES[failure.code] : "Voice download failed.", "error");
372
+ else {
373
+ // Push-to-talk is turned on by a successful download (assumption in
374
+ // the task document); the menu turns it off again.
375
+ installed = { target: plan.target, platform: plan.platform };
376
+ let enabled = false;
377
+ try {
378
+ if (run.generation === generation) {
379
+ settings.write(true);
380
+ enabled = true;
381
+ arm(ctx);
382
+ }
383
+ } catch {
384
+ /* push-to-talk stays off; /gentle:voice still dictates */
385
+ }
386
+ say(enabled ? "Voice dictation is installed. Hold Space in the editor to dictate, or run /gentle:voice and choose Start dictation." : "Voice dictation is installed. Run /gentle:voice and choose Start dictation.", "info");
387
+ }
388
+ };
389
+
390
+ const dictate = async (ctx: ExtensionCommandContext, run: Run, target: VoiceTarget, platform: string, say: Say): Promise<void> => {
391
+ let overlay: Overlay<VoiceDictationView> | undefined;
392
+ const dictation = runDictation(createSession, target, (state) => overlay?.view()?.setState(state));
393
+ const { session } = dictation;
394
+ run.abort = dictation.cancel;
395
+ overlay = openOverlay(ctx, (theme, requestRender) => {
396
+ const view = new VoiceDictationView({ theme, requestRender, onStop: dictation.stop, onCancel: dictation.cancel });
397
+ // The factory may run after the session has already moved on.
398
+ if (session.state !== "idle") view.setState(session.state);
399
+ return view;
400
+ });
401
+ dictation.start();
402
+ const result = await dictation.outcome;
403
+ run.abort = () => {};
404
+ const tui = overlay.tui();
405
+ await overlay.close();
406
+ if (run.generation !== generation) return;
407
+ deliver(ctx, result, platform, "command", say, () => {
408
+ setTimeout(() => {
409
+ try {
410
+ tui?.requestRender();
411
+ } catch {
412
+ /* best-effort repaint after the overlay teardown */
413
+ }
414
+ }, 0);
415
+ });
416
+ };
417
+
418
+ // --- push-to-talk (hold Space) ----------------------------------------------
419
+
420
+ interface Armed {
421
+ ctx: ExtensionContext;
422
+ detector: SpaceHoldDetector;
423
+ unsubscribe(): void;
424
+ // The dictation a hold started, until it settles.
425
+ hold?: { release(): void; cancel(): void };
426
+ }
427
+ let armed: Armed | undefined;
428
+ // Learned at the first hold or from the command: a target once voice is
429
+ // known to be installed, "checking" while the lazy check runs.
430
+ let installed: { target: VoiceTarget; platform: string } | "checking" | undefined;
431
+
432
+ const readToggle = (): boolean => {
433
+ try {
434
+ return settings.read();
435
+ } catch {
436
+ return false;
437
+ }
438
+ };
439
+
440
+ const disarm = () => {
441
+ const current = armed;
442
+ armed = undefined;
443
+ if (!current) return;
444
+ current.detector.dispose();
445
+ try {
446
+ current.unsubscribe();
447
+ } catch {
448
+ /* the input listener is already gone */
449
+ }
450
+ };
451
+
452
+ const arm = (ctx: ExtensionContext) => {
453
+ if (armed) return;
454
+ const current: Armed = {
455
+ ctx,
456
+ detector: new SpaceHoldDetector({ readDraft: () => ctx.ui.getEditorText(), onHold: () => startHold(current), onRelease: () => current.hold?.release() }, deps.pushToTalk),
457
+ unsubscribe: () => {},
458
+ };
459
+ armed = current;
460
+ current.unsubscribe = ctx.ui.onTerminalInput((data) => {
461
+ try {
462
+ if (current.hold && !isKeyRelease(data) && matchesKey(data, "escape")) {
463
+ current.detector.end();
464
+ current.hold.cancel();
465
+ return { consume: true };
466
+ }
467
+ return current.detector.handleInput(data);
468
+ } catch {
469
+ return undefined;
470
+ }
471
+ });
472
+ };
473
+
474
+ // The first hold of a session checks the install; its spaces keep typing
475
+ // meanwhile. A missing install disarms push-to-talk until it is re-enabled.
476
+ const checkInstalled = async (current: Armed): Promise<void> => {
477
+ const checkGeneration = generation;
478
+ let found: { target: VoiceTarget; platform: string } | undefined;
479
+ try {
480
+ const platform = probe();
481
+ const diagnosis = diagnoseVoicePlatform(platform);
482
+ if (diagnosis.support !== "unsupported" && diagnosis.target) {
483
+ const status = await readStatus(diagnosis.target);
484
+ if (status.runtime?.state === "installed" && status.model.state === "installed") found = { target: diagnosis.target, platform: platform.platform };
485
+ }
486
+ } catch {
487
+ /* an unreadable install counts as not installed */
488
+ }
489
+ if (installed === "checking") installed = found;
490
+ if (checkGeneration !== generation || armed !== current || found) return;
491
+ disarm();
492
+ current.ctx.ui.notify("Push-to-talk is paused: voice dictation is not installed. Run /gentle:voice to download it.", "warning");
493
+ };
494
+
495
+ const startHold = (current: Armed): boolean => {
496
+ if (armed !== current || active) return false;
497
+ if (installed === undefined) {
498
+ installed = "checking";
499
+ void checkInstalled(current);
500
+ return false;
501
+ }
502
+ if (installed === "checking") return false;
503
+ const run: Run = { generation, abort: () => {} };
504
+ active = run;
505
+ void holdDictation(current, run, installed.target, installed.platform)
506
+ .catch(() => {
507
+ if (run.generation === generation) current.ctx.ui.notify("Voice dictation failed.", "error");
508
+ })
509
+ .finally(() => {
510
+ if (active === run) active = undefined;
511
+ });
512
+ return true;
513
+ };
514
+
515
+ // A hold records into the draft with a footer status instead of an overlay,
516
+ // so the editor keeps the focus. Esc cancels; nothing is ever sent.
517
+ const holdDictation = async (current: Armed, run: Run, target: VoiceTarget, platform: string): Promise<void> => {
518
+ const { ctx } = current;
519
+ const live = () => run.generation === generation;
520
+ const say: Say = (message, type) => {
521
+ if (live()) ctx.ui.notify(message, type);
522
+ };
523
+ const status = (text: string | undefined) => {
524
+ if (!live()) return;
525
+ try {
526
+ ctx.ui.setStatus(STATUS_KEY, text);
527
+ } catch {
528
+ /* the status is display only */
529
+ }
530
+ };
531
+ const dictation = runDictation(createSession, target, (state) => status(pushToTalkStatus(state)));
532
+ run.abort = dictation.cancel;
533
+ current.hold = { release: dictation.stop, cancel: dictation.cancel };
534
+ status(pushToTalkStatus("starting"));
535
+ try {
536
+ dictation.start();
537
+ const result = await dictation.outcome;
538
+ if (live()) deliver(ctx, result, platform, "hold", say);
539
+ } finally {
540
+ run.abort = () => {};
541
+ current.hold = undefined;
542
+ status(undefined);
543
+ }
544
+ };
545
+
546
+ const setPushToTalk = (ctx: ExtensionContext, enabled: boolean, say: Say) => {
547
+ try {
548
+ settings.write(enabled);
549
+ } catch {
550
+ say("The push-to-talk setting could not be saved.", "error");
551
+ return;
552
+ }
553
+ if (enabled) {
554
+ arm(ctx);
555
+ say("Push-to-talk is on: hold Space in the editor to dictate, release it to insert the text.", "info");
556
+ } else {
557
+ disarm();
558
+ say("Push-to-talk is off.", "info");
559
+ }
560
+ };
561
+
562
+ pi.registerCommand(VOICE_COMMAND_NAME, {
563
+ description: "Local voice dictation with Parakeet: download it once, then record and insert the transcript into the draft (never sent).",
564
+ handler: async (_args, ctx) => command(ctx),
565
+ });
566
+ pi.on("session_start", (_event, ctx) => {
567
+ disarm();
568
+ if (env.GENTLE_PI_AGENTS_CHILD === "1" || !ctx?.hasUI || ctx.mode !== "tui") return;
569
+ if (readToggle()) arm(ctx);
570
+ });
571
+ pi.on("session_shutdown", () => {
572
+ generation++;
573
+ active?.abort();
574
+ disarm();
575
+ });
576
+ }