@kolisachint/hoocode-agent 0.4.103 → 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 +29 -0
- package/dist/config.d.ts +0 -6
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +0 -14
- package/dist/config.js.map +1 -1
- package/dist/migrations.d.ts.map +1 -1
- package/dist/migrations.js +2 -1
- package/dist/migrations.js.map +1 -1
- 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 +42 -3
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +260 -16
- 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 +163 -2
- package/dist/modes/interactive/voice-transcribe.js.map +1 -1
- package/dist/utils/tools-manager.d.ts +1 -1
- package/dist/utils/tools-manager.d.ts.map +1 -1
- package/dist/utils/tools-manager.js +23 -0
- package/dist/utils/tools-manager.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
|
@@ -9,7 +9,7 @@ import * as path from "node:path";
|
|
|
9
9
|
import { getProviders, } from "@kolisachint/hoocode-ai";
|
|
10
10
|
import { CombinedAutocompleteProvider, Container, fuzzyFilter, getCapabilities, hyperlink, Loader, Markdown, matchesKey, ProcessTerminal, Spacer, setKeybindings, Text, TruncatedText, TUI, } from "@kolisachint/hoocode-tui";
|
|
11
11
|
import { spawn, spawnSync } from "child_process";
|
|
12
|
-
import { APP_NAME, APP_TITLE, getAgentDir, getAuthPath, getDocsPath,
|
|
12
|
+
import { APP_NAME, APP_TITLE, getAgentDir, getAuthPath, getDocsPath, VERSION } from "../../config.js";
|
|
13
13
|
import { parseSkillBlock } from "../../core/agent-session.js";
|
|
14
14
|
import { FooterDataProvider } from "../../core/footer-data-provider.js";
|
|
15
15
|
import { KeybindingsManager } from "../../core/keybindings.js";
|
|
@@ -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,6 +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;
|
|
132
|
+
/** True while the voicetools binary is being resolved/downloaded before a session starts. */
|
|
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;
|
|
119
138
|
autocompleteProvider;
|
|
120
139
|
autocompleteProviderWrappers = [];
|
|
121
140
|
fdPath;
|
|
@@ -2760,39 +2779,256 @@ export class InteractiveMode {
|
|
|
2760
2779
|
* we update the previous status line instead of appending new ones to avoid log spam.
|
|
2761
2780
|
*/
|
|
2762
2781
|
/**
|
|
2763
|
-
* Toggle voice-to-text capture.
|
|
2764
|
-
*
|
|
2765
|
-
*
|
|
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.
|
|
2766
2790
|
*/
|
|
2767
2791
|
toggleVoiceTranscribe() {
|
|
2768
|
-
if (this.
|
|
2769
|
-
this.
|
|
2792
|
+
if (this.voiceActive) {
|
|
2793
|
+
if (this.voiceDaemon?.isReady) {
|
|
2794
|
+
this.voiceDaemon.cancel();
|
|
2795
|
+
}
|
|
2796
|
+
else {
|
|
2797
|
+
this.voiceSession?.stop();
|
|
2798
|
+
}
|
|
2770
2799
|
this.voiceSession = undefined;
|
|
2771
|
-
this.
|
|
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.
|
|
2807
|
+
this.voiceStarting = false;
|
|
2808
|
+
this.resetVoiceUI();
|
|
2809
|
+
return;
|
|
2810
|
+
}
|
|
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();
|
|
2772
2835
|
return;
|
|
2773
2836
|
}
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
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.
|
|
2845
|
+
this.voiceStarting = true;
|
|
2846
|
+
this.showVoiceWarming("Warming up voice input...");
|
|
2847
|
+
void this.resolveVoiceBin()
|
|
2848
|
+
.then(async (bin) => {
|
|
2849
|
+
if (!bin) {
|
|
2850
|
+
this.voiceStarting = false;
|
|
2851
|
+
this.resetVoiceUI();
|
|
2852
|
+
this.showError(VOICE_UNAVAILABLE_MESSAGE);
|
|
2853
|
+
return;
|
|
2854
|
+
}
|
|
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;
|
|
2862
|
+
}
|
|
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();
|
|
2879
|
+
})
|
|
2880
|
+
.catch((err) => {
|
|
2881
|
+
this.voiceStarting = false;
|
|
2882
|
+
this.resetVoiceUI();
|
|
2883
|
+
this.showError(`Voice input failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
2884
|
+
});
|
|
2885
|
+
}
|
|
2886
|
+
/**
|
|
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
|
+
},
|
|
2778
2959
|
};
|
|
2779
|
-
|
|
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, {
|
|
2780
2977
|
onStatus: (status) => {
|
|
2781
|
-
this.showStatus(statusLabels[status] ?? status);
|
|
2782
2978
|
if (status === "done") {
|
|
2979
|
+
this.voiceActive = false;
|
|
2783
2980
|
this.voiceSession = undefined;
|
|
2981
|
+
this.resetVoiceUI();
|
|
2982
|
+
return;
|
|
2784
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();
|
|
2785
2988
|
},
|
|
2786
2989
|
onSegment: (text) => {
|
|
2787
|
-
|
|
2788
|
-
this.editor.handleInput(`\x1b[200~${text} \x1b[201~`);
|
|
2990
|
+
this.commitVoiceText(text);
|
|
2789
2991
|
},
|
|
2790
2992
|
onError: (message) => {
|
|
2993
|
+
this.voiceActive = false;
|
|
2791
2994
|
this.voiceSession = undefined;
|
|
2995
|
+
this.resetVoiceUI();
|
|
2792
2996
|
this.showError(`Voice input failed: ${message}`);
|
|
2793
2997
|
},
|
|
2794
2998
|
});
|
|
2795
2999
|
}
|
|
3000
|
+
/** Resolve the `voicetools` binary path. Prefers an explicit VOICETOOLS_BIN
|
|
3001
|
+
* override, otherwise resolves via the managed tools manager (bin dir / PATH /
|
|
3002
|
+
* download from the published release). Returns undefined when unavailable.
|
|
3003
|
+
*/
|
|
3004
|
+
async resolveVoiceBin() {
|
|
3005
|
+
const override = process.env.VOICETOOLS_BIN?.trim();
|
|
3006
|
+
if (override)
|
|
3007
|
+
return override;
|
|
3008
|
+
return ensureTool("voicetools", true);
|
|
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
|
+
}
|
|
2796
3032
|
showStatus(message) {
|
|
2797
3033
|
const children = this.chatContainer.children;
|
|
2798
3034
|
const last = children.length > 0 ? children[children.length - 1] : undefined;
|
|
@@ -4471,6 +4707,14 @@ export class InteractiveMode {
|
|
|
4471
4707
|
this.voiceSession.stop();
|
|
4472
4708
|
this.voiceSession = undefined;
|
|
4473
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;
|
|
4717
|
+
this.voiceStarting = false;
|
|
4474
4718
|
if (this.settingsManager.getShowTerminalProgress()) {
|
|
4475
4719
|
this.ui.terminal.setProgress(false);
|
|
4476
4720
|
}
|