@kolisachint/hoocode-agent 0.4.104 → 0.4.105
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.
- package/CHANGELOG.md +18 -0
- package/dist/modes/interactive/components/voice-panel.d.ts +56 -0
- package/dist/modes/interactive/components/voice-panel.d.ts.map +1 -0
- package/dist/modes/interactive/components/voice-panel.js +195 -0
- package/dist/modes/interactive/components/voice-panel.js.map +1 -0
- package/dist/modes/interactive/interactive-mode.d.ts +39 -3
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +247 -40
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/interactive/voice-transcribe.d.ts +68 -1
- package/dist/modes/interactive/voice-transcribe.d.ts.map +1 -1
- package/dist/modes/interactive/voice-transcribe.js +154 -0
- package/dist/modes/interactive/voice-transcribe.js.map +1 -1
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/with-deps/package.json +1 -1
- package/package.json +4 -4
|
@@ -59,8 +59,9 @@ import { ToolExecutionComponent } from "./components/tool-execution.js";
|
|
|
59
59
|
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
|
+
import { VoicePanel } from "./components/voice-panel.js";
|
|
62
63
|
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";
|
|
64
|
+
import { startVoiceTranscribe, VoiceDaemon } from "./voice-transcribe.js";
|
|
64
65
|
function isExpandable(obj) {
|
|
65
66
|
return typeof obj === "object" && obj !== null && "setExpanded" in obj && typeof obj.setExpanded === "function";
|
|
66
67
|
}
|
|
@@ -84,6 +85,12 @@ function isDeadTerminalError(error) {
|
|
|
84
85
|
const code = error.code;
|
|
85
86
|
return code !== undefined && DEAD_TERMINAL_ERROR_CODES.has(code);
|
|
86
87
|
}
|
|
88
|
+
// Mirrors voicetools' own default trailing-silence window (see `--silence-ms` /
|
|
89
|
+
// VOICE_SILENCE_MS upstream). Only used to drive the cosmetic countdown in the
|
|
90
|
+
// panel; the actual cutoff decision is made by the binary, not this timer.
|
|
91
|
+
const VOICE_SILENCE_MS = 600;
|
|
92
|
+
const VOICE_UNAVAILABLE_MESSAGE = "Voice input failed: voicetools binary unavailable and could not be downloaded. " +
|
|
93
|
+
"Install it, set VOICETOOLS_BIN, or ensure a published release exists for this platform.";
|
|
87
94
|
const ANTHROPIC_SUBSCRIPTION_AUTH_WARNING = "Anthropic subscription auth: billed per token as extra usage, not plan limits.";
|
|
88
95
|
function isAnthropicSubscriptionAuthKey(apiKey) {
|
|
89
96
|
return typeof apiKey === "string" && apiKey.startsWith("sk-ant-oat");
|
|
@@ -116,8 +123,18 @@ export class InteractiveMode {
|
|
|
116
123
|
editor;
|
|
117
124
|
editorComponentFactory;
|
|
118
125
|
voiceSession;
|
|
126
|
+
// Persistent `voicetools serve` process, once probed successfully. Stays alive
|
|
127
|
+
// (and warm) across captures for the life of the interactive session.
|
|
128
|
+
voiceDaemon;
|
|
129
|
+
// Sticky for the session: set once `serve` is confirmed unsupported (old binary)
|
|
130
|
+
// so later presses skip straight to the per-press `transcribe` fallback.
|
|
131
|
+
voiceDaemonUnsupported = false;
|
|
119
132
|
/** True while the voicetools binary is being resolved/downloaded before a session starts. */
|
|
120
133
|
voiceStarting = false;
|
|
134
|
+
/** True while a capture (daemon or legacy) is in flight. */
|
|
135
|
+
voiceActive = false;
|
|
136
|
+
/** The live multi-line status panel for the current capture (undefined when idle). */
|
|
137
|
+
voicePanel;
|
|
121
138
|
autocompleteProvider;
|
|
122
139
|
autocompleteProviderWrappers = [];
|
|
123
140
|
fdPath;
|
|
@@ -2762,64 +2779,225 @@ export class InteractiveMode {
|
|
|
2762
2779
|
* we update the previous status line instead of appending new ones to avoid log spam.
|
|
2763
2780
|
*/
|
|
2764
2781
|
/**
|
|
2765
|
-
* Toggle voice-to-text capture.
|
|
2766
|
-
*
|
|
2767
|
-
*
|
|
2782
|
+
* Toggle voice-to-text capture.
|
|
2783
|
+
*
|
|
2784
|
+
* Prefers a persistent `voicetools serve` daemon: the first press probes for
|
|
2785
|
+
* support and (if found) loads models once, showing a "warming up" spinner;
|
|
2786
|
+
* every later press reuses the already-warm daemon and jumps straight to
|
|
2787
|
+
* Listening. Binaries without `serve` support fall back to spawning
|
|
2788
|
+
* `voicetools transcribe` per press, same as before. Pressing the shortcut
|
|
2789
|
+
* again while listening (or while still warming up) cancels.
|
|
2768
2790
|
*/
|
|
2769
2791
|
toggleVoiceTranscribe() {
|
|
2770
|
-
if (this.
|
|
2771
|
-
this.
|
|
2792
|
+
if (this.voiceActive) {
|
|
2793
|
+
if (this.voiceDaemon?.isReady) {
|
|
2794
|
+
this.voiceDaemon.cancel();
|
|
2795
|
+
}
|
|
2796
|
+
else {
|
|
2797
|
+
this.voiceSession?.stop();
|
|
2798
|
+
}
|
|
2772
2799
|
this.voiceSession = undefined;
|
|
2800
|
+
this.voiceActive = false;
|
|
2801
|
+
this.resetVoiceUI();
|
|
2802
|
+
return;
|
|
2803
|
+
}
|
|
2804
|
+
if (this.voiceStarting) {
|
|
2805
|
+
// A second press while resolving/warming up: honour the cancel. Any
|
|
2806
|
+
// daemon that finishes loading afterwards is kept warm for next time.
|
|
2773
2807
|
this.voiceStarting = false;
|
|
2774
|
-
this.
|
|
2808
|
+
this.resetVoiceUI();
|
|
2775
2809
|
return;
|
|
2776
2810
|
}
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2811
|
+
if (this.voiceDaemonUnsupported) {
|
|
2812
|
+
this.voiceStarting = true;
|
|
2813
|
+
this.showVoiceWarming("Starting voice input...");
|
|
2814
|
+
void this.resolveVoiceBin()
|
|
2815
|
+
.then((bin) => {
|
|
2816
|
+
if (!this.voiceStarting)
|
|
2817
|
+
return;
|
|
2818
|
+
this.voiceStarting = false;
|
|
2819
|
+
if (!bin) {
|
|
2820
|
+
this.resetVoiceUI();
|
|
2821
|
+
this.showError(VOICE_UNAVAILABLE_MESSAGE);
|
|
2822
|
+
return;
|
|
2823
|
+
}
|
|
2824
|
+
this.beginLegacyVoiceCapture(bin);
|
|
2825
|
+
})
|
|
2826
|
+
.catch((err) => {
|
|
2827
|
+
this.voiceStarting = false;
|
|
2828
|
+
this.resetVoiceUI();
|
|
2829
|
+
this.showError(`Voice input failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
2830
|
+
});
|
|
2831
|
+
return;
|
|
2832
|
+
}
|
|
2833
|
+
if (this.voiceDaemon?.isReady) {
|
|
2834
|
+
this.beginDaemonVoiceCapture();
|
|
2835
|
+
return;
|
|
2836
|
+
}
|
|
2837
|
+
// No daemon yet: resolve the binary, then probe for `serve` support by
|
|
2838
|
+
// spawning it. `VoiceDaemon.spawn` doubles as the probe: it resolves to a
|
|
2839
|
+
// live daemon once READY arrives, to "unsupported" if the process exits
|
|
2840
|
+
// with no output at all (an old binary rejecting the unrecognized `serve`
|
|
2841
|
+
// subcommand), or to "error" if it printed a real ERROR first (e.g. no
|
|
2842
|
+
// model installed yet) — already surfaced via onError, so that case skips
|
|
2843
|
+
// the legacy fallback (it would just hit the same error) but leaves
|
|
2844
|
+
// daemon mode available to retry on the next press.
|
|
2786
2845
|
this.voiceStarting = true;
|
|
2787
|
-
this.
|
|
2846
|
+
this.showVoiceWarming("Warming up voice input...");
|
|
2788
2847
|
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;
|
|
2848
|
+
.then(async (bin) => {
|
|
2794
2849
|
if (!bin) {
|
|
2795
|
-
this.
|
|
2796
|
-
|
|
2850
|
+
this.voiceStarting = false;
|
|
2851
|
+
this.resetVoiceUI();
|
|
2852
|
+
this.showError(VOICE_UNAVAILABLE_MESSAGE);
|
|
2797
2853
|
return;
|
|
2798
2854
|
}
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2855
|
+
const result = await VoiceDaemon.spawn(bin, this.buildVoiceDaemonHandlers());
|
|
2856
|
+
if (!result.ok) {
|
|
2857
|
+
if (result.reason === "unsupported") {
|
|
2858
|
+
this.voiceDaemonUnsupported = true;
|
|
2859
|
+
if (!this.voiceStarting) {
|
|
2860
|
+
this.resetVoiceUI();
|
|
2861
|
+
return;
|
|
2804
2862
|
}
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2863
|
+
this.voiceStarting = false;
|
|
2864
|
+
this.beginLegacyVoiceCapture(bin);
|
|
2865
|
+
return;
|
|
2866
|
+
}
|
|
2867
|
+
this.voiceStarting = false;
|
|
2868
|
+
this.resetVoiceUI();
|
|
2869
|
+
return;
|
|
2870
|
+
}
|
|
2871
|
+
this.voiceDaemon = result.daemon;
|
|
2872
|
+
if (!this.voiceStarting) {
|
|
2873
|
+
// Cancelled while warming up: keep the loaded daemon warm for next time.
|
|
2874
|
+
this.resetVoiceUI();
|
|
2875
|
+
return;
|
|
2876
|
+
}
|
|
2877
|
+
this.voiceStarting = false;
|
|
2878
|
+
this.beginDaemonVoiceCapture();
|
|
2815
2879
|
})
|
|
2816
2880
|
.catch((err) => {
|
|
2817
2881
|
this.voiceStarting = false;
|
|
2882
|
+
this.resetVoiceUI();
|
|
2818
2883
|
this.showError(`Voice input failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
2819
2884
|
});
|
|
2820
2885
|
}
|
|
2821
2886
|
/**
|
|
2822
|
-
*
|
|
2887
|
+
* Build the (stable, reused-across-captures) handlers for the daemon.
|
|
2888
|
+
*
|
|
2889
|
+
* Every handler bails out once `voiceActive` is false: CANCEL is a soft
|
|
2890
|
+
* request (unlike the legacy path's `proc.kill()`, it doesn't sever the
|
|
2891
|
+
* pipe), so the daemon can still have a trailing PARTIAL/FINAL/DONE for the
|
|
2892
|
+
* just-cancelled capture in flight when the user presses cancel. Without
|
|
2893
|
+
* this guard that stale text would land in the editor after the panel had
|
|
2894
|
+
* already collapsed.
|
|
2895
|
+
*
|
|
2896
|
+
* v0.1.4 serve streams `PARTIAL <full growing hypothesis>` (live preview,
|
|
2897
|
+
* never committed) and ends with a single `FINAL <complete text>` (the one
|
|
2898
|
+
* commit to the editor). `SEGMENT` is still handled for the legacy
|
|
2899
|
+
* transcribe path and any binary that streams committed chunks directly.
|
|
2900
|
+
*/
|
|
2901
|
+
buildVoiceDaemonHandlers() {
|
|
2902
|
+
return {
|
|
2903
|
+
onSegment: (text) => {
|
|
2904
|
+
if (!this.voiceActive)
|
|
2905
|
+
return;
|
|
2906
|
+
this.commitVoiceText(text);
|
|
2907
|
+
},
|
|
2908
|
+
onPartial: (text) => {
|
|
2909
|
+
if (!this.voiceActive)
|
|
2910
|
+
return;
|
|
2911
|
+
this.voicePanel?.setPartial(text);
|
|
2912
|
+
},
|
|
2913
|
+
onFinal: (text) => {
|
|
2914
|
+
if (!this.voiceActive)
|
|
2915
|
+
return;
|
|
2916
|
+
this.commitVoiceText(text);
|
|
2917
|
+
},
|
|
2918
|
+
onStatus: (status) => {
|
|
2919
|
+
if (!this.voiceActive)
|
|
2920
|
+
return;
|
|
2921
|
+
if (status === "done") {
|
|
2922
|
+
this.voiceActive = false;
|
|
2923
|
+
this.resetVoiceUI();
|
|
2924
|
+
return;
|
|
2925
|
+
}
|
|
2926
|
+
if (status === "transcribing") {
|
|
2927
|
+
this.voicePanel?.setTranscribing();
|
|
2928
|
+
}
|
|
2929
|
+
else if (status === "listening") {
|
|
2930
|
+
this.voicePanel?.startListening();
|
|
2931
|
+
}
|
|
2932
|
+
},
|
|
2933
|
+
onLevel: (rms) => {
|
|
2934
|
+
if (!this.voiceActive)
|
|
2935
|
+
return;
|
|
2936
|
+
this.voicePanel?.pushLevel(rms);
|
|
2937
|
+
},
|
|
2938
|
+
onPhase: (phase) => {
|
|
2939
|
+
if (!this.voiceActive)
|
|
2940
|
+
return;
|
|
2941
|
+
if (phase === "silence") {
|
|
2942
|
+
this.voicePanel?.beginSilence(VOICE_SILENCE_MS);
|
|
2943
|
+
}
|
|
2944
|
+
else {
|
|
2945
|
+
this.voicePanel?.endSilence();
|
|
2946
|
+
}
|
|
2947
|
+
},
|
|
2948
|
+
onError: (message) => {
|
|
2949
|
+
this.voiceActive = false;
|
|
2950
|
+
this.resetVoiceUI();
|
|
2951
|
+
this.showError(`Voice input failed: ${message}`);
|
|
2952
|
+
},
|
|
2953
|
+
onCrash: (message) => {
|
|
2954
|
+
this.voiceActive = false;
|
|
2955
|
+
this.voiceDaemon = undefined;
|
|
2956
|
+
this.resetVoiceUI();
|
|
2957
|
+
this.showError(`Voice input daemon crashed: ${message}. It will restart on next use.`);
|
|
2958
|
+
},
|
|
2959
|
+
};
|
|
2960
|
+
}
|
|
2961
|
+
/** Inject decoded text into the editor via bracketed paste (with a trailing space). */
|
|
2962
|
+
commitVoiceText(text) {
|
|
2963
|
+
this.editor.handleInput(`\x1b[200~${text} \x1b[201~`);
|
|
2964
|
+
}
|
|
2965
|
+
beginDaemonVoiceCapture() {
|
|
2966
|
+
if (!this.voiceDaemon?.isReady)
|
|
2967
|
+
return;
|
|
2968
|
+
this.voiceActive = true;
|
|
2969
|
+
this.showVoicePanel().startListening();
|
|
2970
|
+
this.voiceDaemon.startCapture();
|
|
2971
|
+
}
|
|
2972
|
+
beginLegacyVoiceCapture(bin) {
|
|
2973
|
+
this.voiceActive = true;
|
|
2974
|
+
const panel = this.showVoicePanel();
|
|
2975
|
+
panel.startListening();
|
|
2976
|
+
this.voiceSession = startVoiceTranscribe(bin, {
|
|
2977
|
+
onStatus: (status) => {
|
|
2978
|
+
if (status === "done") {
|
|
2979
|
+
this.voiceActive = false;
|
|
2980
|
+
this.voiceSession = undefined;
|
|
2981
|
+
this.resetVoiceUI();
|
|
2982
|
+
return;
|
|
2983
|
+
}
|
|
2984
|
+
// Old binaries emit no LEVEL/PARTIAL, so the panel shows a spinner
|
|
2985
|
+
// for the batch phases; committed words still stream into the editor.
|
|
2986
|
+
if (status === "transcribing")
|
|
2987
|
+
panel.setTranscribing();
|
|
2988
|
+
},
|
|
2989
|
+
onSegment: (text) => {
|
|
2990
|
+
this.commitVoiceText(text);
|
|
2991
|
+
},
|
|
2992
|
+
onError: (message) => {
|
|
2993
|
+
this.voiceActive = false;
|
|
2994
|
+
this.voiceSession = undefined;
|
|
2995
|
+
this.resetVoiceUI();
|
|
2996
|
+
this.showError(`Voice input failed: ${message}`);
|
|
2997
|
+
},
|
|
2998
|
+
});
|
|
2999
|
+
}
|
|
3000
|
+
/** Resolve the `voicetools` binary path. Prefers an explicit VOICETOOLS_BIN
|
|
2823
3001
|
* override, otherwise resolves via the managed tools manager (bin dir / PATH /
|
|
2824
3002
|
* download from the published release). Returns undefined when unavailable.
|
|
2825
3003
|
*/
|
|
@@ -2829,6 +3007,28 @@ export class InteractiveMode {
|
|
|
2829
3007
|
return override;
|
|
2830
3008
|
return ensureTool("voicetools", true);
|
|
2831
3009
|
}
|
|
3010
|
+
/** Create (or reuse) the voice panel and mount it in the status container. */
|
|
3011
|
+
showVoicePanel() {
|
|
3012
|
+
if (!this.voicePanel) {
|
|
3013
|
+
this.statusContainer.clear();
|
|
3014
|
+
this.voicePanel = new VoicePanel(this.ui, keyHint("app.input.voiceTranscribe", "cancel"));
|
|
3015
|
+
this.statusContainer.addChild(this.voicePanel);
|
|
3016
|
+
}
|
|
3017
|
+
this.ui.requestRender();
|
|
3018
|
+
return this.voicePanel;
|
|
3019
|
+
}
|
|
3020
|
+
showVoiceWarming(message) {
|
|
3021
|
+
this.showVoicePanel().setWarming(message);
|
|
3022
|
+
}
|
|
3023
|
+
/** Collapse the voice panel back to nothing (idle) and stop its animation. */
|
|
3024
|
+
resetVoiceUI() {
|
|
3025
|
+
if (this.voicePanel) {
|
|
3026
|
+
this.voicePanel.dispose();
|
|
3027
|
+
this.voicePanel = undefined;
|
|
3028
|
+
}
|
|
3029
|
+
this.statusContainer.clear();
|
|
3030
|
+
this.ui.requestRender();
|
|
3031
|
+
}
|
|
2832
3032
|
showStatus(message) {
|
|
2833
3033
|
const children = this.chatContainer.children;
|
|
2834
3034
|
const last = children.length > 0 ? children[children.length - 1] : undefined;
|
|
@@ -4507,6 +4707,13 @@ export class InteractiveMode {
|
|
|
4507
4707
|
this.voiceSession.stop();
|
|
4508
4708
|
this.voiceSession = undefined;
|
|
4509
4709
|
}
|
|
4710
|
+
this.voiceDaemon?.shutdown();
|
|
4711
|
+
this.voiceDaemon = undefined;
|
|
4712
|
+
if (this.voicePanel) {
|
|
4713
|
+
this.voicePanel.dispose();
|
|
4714
|
+
this.voicePanel = undefined;
|
|
4715
|
+
}
|
|
4716
|
+
this.voiceActive = false;
|
|
4510
4717
|
this.voiceStarting = false;
|
|
4511
4718
|
if (this.settingsManager.getShowTerminalProgress()) {
|
|
4512
4719
|
this.ui.terminal.setProgress(false);
|