lsd-pi 1.3.2 → 1.3.7

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 (205) hide show
  1. package/dist/cli.js +2 -1
  2. package/dist/lsd-settings-manager.d.ts +2 -0
  3. package/dist/lsd-settings-manager.js +5 -0
  4. package/dist/resource-loader.js +33 -3
  5. package/dist/resources/extensions/browser-tools/tools/codegen.js +5 -5
  6. package/dist/resources/extensions/browser-tools/tools/navigation.js +107 -178
  7. package/dist/resources/extensions/browser-tools/tools/network-mock.js +112 -167
  8. package/dist/resources/extensions/browser-tools/tools/pages.js +182 -234
  9. package/dist/resources/extensions/browser-tools/tools/refs.js +202 -461
  10. package/dist/resources/extensions/browser-tools/tools/session.js +176 -323
  11. package/dist/resources/extensions/browser-tools/tools/state-persistence.js +91 -154
  12. package/dist/resources/extensions/browser-tools/utils.js +1 -1
  13. package/dist/resources/extensions/cache-timer/index.js +3 -2
  14. package/dist/resources/extensions/slash-commands/extension-manifest.json +2 -2
  15. package/dist/resources/extensions/slash-commands/fast.js +73 -0
  16. package/dist/resources/extensions/slash-commands/index.js +2 -0
  17. package/dist/resources/extensions/slash-commands/plan.js +37 -12
  18. package/dist/resources/extensions/subagent/background-job-manager.js +13 -0
  19. package/dist/resources/extensions/subagent/in-process-runner.js +387 -0
  20. package/dist/resources/extensions/subagent/index.js +278 -626
  21. package/dist/resources/extensions/subagent/legacy-runner.js +503 -0
  22. package/dist/resources/extensions/voice/index.js +96 -36
  23. package/dist/resources/extensions/voice/push-to-talk.js +26 -0
  24. package/dist/welcome-screen.js +2 -2
  25. package/package.json +1 -1
  26. package/packages/pi-agent-core/dist/agent.d.ts +19 -0
  27. package/packages/pi-agent-core/dist/agent.d.ts.map +1 -1
  28. package/packages/pi-agent-core/dist/agent.js +16 -0
  29. package/packages/pi-agent-core/dist/agent.js.map +1 -1
  30. package/packages/pi-agent-core/src/agent.ts +32 -2
  31. package/packages/pi-ai/dist/providers/openai-codex-responses.d.ts +34 -1
  32. package/packages/pi-ai/dist/providers/openai-codex-responses.d.ts.map +1 -1
  33. package/packages/pi-ai/dist/providers/openai-codex-responses.js +32 -4
  34. package/packages/pi-ai/dist/providers/openai-codex-responses.js.map +1 -1
  35. package/packages/pi-ai/dist/providers/openai-codex-responses.test.js +127 -16
  36. package/packages/pi-ai/dist/providers/openai-codex-responses.test.js.map +1 -1
  37. package/packages/pi-ai/dist/providers/openai-responses.d.ts +8 -1
  38. package/packages/pi-ai/dist/providers/openai-responses.d.ts.map +1 -1
  39. package/packages/pi-ai/dist/providers/openai-responses.fast-mode.test.d.ts +2 -0
  40. package/packages/pi-ai/dist/providers/openai-responses.fast-mode.test.d.ts.map +1 -0
  41. package/packages/pi-ai/dist/providers/openai-responses.fast-mode.test.js +67 -0
  42. package/packages/pi-ai/dist/providers/openai-responses.fast-mode.test.js.map +1 -0
  43. package/packages/pi-ai/dist/providers/openai-responses.js +21 -3
  44. package/packages/pi-ai/dist/providers/openai-responses.js.map +1 -1
  45. package/packages/pi-ai/dist/providers/simple-options.d.ts.map +1 -1
  46. package/packages/pi-ai/dist/providers/simple-options.js +2 -0
  47. package/packages/pi-ai/dist/providers/simple-options.js.map +1 -1
  48. package/packages/pi-ai/dist/types.d.ts +5 -0
  49. package/packages/pi-ai/dist/types.d.ts.map +1 -1
  50. package/packages/pi-ai/dist/types.js.map +1 -1
  51. package/packages/pi-ai/src/providers/openai-codex-responses.test.ts +143 -20
  52. package/packages/pi-ai/src/providers/openai-codex-responses.ts +47 -4
  53. package/packages/pi-ai/src/providers/openai-responses.fast-mode.test.ts +73 -0
  54. package/packages/pi-ai/src/providers/openai-responses.ts +26 -3
  55. package/packages/pi-ai/src/providers/simple-options.ts +2 -0
  56. package/packages/pi-ai/src/types.ts +5 -0
  57. package/packages/pi-coding-agent/dist/core/keybindings.d.ts +1 -1
  58. package/packages/pi-coding-agent/dist/core/keybindings.d.ts.map +1 -1
  59. package/packages/pi-coding-agent/dist/core/keybindings.js +2 -0
  60. package/packages/pi-coding-agent/dist/core/keybindings.js.map +1 -1
  61. package/packages/pi-coding-agent/dist/core/sdk.d.ts.map +1 -1
  62. package/packages/pi-coding-agent/dist/core/sdk.js +4 -2
  63. package/packages/pi-coding-agent/dist/core/sdk.js.map +1 -1
  64. package/packages/pi-coding-agent/dist/core/settings-manager.collapse-tool-calls.test.d.ts +2 -0
  65. package/packages/pi-coding-agent/dist/core/settings-manager.collapse-tool-calls.test.d.ts.map +1 -0
  66. package/packages/pi-coding-agent/dist/core/settings-manager.collapse-tool-calls.test.js +35 -0
  67. package/packages/pi-coding-agent/dist/core/settings-manager.collapse-tool-calls.test.js.map +1 -0
  68. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +12 -0
  69. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
  70. package/packages/pi-coding-agent/dist/core/settings-manager.fast-mode.test.d.ts +2 -0
  71. package/packages/pi-coding-agent/dist/core/settings-manager.fast-mode.test.d.ts.map +1 -0
  72. package/packages/pi-coding-agent/dist/core/settings-manager.fast-mode.test.js +35 -0
  73. package/packages/pi-coding-agent/dist/core/settings-manager.fast-mode.test.js.map +1 -0
  74. package/packages/pi-coding-agent/dist/core/settings-manager.js +24 -0
  75. package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
  76. package/packages/pi-coding-agent/dist/core/slash-commands.d.ts.map +1 -1
  77. package/packages/pi-coding-agent/dist/core/slash-commands.js +1 -0
  78. package/packages/pi-coding-agent/dist/core/slash-commands.js.map +1 -1
  79. package/packages/pi-coding-agent/dist/core/system-prompt.d.ts.map +1 -1
  80. package/packages/pi-coding-agent/dist/core/system-prompt.js +6 -1
  81. package/packages/pi-coding-agent/dist/core/system-prompt.js.map +1 -1
  82. package/packages/pi-coding-agent/dist/core/tool-priority.d.ts +4 -0
  83. package/packages/pi-coding-agent/dist/core/tool-priority.d.ts.map +1 -0
  84. package/packages/pi-coding-agent/dist/core/tool-priority.js +18 -0
  85. package/packages/pi-coding-agent/dist/core/tool-priority.js.map +1 -0
  86. package/packages/pi-coding-agent/dist/core/tool-priority.test.d.ts +2 -0
  87. package/packages/pi-coding-agent/dist/core/tool-priority.test.d.ts.map +1 -0
  88. package/packages/pi-coding-agent/dist/core/tool-priority.test.js +27 -0
  89. package/packages/pi-coding-agent/dist/core/tool-priority.test.js.map +1 -0
  90. package/packages/pi-coding-agent/dist/core/tools/edit-diff.d.ts +5 -0
  91. package/packages/pi-coding-agent/dist/core/tools/edit-diff.d.ts.map +1 -1
  92. package/packages/pi-coding-agent/dist/core/tools/edit-diff.js +21 -0
  93. package/packages/pi-coding-agent/dist/core/tools/edit-diff.js.map +1 -1
  94. package/packages/pi-coding-agent/dist/core/tools/edit-diff.test.js +16 -1
  95. package/packages/pi-coding-agent/dist/core/tools/edit-diff.test.js.map +1 -1
  96. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/tool-summary-line.test.d.ts +2 -0
  97. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/tool-summary-line.test.d.ts.map +1 -0
  98. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/tool-summary-line.test.js +34 -0
  99. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/tool-summary-line.test.js.map +1 -0
  100. package/packages/pi-coding-agent/dist/modes/interactive/components/btw-overlay.d.ts +45 -0
  101. package/packages/pi-coding-agent/dist/modes/interactive/components/btw-overlay.d.ts.map +1 -0
  102. package/packages/pi-coding-agent/dist/modes/interactive/components/btw-overlay.js +314 -0
  103. package/packages/pi-coding-agent/dist/modes/interactive/components/btw-overlay.js.map +1 -0
  104. package/packages/pi-coding-agent/dist/modes/interactive/components/btw-overlay.test.d.ts +2 -0
  105. package/packages/pi-coding-agent/dist/modes/interactive/components/btw-overlay.test.d.ts.map +1 -0
  106. package/packages/pi-coding-agent/dist/modes/interactive/components/btw-overlay.test.js +122 -0
  107. package/packages/pi-coding-agent/dist/modes/interactive/components/btw-overlay.test.js.map +1 -0
  108. package/packages/pi-coding-agent/dist/modes/interactive/components/diff.d.ts +7 -5
  109. package/packages/pi-coding-agent/dist/modes/interactive/components/diff.d.ts.map +1 -1
  110. package/packages/pi-coding-agent/dist/modes/interactive/components/diff.js +86 -28
  111. package/packages/pi-coding-agent/dist/modes/interactive/components/diff.js.map +1 -1
  112. package/packages/pi-coding-agent/dist/modes/interactive/components/footer.d.ts +4 -0
  113. package/packages/pi-coding-agent/dist/modes/interactive/components/footer.d.ts.map +1 -1
  114. package/packages/pi-coding-agent/dist/modes/interactive/components/footer.js +23 -10
  115. package/packages/pi-coding-agent/dist/modes/interactive/components/footer.js.map +1 -1
  116. package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.d.ts +8 -0
  117. package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  118. package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.js +52 -6
  119. package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.js.map +1 -1
  120. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts +19 -0
  121. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  122. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js +127 -14
  123. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js.map +1 -1
  124. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-summary-line.d.ts +14 -0
  125. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-summary-line.d.ts.map +1 -0
  126. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-summary-line.js +93 -0
  127. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-summary-line.js.map +1 -0
  128. package/packages/pi-coding-agent/dist/modes/interactive/controllers/__tests__/chat-controller.collapsed-tool-summary.test.d.ts +2 -0
  129. package/packages/pi-coding-agent/dist/modes/interactive/controllers/__tests__/chat-controller.collapsed-tool-summary.test.d.ts.map +1 -0
  130. package/packages/pi-coding-agent/dist/modes/interactive/controllers/__tests__/chat-controller.collapsed-tool-summary.test.js +328 -0
  131. package/packages/pi-coding-agent/dist/modes/interactive/controllers/__tests__/chat-controller.collapsed-tool-summary.test.js.map +1 -0
  132. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
  133. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js +123 -0
  134. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
  135. package/packages/pi-coding-agent/dist/modes/interactive/controllers/extension-ui-controller.js +1 -1
  136. package/packages/pi-coding-agent/dist/modes/interactive/controllers/extension-ui-controller.js.map +1 -1
  137. package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.d.ts.map +1 -1
  138. package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.js +7 -0
  139. package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.js.map +1 -1
  140. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode-state.d.ts +4 -0
  141. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode-state.d.ts.map +1 -1
  142. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode-state.js.map +1 -1
  143. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts +9 -1
  144. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  145. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +103 -23
  146. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  147. package/packages/pi-coding-agent/dist/modes/interactive/slash-command-handlers.d.ts.map +1 -1
  148. package/packages/pi-coding-agent/dist/modes/interactive/slash-command-handlers.js +41 -0
  149. package/packages/pi-coding-agent/dist/modes/interactive/slash-command-handlers.js.map +1 -1
  150. package/packages/pi-coding-agent/dist/modes/interactive/theme/themes.js +4 -4
  151. package/packages/pi-coding-agent/dist/modes/interactive/theme/themes.js.map +1 -1
  152. package/packages/pi-coding-agent/package.json +1 -1
  153. package/packages/pi-coding-agent/src/core/keybindings.ts +4 -1
  154. package/packages/pi-coding-agent/src/core/sdk.ts +4 -2
  155. package/packages/pi-coding-agent/src/core/settings-manager.collapse-tool-calls.test.ts +46 -0
  156. package/packages/pi-coding-agent/src/core/settings-manager.fast-mode.test.ts +46 -0
  157. package/packages/pi-coding-agent/src/core/settings-manager.ts +36 -0
  158. package/packages/pi-coding-agent/src/core/slash-commands.ts +1 -0
  159. package/packages/pi-coding-agent/src/core/system-prompt.ts +6 -1
  160. package/packages/pi-coding-agent/src/core/tool-priority.test.ts +30 -0
  161. package/packages/pi-coding-agent/src/core/tool-priority.ts +17 -0
  162. package/packages/pi-coding-agent/src/core/tools/edit-diff.test.ts +20 -0
  163. package/packages/pi-coding-agent/src/core/tools/edit-diff.ts +26 -0
  164. package/packages/pi-coding-agent/src/modes/interactive/components/__tests__/tool-summary-line.test.ts +41 -0
  165. package/packages/pi-coding-agent/src/modes/interactive/components/btw-overlay.test.ts +172 -0
  166. package/packages/pi-coding-agent/src/modes/interactive/components/btw-overlay.ts +402 -0
  167. package/packages/pi-coding-agent/src/modes/interactive/components/diff.ts +105 -28
  168. package/packages/pi-coding-agent/src/modes/interactive/components/footer.ts +21 -6
  169. package/packages/pi-coding-agent/src/modes/interactive/components/settings-selector.ts +63 -6
  170. package/packages/pi-coding-agent/src/modes/interactive/components/tool-execution.ts +1262 -1138
  171. package/packages/pi-coding-agent/src/modes/interactive/components/tool-summary-line.ts +120 -0
  172. package/packages/pi-coding-agent/src/modes/interactive/controllers/__tests__/chat-controller.collapsed-tool-summary.test.ts +396 -0
  173. package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts +530 -398
  174. package/packages/pi-coding-agent/src/modes/interactive/controllers/extension-ui-controller.ts +1 -1
  175. package/packages/pi-coding-agent/src/modes/interactive/controllers/input-controller.ts +7 -0
  176. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode-state.ts +4 -0
  177. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +109 -23
  178. package/packages/pi-coding-agent/src/modes/interactive/slash-command-handlers.ts +60 -1
  179. package/packages/pi-coding-agent/src/modes/interactive/theme/themes.ts +4 -4
  180. package/packages/pi-tui/dist/components/editor.js +3 -3
  181. package/packages/pi-tui/dist/components/editor.js.map +1 -1
  182. package/packages/pi-tui/src/components/editor.ts +3 -3
  183. package/pkg/dist/modes/interactive/theme/themes.js +4 -4
  184. package/pkg/dist/modes/interactive/theme/themes.js.map +1 -1
  185. package/pkg/package.json +1 -1
  186. package/src/resources/extensions/browser-tools/tools/codegen.ts +5 -5
  187. package/src/resources/extensions/browser-tools/tools/navigation.ts +118 -196
  188. package/src/resources/extensions/browser-tools/tools/network-mock.ts +114 -205
  189. package/src/resources/extensions/browser-tools/tools/pages.ts +183 -237
  190. package/src/resources/extensions/browser-tools/tools/refs.ts +193 -507
  191. package/src/resources/extensions/browser-tools/tools/session.ts +182 -321
  192. package/src/resources/extensions/browser-tools/tools/state-persistence.ts +94 -172
  193. package/src/resources/extensions/browser-tools/utils.ts +1 -1
  194. package/src/resources/extensions/cache-timer/index.ts +3 -2
  195. package/src/resources/extensions/slash-commands/extension-manifest.json +2 -2
  196. package/src/resources/extensions/slash-commands/fast.ts +89 -0
  197. package/src/resources/extensions/slash-commands/index.ts +2 -0
  198. package/src/resources/extensions/slash-commands/plan.ts +42 -12
  199. package/src/resources/extensions/subagent/background-job-manager.ts +28 -0
  200. package/src/resources/extensions/subagent/in-process-runner.ts +534 -0
  201. package/src/resources/extensions/subagent/index.ts +489 -799
  202. package/src/resources/extensions/subagent/legacy-runner.ts +607 -0
  203. package/src/resources/extensions/voice/index.ts +308 -238
  204. package/src/resources/extensions/voice/push-to-talk.ts +42 -0
  205. package/src/resources/extensions/voice/tests/push-to-talk.test.ts +109 -0
@@ -1,12 +1,13 @@
1
1
  import type { ExtensionAPI, ExtensionContext } from "@gsd/pi-coding-agent";
2
2
  import { shortcutDesc } from "../shared/mod.js";
3
3
  import type { AssistantMessage } from "@gsd/pi-ai";
4
- import { isKeyRelease, Key, matchesKey, truncateToWidth, visibleWidth } from "@gsd/pi-tui";
4
+ import { isKeyRelease, isKittyProtocolActive, Key, matchesKey, truncateToWidth, visibleWidth } from "@gsd/pi-tui";
5
5
  import { spawn, execFileSync, type ChildProcess } from "node:child_process";
6
6
  import * as fs from "node:fs";
7
7
  import * as path from "node:path";
8
8
  import * as readline from "node:readline";
9
- import { linuxPython, diagnoseSounddeviceError, ensureVoiceVenv, VOICE_VENV_PYTHON } from "./linux-ready.js";
9
+ import { linuxPython, diagnoseSounddeviceError, ensureVoiceVenv } from "./linux-ready.js";
10
+ import { handlePushToTalkInput, type VoiceActivationMode } from "./push-to-talk.js";
10
11
 
11
12
  const __extensionDir = import.meta.dirname!;
12
13
  const SWIFT_SRC = path.join(__extensionDir, "speech-recognizer.swift");
@@ -17,247 +18,316 @@ const IS_DARWIN = process.platform === "darwin";
17
18
  const IS_LINUX = process.platform === "linux";
18
19
 
19
20
  function ensureBinary(): boolean {
20
- if (fs.existsSync(RECOGNIZER_BIN)) return true;
21
- try {
22
- execFileSync("swiftc", [SWIFT_SRC, "-o", RECOGNIZER_BIN, "-framework", "Speech", "-framework", "AVFoundation"], {
23
- timeout: 60000,
24
- });
25
- return true;
26
- } catch {
27
- return false;
28
- }
21
+ if (fs.existsSync(RECOGNIZER_BIN)) return true;
22
+ try {
23
+ execFileSync("swiftc", [SWIFT_SRC, "-o", RECOGNIZER_BIN, "-framework", "Speech", "-framework", "AVFoundation"], {
24
+ timeout: 60000,
25
+ });
26
+ return true;
27
+ } catch {
28
+ return false;
29
+ }
29
30
  }
30
31
 
31
32
  let linuxReady = false;
32
33
 
33
34
  function ensureLinuxReady(ctx: ExtensionContext): boolean {
34
- if (linuxReady) return true;
35
-
36
- // Check GROQ_API_KEY is available
37
- if (!process.env.GROQ_API_KEY) {
38
- ctx.ui.notify("Voice: GROQ_API_KEY not set — run 'gsd config' to configure", "error");
39
- return false;
40
- }
41
-
42
- // Check python3 exists
43
- try {
44
- execFileSync("which", ["python3"], { stdio: "pipe" });
45
- } catch {
46
- ctx.ui.notify("Voice: python3 not found — install with: sudo apt install python3", "error");
47
- return false;
48
- }
49
-
50
- // Check that sounddevice is importable
51
- const py = linuxPython();
52
- try {
53
- execFileSync(py, ["-c", "import sounddevice"], {
54
- stdio: "pipe",
55
- timeout: 10000,
56
- });
57
- } catch (err: unknown) {
58
- const stderr = (err as { stderr?: Buffer })?.stderr?.toString() ?? "";
59
- const diagnosis = diagnoseSounddeviceError(stderr);
60
-
61
- if (diagnosis === "missing-module") {
62
- // Module not installed — auto-create venv (handles PEP 668 systems
63
- // where system pip is blocked). See #2403.
64
- if (!ensureVoiceVenv({ notify: (msg, level) => ctx.ui.notify(msg, level) })) {
65
- return false;
66
- }
67
- linuxReady = true;
68
- return true;
69
- } else if (diagnosis === "missing-portaudio") {
70
- ctx.ui.notify("Voice: install libportaudio2 with: sudo apt install libportaudio2", "error");
71
- } else {
72
- ctx.ui.notify(`Voice: dependency check failed — ${stderr.split("\n")[0] || "unknown error"}`, "error");
73
- }
74
- return false;
75
- }
76
-
77
- linuxReady = true;
78
- return true;
35
+ if (linuxReady) return true;
36
+
37
+ // Check GROQ_API_KEY is available
38
+ if (!process.env.GROQ_API_KEY) {
39
+ ctx.ui.notify("Voice: GROQ_API_KEY not set — run 'gsd config' to configure", "error");
40
+ return false;
41
+ }
42
+
43
+ // Check python3 exists
44
+ try {
45
+ execFileSync("which", ["python3"], { stdio: "pipe" });
46
+ } catch {
47
+ ctx.ui.notify("Voice: python3 not found — install with: sudo apt install python3", "error");
48
+ return false;
49
+ }
50
+
51
+ // Check that sounddevice is importable
52
+ const py = linuxPython();
53
+ try {
54
+ execFileSync(py, ["-c", "import sounddevice"], {
55
+ stdio: "pipe",
56
+ timeout: 10000,
57
+ });
58
+ } catch (err: unknown) {
59
+ const stderr = (err as { stderr?: Buffer })?.stderr?.toString() ?? "";
60
+ const diagnosis = diagnoseSounddeviceError(stderr);
61
+
62
+ if (diagnosis === "missing-module") {
63
+ // Module not installed — auto-create venv (handles PEP 668 systems
64
+ // where system pip is blocked). See #2403.
65
+ if (!ensureVoiceVenv({ notify: (msg, level) => ctx.ui.notify(msg, level) })) {
66
+ return false;
67
+ }
68
+ linuxReady = true;
69
+ return true;
70
+ } else if (diagnosis === "missing-portaudio") {
71
+ ctx.ui.notify("Voice: install libportaudio2 with: sudo apt install libportaudio2", "error");
72
+ } else {
73
+ ctx.ui.notify(`Voice: dependency check failed — ${stderr.split("\n")[0] || "unknown error"}`, "error");
74
+ }
75
+ return false;
76
+ }
77
+
78
+ linuxReady = true;
79
+ return true;
79
80
  }
80
81
 
81
- export default function (pi: ExtensionAPI) {
82
- if (!IS_DARWIN && !IS_LINUX) return;
83
-
84
- let active = false;
85
- let recognizerProcess: ChildProcess | null = null;
86
- let flashOn = true;
87
- let flashTimer: ReturnType<typeof setInterval> | null = null;
88
- let footerTui: { requestRender: () => void } | null = null;
89
-
90
- function setVoiceFooter(ctx: ExtensionContext, on: boolean) {
91
- if (!on) {
92
- stopFlash();
93
- ctx.ui.setFooter(undefined);
94
- return;
95
- }
96
-
97
- flashOn = true;
98
- flashTimer = setInterval(() => {
99
- flashOn = !flashOn;
100
- footerTui?.requestRender();
101
- }, 500);
102
-
103
- ctx.ui.setFooter((tui, theme, footerData) => {
104
- footerTui = tui;
105
- const branchUnsub = footerData.onBranchChange(() => tui.requestRender());
106
-
107
- return {
108
- dispose: branchUnsub,
109
- invalidate() {},
110
- render(width: number): string[] {
111
- // Row 1: pwd (branch) ... ● transcribing
112
- let pwd = process.cwd();
113
- const home = process.env.HOME || process.env.USERPROFILE;
114
- if (home && pwd.startsWith(home)) pwd = `~${pwd.slice(home.length)}`;
115
- const branch = footerData.getGitBranch();
116
- if (branch) pwd = `${pwd} (${branch})`;
117
-
118
- const dot = flashOn ? theme.fg("error", "●") : theme.fg("dim", "●");
119
- const voiceTag = `${dot} ${theme.fg("error", "transcribing")}`;
120
- const voiceTagWidth = visibleWidth(voiceTag);
121
-
122
- const maxPwdWidth = width - voiceTagWidth - 2;
123
- const pwdStr = truncateToWidth(theme.fg("dim", pwd), maxPwdWidth, theme.fg("dim", "..."));
124
- const pad1 = " ".repeat(Math.max(1, width - visibleWidth(pwdStr) - voiceTagWidth));
125
- const row1 = truncateToWidth(pwdStr + pad1 + voiceTag, width);
126
-
127
- // Row 2: stats ... model
128
- let totalInput = 0, totalOutput = 0, totalCost = 0;
129
- for (const entry of ctx.sessionManager.getEntries()) {
130
- if (entry.type === "message" && entry.message.role === "assistant") {
131
- const m = entry.message as AssistantMessage;
132
- totalInput += m.usage.input;
133
- totalOutput += m.usage.output;
134
- totalCost += m.usage.cost.total;
135
- }
136
- }
137
-
138
- const fmt = (n: number) => n < 1000 ? `${n}` : n < 10000 ? `${(n / 1000).toFixed(1)}k` : `${Math.round(n / 1000)}k`;
139
- const parts: string[] = [];
140
- if (totalInput) parts.push(`↑${fmt(totalInput)}`);
141
- if (totalOutput) parts.push(`↓${fmt(totalOutput)}`);
142
- if (totalCost) parts.push(`$${totalCost.toFixed(3)}`);
143
-
144
- const usage = ctx.getContextUsage();
145
- const ctxPct = usage?.percent !== null && usage?.percent !== undefined ? `${usage.percent.toFixed(1)}%` : "?";
146
- const ctxWin = usage?.contextWindow ?? ctx.model?.contextWindow ?? 0;
147
- parts.push(`${ctxPct}/${fmt(ctxWin)}`);
148
-
149
- const statsLeft = theme.fg("dim", parts.join(" "));
150
- const modelRight = theme.fg("dim", ctx.model?.id || "no-model");
151
- const statsLeftW = visibleWidth(statsLeft);
152
- const modelRightW = visibleWidth(modelRight);
153
- const pad2 = " ".repeat(Math.max(2, width - statsLeftW - modelRightW));
154
- const row2 = truncateToWidth(statsLeft + pad2 + modelRight, width);
155
-
156
- return [row1, row2];
157
- },
158
- };
159
- });
160
- }
161
-
162
- function stopFlash() {
163
- if (flashTimer) { clearInterval(flashTimer); flashTimer = null; }
164
- footerTui = null;
165
- }
166
-
167
- async function toggleVoice(ctx: ExtensionContext) {
168
- if (active) {
169
- killRecognizer();
170
- active = false;
171
- setVoiceFooter(ctx, false);
172
- return;
173
- }
174
-
175
- if (IS_DARWIN) {
176
- if (!ensureBinary()) {
177
- ctx.ui.notify("Voice: failed to compile speech recognizer (need Xcode CLI tools)", "error");
178
- return;
179
- }
180
- } else if (IS_LINUX) {
181
- if (!ensureLinuxReady(ctx)) {
182
- return;
183
- }
184
- }
185
-
186
- active = true;
187
- setVoiceFooter(ctx, true);
188
- await runVoiceSession(ctx);
189
- }
190
-
191
- pi.registerCommand("voice", {
192
- description: "Toggle voice mode",
193
- handler: async (_args, ctx) => toggleVoice(ctx),
194
- });
195
-
196
- pi.registerShortcut("ctrl+alt+v", {
197
- description: shortcutDesc("Toggle voice mode", "/voice"),
198
- handler: async (ctx) => toggleVoice(ctx),
199
- });
200
-
201
- function killRecognizer() {
202
- if (recognizerProcess) { recognizerProcess.kill("SIGTERM"); recognizerProcess = null; }
203
- }
204
-
205
- function startRecognizer(
206
- onPartial: (text: string) => void,
207
- onFinal: (text: string) => void,
208
- onError: (msg: string) => void,
209
- onReady: () => void,
210
- ) {
211
- if (IS_LINUX) {
212
- recognizerProcess = spawn(linuxPython(), [PYTHON_SCRIPT], {
213
- stdio: ["pipe", "pipe", "pipe"],
214
- });
215
- } else {
216
- recognizerProcess = spawn(RECOGNIZER_BIN, [], { stdio: ["pipe", "pipe", "pipe"] });
217
- }
218
- const rl = readline.createInterface({ input: recognizerProcess.stdout! });
219
- rl.on("line", (line: string) => {
220
- if (line === "READY") { onReady(); return; }
221
- if (line.startsWith("PARTIAL:")) onPartial(line.slice(8));
222
- else if (line.startsWith("FINAL:")) onFinal(line.slice(6));
223
- else if (line.startsWith("ERROR:")) onError(line.slice(6));
224
- });
225
- recognizerProcess.on("error", (err) => onError(err.message));
226
- recognizerProcess.on("exit", () => { recognizerProcess = null; });
227
- }
228
-
229
- async function runVoiceSession(ctx: ExtensionContext): Promise<void> {
230
- return new Promise<void>((resolve) => {
231
- // The Swift recognizer handles accumulation across pause-induced
232
- // transcription resets. Both PARTIAL and FINAL messages contain
233
- // the full accumulated text, so we just pass them through.
234
- startRecognizer(
235
- (text) => {
236
- ctx.ui.setEditorText(text);
237
- },
238
- (text) => {
239
- ctx.ui.setEditorText(text);
240
- },
241
- (msg) => ctx.ui.notify(`Voice: ${msg}`, "error"),
242
- () => {},
243
- );
244
-
245
- ctx.ui.custom<void>(
246
- (_tui, _theme, _kb, done) => ({
247
- render(): string[] { return []; },
248
- handleInput(data: string) {
249
- if (isKeyRelease(data)) return;
250
- if (matchesKey(data, Key.escape) || matchesKey(data, Key.enter)) {
251
- killRecognizer();
252
- active = false;
253
- setVoiceFooter(ctx, false);
254
- done();
255
- }
256
- },
257
- invalidate() {},
258
- }),
259
- { overlay: true, overlayOptions: { anchor: "bottom-center", width: "100%" } },
260
- ).then(() => resolve());
261
- });
262
- }
82
+ export default function(pi: ExtensionAPI) {
83
+ if (!IS_DARWIN && !IS_LINUX) return;
84
+
85
+ let active = false;
86
+ let activationMode: VoiceActivationMode | null = null;
87
+ let recognizerProcess: ChildProcess | null = null;
88
+ let flashOn = true;
89
+ let flashTimer: ReturnType<typeof setInterval> | null = null;
90
+ let footerTui: { requestRender: () => void } | null = null;
91
+ let closeVoiceOverlay: (() => void) | null = null;
92
+ let voiceSessionPromise: Promise<void> | null = null;
93
+ let holdToTalkUnsupportedNotified = false;
94
+
95
+ function setVoiceFooter(ctx: ExtensionContext, on: boolean) {
96
+ if (!on) {
97
+ stopFlash();
98
+ ctx.ui.setFooter(undefined);
99
+ return;
100
+ }
101
+
102
+ flashOn = true;
103
+ flashTimer = setInterval(() => {
104
+ flashOn = !flashOn;
105
+ footerTui?.requestRender();
106
+ }, 500);
107
+
108
+ ctx.ui.setFooter((tui, theme, footerData) => {
109
+ footerTui = tui;
110
+ const branchUnsub = footerData.onBranchChange(() => tui.requestRender());
111
+
112
+ return {
113
+ dispose: branchUnsub,
114
+ invalidate() { },
115
+ render(width: number): string[] {
116
+ // Row 1: pwd (branch) ... ● transcribing
117
+ let pwd = process.cwd();
118
+ const home = process.env.HOME || process.env.USERPROFILE;
119
+ if (home && pwd.startsWith(home)) pwd = `~${pwd.slice(home.length)}`;
120
+ const branch = footerData.getGitBranch();
121
+ if (branch) pwd = `${pwd} (${branch})`;
122
+
123
+ const dot = flashOn ? theme.fg("error", "●") : theme.fg("dim", "●");
124
+ const voiceTag = `${dot} ${theme.fg("error", "transcribing")}`;
125
+ const voiceTagWidth = visibleWidth(voiceTag);
126
+
127
+ const maxPwdWidth = width - voiceTagWidth - 2;
128
+ const pwdStr = truncateToWidth(theme.fg("dim", pwd), maxPwdWidth, theme.fg("dim", "..."));
129
+ const pad1 = " ".repeat(Math.max(1, width - visibleWidth(pwdStr) - voiceTagWidth));
130
+ const row1 = truncateToWidth(pwdStr + pad1 + voiceTag, width);
131
+
132
+ // Row 2: stats ... model
133
+ let totalInput = 0, totalOutput = 0, totalCost = 0;
134
+ for (const entry of ctx.sessionManager.getEntries()) {
135
+ if (entry.type === "message" && entry.message.role === "assistant") {
136
+ const m = entry.message as AssistantMessage;
137
+ totalInput += m.usage.input;
138
+ totalOutput += m.usage.output;
139
+ totalCost += m.usage.cost.total;
140
+ }
141
+ }
142
+
143
+ const fmt = (n: number) => n < 1000 ? `${n}` : n < 10000 ? `${(n / 1000).toFixed(1)}k` : `${Math.round(n / 1000)}k`;
144
+ const parts: string[] = [];
145
+ if (totalInput) parts.push(`↑${fmt(totalInput)}`);
146
+ if (totalOutput) parts.push(`↓${fmt(totalOutput)}`);
147
+ if (totalCost) parts.push(`$${totalCost.toFixed(3)}`);
148
+
149
+ const usage = ctx.getContextUsage();
150
+ const ctxPct = usage?.percent !== null && usage?.percent !== undefined ? `${usage.percent.toFixed(1)}%` : "?";
151
+ const ctxWin = usage?.contextWindow ?? ctx.model?.contextWindow ?? 0;
152
+ parts.push(`${ctxPct}/${fmt(ctxWin)}`);
153
+
154
+ const statsLeft = theme.fg("dim", parts.join(" "));
155
+ const modelRight = theme.fg("dim", ctx.model?.id || "no-model");
156
+ const statsLeftW = visibleWidth(statsLeft);
157
+ const modelRightW = visibleWidth(modelRight);
158
+ const pad2 = " ".repeat(Math.max(2, width - statsLeftW - modelRightW));
159
+ const row2 = truncateToWidth(statsLeft + pad2 + modelRight, width);
160
+
161
+ return [row1, row2];
162
+ },
163
+ };
164
+ });
165
+ }
166
+
167
+ function stopFlash() {
168
+ if (flashTimer) {
169
+ clearInterval(flashTimer);
170
+ flashTimer = null;
171
+ }
172
+ footerTui = null;
173
+ }
174
+
175
+ function killRecognizer() {
176
+ if (recognizerProcess) {
177
+ recognizerProcess.kill("SIGTERM");
178
+ recognizerProcess = null;
179
+ }
180
+ }
181
+
182
+ function prepareVoice(ctx: ExtensionContext): boolean {
183
+ if (IS_DARWIN) {
184
+ if (!ensureBinary()) {
185
+ ctx.ui.notify("Voice: failed to compile speech recognizer (need Xcode CLI tools)", "error");
186
+ return false;
187
+ }
188
+ } else if (IS_LINUX) {
189
+ if (!ensureLinuxReady(ctx)) {
190
+ return false;
191
+ }
192
+ }
193
+ return true;
194
+ }
195
+
196
+ async function startVoice(ctx: ExtensionContext, mode: VoiceActivationMode): Promise<boolean> {
197
+ if (active) return false;
198
+ if (!prepareVoice(ctx)) return false;
199
+
200
+ active = true;
201
+ activationMode = mode;
202
+ setVoiceFooter(ctx, true);
203
+ voiceSessionPromise = runVoiceSession(ctx).finally(() => {
204
+ if (voiceSessionPromise) {
205
+ voiceSessionPromise = null;
206
+ }
207
+ });
208
+ return true;
209
+ }
210
+
211
+ async function stopVoice(ctx: ExtensionContext): Promise<void> {
212
+ if (!active && !closeVoiceOverlay && !recognizerProcess) return;
213
+
214
+ killRecognizer();
215
+ active = false;
216
+ activationMode = null;
217
+ setVoiceFooter(ctx, false);
218
+
219
+ const close = closeVoiceOverlay;
220
+ closeVoiceOverlay = null;
221
+ close?.();
222
+
223
+ await voiceSessionPromise;
224
+ }
225
+
226
+ async function toggleVoice(ctx: ExtensionContext) {
227
+ if (active) {
228
+ await stopVoice(ctx);
229
+ return;
230
+ }
231
+
232
+ await startVoice(ctx, "toggle");
233
+ }
234
+
235
+ function startRecognizer(
236
+ onPartial: (text: string) => void,
237
+ onFinal: (text: string) => void,
238
+ onError: (msg: string) => void,
239
+ onReady: () => void,
240
+ ) {
241
+ if (IS_LINUX) {
242
+ recognizerProcess = spawn(linuxPython(), [PYTHON_SCRIPT], {
243
+ stdio: ["pipe", "pipe", "pipe"],
244
+ });
245
+ } else {
246
+ recognizerProcess = spawn(RECOGNIZER_BIN, [], { stdio: ["pipe", "pipe", "pipe"] });
247
+ }
248
+ const rl = readline.createInterface({ input: recognizerProcess.stdout! });
249
+ rl.on("line", (line: string) => {
250
+ if (line === "READY") { onReady(); return; }
251
+ if (line.startsWith("PARTIAL:")) onPartial(line.slice(8));
252
+ else if (line.startsWith("FINAL:")) onFinal(line.slice(6));
253
+ else if (line.startsWith("ERROR:")) onError(line.slice(6));
254
+ });
255
+ recognizerProcess.on("error", (err) => onError(err.message));
256
+ recognizerProcess.on("exit", () => { recognizerProcess = null; });
257
+ }
258
+
259
+ async function runVoiceSession(ctx: ExtensionContext): Promise<void> {
260
+ return new Promise<void>((resolve) => {
261
+ // The Swift recognizer handles accumulation across pause-induced
262
+ // transcription resets. Both PARTIAL and FINAL messages contain
263
+ // the full accumulated text, so we just pass them through.
264
+ startRecognizer(
265
+ (text) => {
266
+ ctx.ui.setEditorText(text);
267
+ },
268
+ (text) => {
269
+ ctx.ui.setEditorText(text);
270
+ },
271
+ (msg) => ctx.ui.notify(`Voice: ${msg}`, "error"),
272
+ () => { },
273
+ );
274
+
275
+ ctx.ui.custom<void>(
276
+ (_tui, _theme, _kb, done) => {
277
+ const close = () => done();
278
+ closeVoiceOverlay = close;
279
+ return {
280
+ render(): string[] { return []; },
281
+ handleInput(data: string) {
282
+ if (isKeyRelease(data)) return;
283
+ if (matchesKey(data, Key.escape) || matchesKey(data, Key.enter)) {
284
+ void stopVoice(ctx);
285
+ }
286
+ },
287
+ invalidate() { },
288
+ dispose() {
289
+ if (closeVoiceOverlay === close) {
290
+ closeVoiceOverlay = null;
291
+ }
292
+ },
293
+ };
294
+ },
295
+ { overlay: true, overlayOptions: { anchor: "bottom-center", width: "100%" } },
296
+ ).then(() => resolve());
297
+ });
298
+ }
299
+
300
+ pi.on("session_start", async (_event, ctx) => {
301
+ ctx.ui.onTerminalInput((data) => handlePushToTalkInput(data, {
302
+ active,
303
+ activationMode,
304
+ editorText: ctx.ui.getEditorText(),
305
+ holdToTalkSupported: isKittyProtocolActive(),
306
+ onUnsupported: () => {
307
+ if (holdToTalkUnsupportedNotified) return;
308
+ holdToTalkUnsupportedNotified = true;
309
+ ctx.ui.notify("Voice: hold Space requires Kitty key-release support in this terminal — use /voice or Ctrl+Alt+V", "warning");
310
+ },
311
+ startPushToTalk: async () => {
312
+ await startVoice(ctx, "push-to-talk");
313
+ },
314
+ stopVoice: async () => {
315
+ await stopVoice(ctx);
316
+ },
317
+ }));
318
+ });
319
+
320
+ pi.on("session_shutdown", async (_event, ctx) => {
321
+ await stopVoice(ctx);
322
+ });
323
+
324
+ pi.registerCommand("voice", {
325
+ description: "Toggle voice mode",
326
+ handler: async (_args, ctx) => toggleVoice(ctx),
327
+ });
328
+
329
+ pi.registerShortcut("ctrl+alt+v", {
330
+ description: shortcutDesc("Toggle voice mode", "/voice"),
331
+ handler: async (ctx) => toggleVoice(ctx),
332
+ });
263
333
  }
@@ -0,0 +1,42 @@
1
+ import { isKeyRelease, Key, matchesKey } from "@gsd/pi-tui";
2
+ import type { TerminalInputHandler } from "@gsd/pi-coding-agent";
3
+
4
+ export type VoiceActivationMode = "toggle" | "push-to-talk";
5
+
6
+ export interface PushToTalkState {
7
+ active: boolean;
8
+ activationMode: VoiceActivationMode | null;
9
+ editorText: string;
10
+ holdToTalkSupported: boolean;
11
+ onUnsupported?(): void;
12
+ startPushToTalk(): void | Promise<void>;
13
+ stopVoice(): void | Promise<void>;
14
+ }
15
+
16
+ export function handlePushToTalkInput(data: string, state: PushToTalkState): ReturnType<TerminalInputHandler> {
17
+ if (!matchesKey(data, Key.space)) return undefined;
18
+
19
+ if (isKeyRelease(data)) {
20
+ if (state.activationMode === "push-to-talk") {
21
+ void Promise.resolve(state.stopVoice());
22
+ return { consume: true };
23
+ }
24
+ return undefined;
25
+ }
26
+
27
+ if (state.activationMode === "push-to-talk") {
28
+ // Consume repeat events while the key is held so we do not leak spaces.
29
+ return { consume: true };
30
+ }
31
+
32
+ if (state.active) return undefined;
33
+ if (state.editorText.length > 0) return undefined;
34
+
35
+ if (!state.holdToTalkSupported) {
36
+ state.onUnsupported?.();
37
+ return { consume: true };
38
+ }
39
+
40
+ void Promise.resolve(state.startPushToTalk());
41
+ return { consume: true };
42
+ }