@kolisachint/hoocode-agent 0.4.102 → 0.4.104

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.
@@ -60,6 +60,7 @@ import { TreeSelectorComponent } from "./components/tree-selector.js";
60
60
  import { UserMessageComponent } from "./components/user-message.js";
61
61
  import { UserMessageSelectorComponent } from "./components/user-message-selector.js";
62
62
  import { getAvailableThemes, getAvailableThemesWithPaths, getEditorTheme, getMarkdownTheme, getThemeByName, initTheme, onThemeChange, setRegisteredThemes, setTheme, setThemeInstance, stopThemeWatcher, Theme, theme, } from "./theme/theme.js";
63
+ import { startVoiceTranscribe } from "./voice-transcribe.js";
63
64
  function isExpandable(obj) {
64
65
  return typeof obj === "object" && obj !== null && "setExpanded" in obj && typeof obj.setExpanded === "function";
65
66
  }
@@ -114,6 +115,9 @@ export class InteractiveMode {
114
115
  defaultEditor;
115
116
  editor;
116
117
  editorComponentFactory;
118
+ voiceSession;
119
+ /** True while the voicetools binary is being resolved/downloaded before a session starts. */
120
+ voiceStarting = false;
117
121
  autocompleteProvider;
118
122
  autocompleteProviderWrappers = [];
119
123
  fdPath;
@@ -2081,6 +2085,7 @@ export class InteractiveMode {
2081
2085
  });
2082
2086
  this.defaultEditor.onAction("app.team.focus", () => this.enterTeamFocus());
2083
2087
  this.defaultEditor.onAction("app.editor.external", () => this.openExternalEditor());
2088
+ this.defaultEditor.onAction("app.input.voiceTranscribe", () => this.toggleVoiceTranscribe());
2084
2089
  this.defaultEditor.onAction("app.message.followUp", () => this.handleFollowUp());
2085
2090
  this.defaultEditor.onAction("app.message.dequeue", () => this.handleDequeue());
2086
2091
  this.defaultEditor.onAction("app.session.new", () => this.commandExecutor.handleClear());
@@ -2756,6 +2761,74 @@ export class InteractiveMode {
2756
2761
  * If multiple status messages are emitted back-to-back (without anything else being added to the chat),
2757
2762
  * we update the previous status line instead of appending new ones to avoid log spam.
2758
2763
  */
2764
+ /**
2765
+ * Toggle voice-to-text capture. First press spawns `voicetools`, streams
2766
+ * decoded segments into the editor via bracketed paste, and reports state
2767
+ * through the status line. Pressing the shortcut again stops early.
2768
+ */
2769
+ toggleVoiceTranscribe() {
2770
+ if (this.voiceSession?.running || this.voiceStarting) {
2771
+ this.voiceSession?.stop();
2772
+ this.voiceSession = undefined;
2773
+ this.voiceStarting = false;
2774
+ this.showStatus("Voice input cancelled");
2775
+ return;
2776
+ }
2777
+ const statusLabels = {
2778
+ recording: "Recording... speak now (press again to cancel)",
2779
+ transcribing: "Transcribing...",
2780
+ done: "Voice input done",
2781
+ };
2782
+ // Resolve the binary the same way as the other managed tools (webtools etc.):
2783
+ // an explicit VOICETOOLS_BIN wins, otherwise resolve from bin/PATH and download
2784
+ // from the published release on demand. This runs async, so guard re-entry with
2785
+ // `voiceStarting` and let a second press before it resolves cancel the start.
2786
+ this.voiceStarting = true;
2787
+ this.showStatus("Preparing voice input...");
2788
+ void this.resolveVoiceBin()
2789
+ .then((bin) => {
2790
+ // A second key press while resolving cleared the flag: honour the cancel.
2791
+ if (!this.voiceStarting)
2792
+ return;
2793
+ this.voiceStarting = false;
2794
+ if (!bin) {
2795
+ this.showError("Voice input failed: voicetools binary unavailable and could not be downloaded. " +
2796
+ "Install it, set VOICETOOLS_BIN, or ensure a published release exists for this platform.");
2797
+ return;
2798
+ }
2799
+ this.voiceSession = startVoiceTranscribe(bin, {
2800
+ onStatus: (status) => {
2801
+ this.showStatus(statusLabels[status] ?? status);
2802
+ if (status === "done") {
2803
+ this.voiceSession = undefined;
2804
+ }
2805
+ },
2806
+ onSegment: (text) => {
2807
+ // Inject via bracketed paste so the editor treats it as pasted text.
2808
+ this.editor.handleInput(`\x1b[200~${text} \x1b[201~`);
2809
+ },
2810
+ onError: (message) => {
2811
+ this.voiceSession = undefined;
2812
+ this.showError(`Voice input failed: ${message}`);
2813
+ },
2814
+ });
2815
+ })
2816
+ .catch((err) => {
2817
+ this.voiceStarting = false;
2818
+ this.showError(`Voice input failed: ${err instanceof Error ? err.message : String(err)}`);
2819
+ });
2820
+ }
2821
+ /**
2822
+ * Resolve the `voicetools` binary path. Prefers an explicit VOICETOOLS_BIN
2823
+ * override, otherwise resolves via the managed tools manager (bin dir / PATH /
2824
+ * download from the published release). Returns undefined when unavailable.
2825
+ */
2826
+ async resolveVoiceBin() {
2827
+ const override = process.env.VOICETOOLS_BIN?.trim();
2828
+ if (override)
2829
+ return override;
2830
+ return ensureTool("voicetools", true);
2831
+ }
2759
2832
  showStatus(message) {
2760
2833
  const children = this.chatContainer.children;
2761
2834
  const last = children.length > 0 ? children[children.length - 1] : undefined;
@@ -4430,6 +4503,11 @@ export class InteractiveMode {
4430
4503
  }
4431
4504
  stop() {
4432
4505
  this.unregisterSignalHandlers();
4506
+ if (this.voiceSession?.running) {
4507
+ this.voiceSession.stop();
4508
+ this.voiceSession = undefined;
4509
+ }
4510
+ this.voiceStarting = false;
4433
4511
  if (this.settingsManager.getShowTerminalProgress()) {
4434
4512
  this.ui.terminal.setProgress(false);
4435
4513
  }