@iaforged/context-code 1.1.3 → 1.1.5
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/README.md +32 -8
- package/dist/src/commands/init.js +91 -219
- package/dist/src/commands/voice/index.js +6 -7
- package/dist/src/commands/voice/voice.js +87 -43
- package/dist/src/commands.js +1 -3
- package/dist/src/components/LogoV2/VoiceModeNotice.js +1 -1
- package/dist/src/components/PromptInput/VoiceIndicator.js +4 -4
- package/dist/src/components/Spinner.js +18 -18
- package/dist/src/constants/spinnerVerbs.js +9 -9
- package/dist/src/hooks/usePasteHandler.js +8 -8
- package/dist/src/hooks/useVoice.js +87 -804
- package/dist/src/hooks/useVoiceEnabled.js +3 -15
- package/dist/src/hooks/useVoiceIntegration.js +6 -25
- package/dist/src/keybindings/defaultBindings.js +9 -6
- package/dist/src/screens/REPL.js +10 -22
- package/dist/src/services/localDictation.js +377 -0
- package/dist/src/services/voice.js +9 -7
- package/dist/src/state/AppState.js +1 -3
- package/dist/src/tools/ConfigTool/ConfigTool.js +12 -15
- package/dist/src/tools/ConfigTool/supportedSettings.js +2 -2
- package/dist/src/utils/imagePaste.js +11 -5
- package/dist/src/utils/settings/types.js +2 -2
- package/dist/src/voice/voiceModeEnabled.js +5 -25
- package/dist/vendor/audio-capture/arm64-darwin/audio-capture.node +0 -0
- package/dist/vendor/audio-capture/arm64-linux/audio-capture.node +0 -0
- package/dist/vendor/audio-capture/arm64-win32/audio-capture.node +0 -0
- package/dist/vendor/audio-capture/x64-darwin/audio-capture.node +0 -0
- package/dist/vendor/audio-capture/x64-linux/audio-capture.node +0 -0
- package/dist/vendor/audio-capture/x64-win32/audio-capture.node +0 -0
- package/dist/vendor/audio-capture-src/index.js +114 -0
- package/dist/vendor/audio-capture-src/index.ts +155 -0
- package/docs/comandos.md +132 -121
- package/package.json +1 -1
|
@@ -1,21 +1,9 @@
|
|
|
1
|
-
import { useMemo } from 'react';
|
|
2
1
|
import { useAppState } from '../state/AppState.js';
|
|
3
|
-
import {
|
|
2
|
+
import { isVoiceGrowthBookEnabled } from '../voice/voiceModeEnabled.js';
|
|
4
3
|
/**
|
|
5
|
-
* Combines user intent (settings.voiceEnabled) with
|
|
6
|
-
* Only the auth half is memoized on authVersion — it's the expensive one
|
|
7
|
-
* (cold getClaudeAIOAuthTokens memoize → sync `security` spawn, ~60ms/call,
|
|
8
|
-
* ~180ms total in profile v5 when token refresh cleared the cache mid-session).
|
|
9
|
-
* GB is a cheap cached-map lookup and stays outside the memo so a mid-session
|
|
10
|
-
* kill-switch flip still takes effect on the next render.
|
|
11
|
-
*
|
|
12
|
-
* authVersion bumps on /login only. Background token refresh leaves it alone
|
|
13
|
-
* (user is still authed), so the auth memo stays correct without re-eval.
|
|
4
|
+
* Combines user intent (settings.voiceEnabled) with the runtime feature gate.
|
|
14
5
|
*/
|
|
15
6
|
export function useVoiceEnabled() {
|
|
16
7
|
const userIntent = useAppState(s => s.settings.voiceEnabled === true);
|
|
17
|
-
|
|
18
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
19
|
-
const authed = useMemo(hasVoiceAuth, [authVersion]);
|
|
20
|
-
return userIntent && authed && isVoiceGrowthBookEnabled();
|
|
8
|
+
return userIntent && isVoiceGrowthBookEnabled();
|
|
21
9
|
}
|
|
@@ -17,12 +17,7 @@ import { useVoiceEnabled } from './useVoiceEnabled.js';
|
|
|
17
17
|
// Capture the module namespace, not the function: spyOn() mutates the module
|
|
18
18
|
// object, so `voiceNs.useVoice(...)` resolves to the spy even if this module
|
|
19
19
|
// was loaded before the spy was installed (test ordering independence).
|
|
20
|
-
const voiceNs =
|
|
21
|
-
useVoice: ({ enabled: _e }) => ({
|
|
22
|
-
state: 'idle',
|
|
23
|
-
handleKeyEvent: (_fallbackMs) => { }
|
|
24
|
-
})
|
|
25
|
-
};
|
|
20
|
+
const voiceNs = require('./useVoice.js');
|
|
26
21
|
/* eslint-enable @typescript-eslint/no-require-imports */
|
|
27
22
|
// Maximum gap (ms) between key presses to count as held (auto-repeat).
|
|
28
23
|
// Terminal auto-repeat fires every 30-80ms; 120ms covers jitter while
|
|
@@ -172,18 +167,12 @@ export function useVoiceIntegration({ setInputValueRaw, inputValueRef, insertTex
|
|
|
172
167
|
// auth + GB kill-switch, with the auth half memoized on authVersion so
|
|
173
168
|
// render loops never hit a cold keychain spawn.
|
|
174
169
|
// biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
|
|
175
|
-
const voiceEnabled =
|
|
176
|
-
const voiceState =
|
|
177
|
-
|
|
178
|
-
useVoiceState(s => s.voiceState) : 'idle';
|
|
179
|
-
const voiceInterimTranscript = feature('VOICE_MODE') ?
|
|
180
|
-
// biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
|
|
181
|
-
useVoiceState(s_0 => s_0.voiceInterimTranscript) : '';
|
|
170
|
+
const voiceEnabled = useVoiceEnabled();
|
|
171
|
+
const voiceState = useVoiceState(s => s.voiceState);
|
|
172
|
+
const voiceInterimTranscript = useVoiceState(s_0 => s_0.voiceInterimTranscript);
|
|
182
173
|
// Set the voice anchor for focus mode (where recording starts via terminal
|
|
183
174
|
// focus, not key hold). Key-hold sets the anchor in stripTrailing.
|
|
184
175
|
useEffect(() => {
|
|
185
|
-
if (!feature('VOICE_MODE'))
|
|
186
|
-
return;
|
|
187
176
|
if (voiceState === 'recording' && voicePrefixRef.current === null) {
|
|
188
177
|
const input = inputValueRef.current;
|
|
189
178
|
const offset_0 = insertTextRef.current?.cursorOffset ?? input.length;
|
|
@@ -201,8 +190,6 @@ export function useVoiceIntegration({ setInputValueRaw, inputValueRef, insertTex
|
|
|
201
190
|
// transcribes speech. The prefix (user-typed text before the cursor) is
|
|
202
191
|
// preserved and the transcript is inserted between prefix and suffix.
|
|
203
192
|
useEffect(() => {
|
|
204
|
-
if (!feature('VOICE_MODE'))
|
|
205
|
-
return;
|
|
206
193
|
if (voicePrefixRef.current === null)
|
|
207
194
|
return;
|
|
208
195
|
const prefix_0 = voicePrefixRef.current;
|
|
@@ -233,8 +220,6 @@ export function useVoiceIntegration({ setInputValueRaw, inputValueRef, insertTex
|
|
|
233
220
|
lastSetInputRef.current = newValue_0;
|
|
234
221
|
}, [voiceInterimTranscript, setInputValueRaw, inputValueRef, insertTextRef]);
|
|
235
222
|
const handleVoiceTranscript = useCallback((text) => {
|
|
236
|
-
if (!feature('VOICE_MODE'))
|
|
237
|
-
return;
|
|
238
223
|
const prefix_1 = voicePrefixRef.current;
|
|
239
224
|
// No voice anchor — voice was reset (or never started). Nothing to do.
|
|
240
225
|
if (prefix_1 === null)
|
|
@@ -283,8 +268,6 @@ export function useVoiceIntegration({ setInputValueRaw, inputValueRef, insertTex
|
|
|
283
268
|
// Compute the character range of interim (not-yet-finalized) transcript
|
|
284
269
|
// text in the input value, so the UI can dim it.
|
|
285
270
|
const interimRange = useMemo(() => {
|
|
286
|
-
if (!feature('VOICE_MODE'))
|
|
287
|
-
return null;
|
|
288
271
|
if (voicePrefixRef.current === null)
|
|
289
272
|
return null;
|
|
290
273
|
if (voiceInterimTranscript.length === 0)
|
|
@@ -335,10 +318,8 @@ export function useVoiceKeybindingHandler({ voiceHandleKeyEvent, stripTrailing,
|
|
|
335
318
|
const keybindingContext = useOptionalKeybindingContext();
|
|
336
319
|
const isModalOverlayActive = useIsModalOverlayActive();
|
|
337
320
|
// biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
|
|
338
|
-
const voiceEnabled =
|
|
339
|
-
const voiceState =
|
|
340
|
-
// biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
|
|
341
|
-
useVoiceState(s => s.voiceState) : 'idle';
|
|
321
|
+
const voiceEnabled = useVoiceEnabled();
|
|
322
|
+
const voiceState = useVoiceState(s => s.voiceState);
|
|
342
323
|
// Find the configured key for voice:pushToTalk from keybinding context.
|
|
343
324
|
// Forward iteration with last-wins (matching the resolver): if a later
|
|
344
325
|
// Chat binding overrides the same chord with null or a different
|
|
@@ -6,10 +6,13 @@ import { getPlatform } from '../utils/platform.js';
|
|
|
6
6
|
* Default keybindings that match current Context Code behavior.
|
|
7
7
|
* These are loaded first, then user keybindings.json overrides them.
|
|
8
8
|
*/
|
|
9
|
-
// Platform-specific image paste
|
|
10
|
-
// - Windows: alt+v
|
|
9
|
+
// Platform-specific image paste shortcuts:
|
|
10
|
+
// - Windows: accept both ctrl+v and alt+v. Some terminals forward ctrl+v as a
|
|
11
|
+
// regular keybinding, while others reserve it for paste handling.
|
|
11
12
|
// - Other platforms: ctrl+v
|
|
12
|
-
const
|
|
13
|
+
const IMAGE_PASTE_BINDINGS = getPlatform() === 'windows'
|
|
14
|
+
? { 'ctrl+v': 'chat:imagePaste', 'alt+v': 'chat:imagePaste' }
|
|
15
|
+
: { 'ctrl+v': 'chat:imagePaste' };
|
|
13
16
|
// Modifier-only chords (like shift+tab) may fail on Windows Terminal without VT mode
|
|
14
17
|
// See: https://github.com/microsoft/terminal/issues/879#issuecomment-618801651
|
|
15
18
|
// Node enabled VT mode in 24.2.0 / 22.17.0: https://github.com/nodejs/node/pull/58358
|
|
@@ -76,8 +79,8 @@ export const DEFAULT_BINDINGS = [
|
|
|
76
79
|
'ctrl+x ctrl+e': 'chat:externalEditor',
|
|
77
80
|
'ctrl+g': 'chat:externalEditor',
|
|
78
81
|
'ctrl+s': 'chat:stash',
|
|
79
|
-
// Image paste
|
|
80
|
-
|
|
82
|
+
// Image paste shortcuts (platform-specific bindings defined above)
|
|
83
|
+
...IMAGE_PASTE_BINDINGS,
|
|
81
84
|
...(feature('MESSAGE_ACTIONS')
|
|
82
85
|
? { 'shift+up': 'chat:messageActions' }
|
|
83
86
|
: {}),
|
|
@@ -86,7 +89,7 @@ export const DEFAULT_BINDINGS = [
|
|
|
86
89
|
// add a voice:pushToTalk entry (last wins); to disable, use /voice
|
|
87
90
|
// — null-unbinding space hits a pre-existing useKeybinding.ts trap
|
|
88
91
|
// where 'unbound' swallows the event (space dead for typing).
|
|
89
|
-
|
|
92
|
+
space: 'voice:pushToTalk',
|
|
90
93
|
},
|
|
91
94
|
},
|
|
92
95
|
{
|
package/dist/src/screens/REPL.js
CHANGED
|
@@ -94,12 +94,8 @@ import { isHumanTurn } from '../utils/messagePredicates.js';
|
|
|
94
94
|
import { logError } from '../utils/log.js';
|
|
95
95
|
// Dead code elimination: conditional imports
|
|
96
96
|
/* eslint-disable custom-rules/no-process-env-top-level, @typescript-eslint/no-require-imports */
|
|
97
|
-
const useVoiceIntegration =
|
|
98
|
-
|
|
99
|
-
handleKeyEvent: () => { },
|
|
100
|
-
resetAnchor: () => { }
|
|
101
|
-
});
|
|
102
|
-
const VoiceKeybindingHandler = feature('VOICE_MODE') ? require('../hooks/useVoiceIntegration.js').VoiceKeybindingHandler : () => null;
|
|
97
|
+
const useVoiceIntegration = require('../hooks/useVoiceIntegration.js').useVoiceIntegration;
|
|
98
|
+
const VoiceKeybindingHandler = require('../hooks/useVoiceIntegration.js').VoiceKeybindingHandler;
|
|
103
99
|
// Frustration detection is ant-only (dogfooding). Conditional require so external
|
|
104
100
|
// builds eliminate the module entirely (including its two O(n) useMemos that run
|
|
105
101
|
// on every messages change, plus the GrowthBook fetch).
|
|
@@ -3609,19 +3605,11 @@ export function REPL({ commands: initialCommands, debug, initialTools, initialMe
|
|
|
3609
3605
|
void onQuery([userMessage], newAbortController, true, [], mainLoopModel);
|
|
3610
3606
|
return true;
|
|
3611
3607
|
}, [onQuery, mainLoopModel, store]);
|
|
3612
|
-
|
|
3613
|
-
|
|
3614
|
-
|
|
3615
|
-
|
|
3616
|
-
|
|
3617
|
-
inputValueRef,
|
|
3618
|
-
insertTextRef
|
|
3619
|
-
}) : {
|
|
3620
|
-
stripTrailing: () => 0,
|
|
3621
|
-
handleKeyEvent: () => { },
|
|
3622
|
-
resetAnchor: () => { },
|
|
3623
|
-
interimRange: null
|
|
3624
|
-
};
|
|
3608
|
+
const voice = useVoiceIntegration({
|
|
3609
|
+
setInputValueRaw,
|
|
3610
|
+
inputValueRef,
|
|
3611
|
+
insertTextRef
|
|
3612
|
+
});
|
|
3625
3613
|
useInboxPoller({
|
|
3626
3614
|
enabled: isAgentSwarmsEnabled(),
|
|
3627
3615
|
isLoading,
|
|
@@ -3977,7 +3965,7 @@ export function REPL({ commands: initialCommands, debug, initialTools, initialMe
|
|
|
3977
3965
|
const transcriptScrollRef = isFullscreenEnvEnabled() && !disableVirtualScroll && !dumpMode ? scrollRef : undefined;
|
|
3978
3966
|
const transcriptMessagesElement = _jsx(Messages, { messages: transcriptMessages, tools: tools, commands: commands, verbose: true, toolJSX: null, toolUseConfirmQueue: [], inProgressToolUseIDs: inProgressToolUseIDs, isMessageSelectorVisible: false, conversationId: conversationId, screen: screen, agentDefinitions: agentDefinitions, streamingToolUses: transcriptStreamingToolUses, showAllInTranscript: showAllInTranscript, onOpenRateLimitOptions: handleOpenRateLimitOptions, isLoading: isLoading, hidePastThinking: true, streamingThinking: streamingThinking, scrollRef: transcriptScrollRef, jumpRef: jumpRef, onSearchMatchesChange: onSearchMatchesChange, scanElement: scanElement, setPositions: setPositions, disableRenderCap: dumpMode });
|
|
3979
3967
|
const transcriptToolJSX = toolJSX && _jsx(Box, { flexDirection: "column", width: "100%", children: toolJSX.jsx });
|
|
3980
|
-
const transcriptReturn = _jsxs(KeybindingSetup, { children: [_jsx(AnimatedTerminalTitle, { isAnimating: titleIsAnimating, title: terminalTitle, disabled: titleDisabled, noPrefix: showStatusInTerminalTab }), _jsx(GlobalKeybindingHandlers, { ...globalKeybindingProps }),
|
|
3968
|
+
const transcriptReturn = _jsxs(KeybindingSetup, { children: [_jsx(AnimatedTerminalTitle, { isAnimating: titleIsAnimating, title: terminalTitle, disabled: titleDisabled, noPrefix: showStatusInTerminalTab }), _jsx(GlobalKeybindingHandlers, { ...globalKeybindingProps }), _jsx(VoiceKeybindingHandler, { voiceHandleKeyEvent: voice.handleKeyEvent, stripTrailing: voice.stripTrailing, resetAnchor: voice.resetAnchor, isActive: !toolJSX?.isLocalJSXCommand }), _jsx(CommandKeybindingHandlers, { onSubmit: onSubmit, isActive: !toolJSX?.isLocalJSXCommand }), transcriptScrollRef ?
|
|
3981
3969
|
_jsx(ScrollKeybindingHandler, { scrollRef: scrollRef,
|
|
3982
3970
|
// Yield wheel/ctrl+u/d to UltraplanChoiceDialog's own scroll
|
|
3983
3971
|
// handler while the modal is showing.
|
|
@@ -4091,7 +4079,7 @@ export function REPL({ commands: initialCommands, debug, initialTools, initialMe
|
|
|
4091
4079
|
// flexGrow in FullscreenLayout resolves against this Box. The transcript
|
|
4092
4080
|
// early return above wraps its virtual-scroll branch the same way; only
|
|
4093
4081
|
// the 30-cap dump branch stays unwrapped for native terminal scrollback.
|
|
4094
|
-
const mainReturn = _jsxs(KeybindingSetup, { children: [_jsx(AnimatedTerminalTitle, { isAnimating: titleIsAnimating, title: terminalTitle, disabled: titleDisabled, noPrefix: showStatusInTerminalTab }), _jsx(GlobalKeybindingHandlers, { ...globalKeybindingProps }),
|
|
4082
|
+
const mainReturn = _jsxs(KeybindingSetup, { children: [_jsx(AnimatedTerminalTitle, { isAnimating: titleIsAnimating, title: terminalTitle, disabled: titleDisabled, noPrefix: showStatusInTerminalTab }), _jsx(GlobalKeybindingHandlers, { ...globalKeybindingProps }), _jsx(VoiceKeybindingHandler, { voiceHandleKeyEvent: voice.handleKeyEvent, stripTrailing: voice.stripTrailing, resetAnchor: voice.resetAnchor, isActive: !toolJSX?.isLocalJSXCommand }), _jsx(CommandKeybindingHandlers, { onSubmit: onSubmit, isActive: !toolJSX?.isLocalJSXCommand }), _jsx(ScrollKeybindingHandler, { scrollRef: scrollRef, isActive: isFullscreenEnvEnabled() && (centeredModal != null || !focusedInputDialog || focusedInputDialog === 'tool-permission'), onScroll: centeredModal || toolPermissionOverlay || viewedAgentTask ? undefined : composedOnScroll }), feature('MESSAGE_ACTIONS') && isFullscreenEnvEnabled() && !disableMessageActions ? _jsx(MessageActionsKeybindings, { handlers: messageActionHandlers, isActive: cursor !== null }) : null, _jsx(CancelRequestHandler, { ...cancelRequestProps }), _jsx(MCPConnectionManager, { dynamicMcpConfig: dynamicMcpConfig, isStrictMcpConfig: strictMcpConfig, children: _jsx(FullscreenLayout, { scrollRef: scrollRef, overlay: toolPermissionOverlay, bottomFloat: feature('BUDDY') && companionVisible && !companionNarrow ? _jsx(CompanionFloatingBubble, {}) : undefined, modal: centeredModal, modalScrollRef: modalScrollRef, dividerYRef: dividerYRef, hidePill: !!viewedAgentTask, hideSticky: !!viewedTeammateTask, newMessageCount: unseenDivider?.count ?? 0, onPillClick: () => {
|
|
4095
4083
|
setCursor(null);
|
|
4096
4084
|
jumpToNew(scrollRef.current);
|
|
4097
4085
|
}, scrollable: _jsxs(_Fragment, { children: [_jsx(TeammateViewHeader, {}), _jsx(Messages, { messages: displayedMessages, tools: tools, commands: commands, verbose: verbose, toolJSX: toolJSX, toolUseConfirmQueue: toolUseConfirmQueue, inProgressToolUseIDs: viewedTeammateTask ? viewedTeammateTask.inProgressToolUseIDs ?? new Set() : inProgressToolUseIDs, isMessageSelectorVisible: isMessageSelectorVisible, conversationId: conversationId, screen: screen, streamingToolUses: streamingToolUses, showAllInTranscript: showAllInTranscript, agentDefinitions: agentDefinitions, onOpenRateLimitOptions: handleOpenRateLimitOptions, isLoading: isLoading, streamingText: isLoading && !viewedAgentTask ? visibleStreamingText : null, isBriefOnly: viewedAgentTask ? false : isBriefOnly, unseenDivider: viewedAgentTask ? undefined : unseenDivider, scrollRef: isFullscreenEnvEnabled() ? scrollRef : undefined, trackStickyPrompt: isFullscreenEnvEnabled() ? true : undefined, cursor: cursor, setCursor: setCursor, cursorNavRef: cursorNavRef }), _jsx(AwsAuthStatusBox, {}), !disabled && placeholderText && !centeredModal && _jsx(UserTextMessage, { param: {
|
|
@@ -4344,7 +4332,7 @@ export function REPL({ commands: initialCommands, debug, initialTools, initialMe
|
|
|
4344
4332
|
}).then(appendStdout).catch(logError);
|
|
4345
4333
|
} }) : null, mrRender(), !toolJSX?.shouldHidePromptInput && !focusedInputDialog && !isExiting && !disabled && !cursor && _jsxs(_Fragment, { children: [autoRunIssueReason && _jsx(AutoRunIssueNotification, { onRun: handleAutoRunIssue, onCancel: handleCancelAutoRunIssue, reason: getAutoRunIssueReasonText(autoRunIssueReason) }), postCompactSurvey.state !== 'closed' ? _jsx(FeedbackSurvey, { state: postCompactSurvey.state, lastResponse: postCompactSurvey.lastResponse, handleSelect: postCompactSurvey.handleSelect, inputValue: inputValue, setInputValue: setInputValue, onRequestFeedback: handleSurveyRequestFeedback }) : memorySurvey.state !== 'closed' ? _jsx(FeedbackSurvey, { state: memorySurvey.state, lastResponse: memorySurvey.lastResponse, handleSelect: memorySurvey.handleSelect, handleTranscriptSelect: memorySurvey.handleTranscriptSelect, inputValue: inputValue, setInputValue: setInputValue, onRequestFeedback: handleSurveyRequestFeedback, message: "How well did Claude use its memory? (optional)" }) : _jsx(FeedbackSurvey, { state: feedbackSurvey.state, lastResponse: feedbackSurvey.lastResponse, handleSelect: feedbackSurvey.handleSelect, handleTranscriptSelect: feedbackSurvey.handleTranscriptSelect, inputValue: inputValue, setInputValue: setInputValue, onRequestFeedback: didAutoRunIssueRef.current ? undefined : handleSurveyRequestFeedback }), frustrationDetection.state !== 'closed' && _jsx(FeedbackSurvey, { state: frustrationDetection.state, lastResponse: null, handleSelect: () => { }, handleTranscriptSelect: frustrationDetection.handleTranscriptSelect, inputValue: inputValue, setInputValue: setInputValue }), "external" === 'ant' && skillImprovementSurvey.suggestion && _jsx(SkillImprovementSurvey, { isOpen: skillImprovementSurvey.isOpen, skillName: skillImprovementSurvey.suggestion.skillName, updates: skillImprovementSurvey.suggestion.updates, handleSelect: skillImprovementSurvey.handleSelect, inputValue: inputValue, setInputValue: setInputValue }), showIssueFlagBanner && _jsx(IssueFlagBanner, {}), _jsx(PromptInput, { debug: debug, ideSelection: ideSelection, hasSuppressedDialogs: !!hasSuppressedDialogs, isLocalJSXCommandActive: isShowingLocalJSXCommand, getToolUseContext: getToolUseContext, toolPermissionContext: toolPermissionContext, setToolPermissionContext: setToolPermissionContext, apiKeyStatus: apiKeyStatus, commands: commands, agents: agentDefinitions.activeAgents, isLoading: isLoading, onExit: handleExit, verbose: verbose, messages: messages, onAutoUpdaterResult: setAutoUpdaterResult, autoUpdaterResult: autoUpdaterResult, input: inputValue, onInputChange: setInputValue, mode: inputMode, onModeChange: setInputMode, stashedPrompt: stashedPrompt, setStashedPrompt: setStashedPrompt, submitCount: submitCount, onShowMessageSelector: handleShowMessageSelector, onMessageActionsEnter:
|
|
4346
4334
|
// Works during isLoading — edit cancels first; uuid selection survives appends.
|
|
4347
|
-
feature('MESSAGE_ACTIONS') && isFullscreenEnvEnabled() && !disableMessageActions ? enterMessageActions : undefined, mcpClients: mcpClients, pastedContents: pastedContents, setPastedContents: setPastedContents, vimMode: vimMode, setVimMode: setVimMode, showBashesDialog: showBashesDialog, setShowBashesDialog: setShowBashesDialog, onSubmit: onSubmit, onAgentSubmit: onAgentSubmit, isSearchingHistory: isSearchingHistory, setIsSearchingHistory: setIsSearchingHistory, helpOpen: isHelpOpen, setHelpOpen: setIsHelpOpen, insertTextRef:
|
|
4335
|
+
feature('MESSAGE_ACTIONS') && isFullscreenEnvEnabled() && !disableMessageActions ? enterMessageActions : undefined, mcpClients: mcpClients, pastedContents: pastedContents, setPastedContents: setPastedContents, vimMode: vimMode, setVimMode: setVimMode, showBashesDialog: showBashesDialog, setShowBashesDialog: setShowBashesDialog, onSubmit: onSubmit, onAgentSubmit: onAgentSubmit, isSearchingHistory: isSearchingHistory, setIsSearchingHistory: setIsSearchingHistory, helpOpen: isHelpOpen, setHelpOpen: setIsHelpOpen, insertTextRef: insertTextRef, voiceInterimRange: voice.interimRange }), _jsx(SessionBackgroundHint, { onBackgroundSession: handleBackgroundSession, isLoading: isLoading })] }), cursor &&
|
|
4348
4336
|
_jsx(MessageActionsBar, { cursor: cursor }), focusedInputDialog === 'message-selector' && _jsx(MessageSelector, { messages: messages, preselectedMessage: messageSelectorPreselect, onPreRestore: onCancel, onRestoreCode: async (message) => {
|
|
4349
4337
|
await fileHistoryRewind((updater) => {
|
|
4350
4338
|
setAppState(prev => ({
|
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
import { accessSync, constants } from 'fs';
|
|
2
|
+
import { mkdir, mkdtemp, readdir, readFile, rm, writeFile } from 'fs/promises';
|
|
3
|
+
import { tmpdir } from 'os';
|
|
4
|
+
import { basename, join } from 'path';
|
|
5
|
+
import { execa } from 'execa';
|
|
6
|
+
import { saveGlobalConfig, getGlobalConfig } from '../utils/config.js';
|
|
7
|
+
import { logForDebugging } from '../utils/debug.js';
|
|
8
|
+
import { getClaudeConfigHomeDir } from '../utils/envUtils.js';
|
|
9
|
+
const SAMPLE_RATE = 16_000;
|
|
10
|
+
const CHANNELS = 1;
|
|
11
|
+
const BITS_PER_SAMPLE = 16;
|
|
12
|
+
const DEFAULT_MODEL_NAME = 'base';
|
|
13
|
+
const WHISPER_RELEASE_API = 'https://api.github.com/repos/ggml-org/whisper.cpp/releases/latest';
|
|
14
|
+
const WHISPER_MODEL_BASE_URL = 'https://huggingface.co/ggerganov/whisper.cpp/resolve/main';
|
|
15
|
+
function fileExists(path) {
|
|
16
|
+
try {
|
|
17
|
+
accessSync(path, constants.F_OK);
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function getStoredDictationConfig() {
|
|
25
|
+
return getGlobalConfig().dictationConfig;
|
|
26
|
+
}
|
|
27
|
+
function getConfiguredExecutable() {
|
|
28
|
+
const explicit = process.env.CONTEXT_CODE_DICTATION_EXECUTABLE ??
|
|
29
|
+
process.env.WHISPER_CPP_PATH;
|
|
30
|
+
if (explicit?.trim()) {
|
|
31
|
+
return explicit.trim();
|
|
32
|
+
}
|
|
33
|
+
const stored = getStoredDictationConfig()?.executablePath;
|
|
34
|
+
if (stored?.trim()) {
|
|
35
|
+
return stored.trim();
|
|
36
|
+
}
|
|
37
|
+
return process.platform === 'win32' ? 'whisper-cli.exe' : 'whisper-cli';
|
|
38
|
+
}
|
|
39
|
+
function getConfiguredModel() {
|
|
40
|
+
const model = process.env.CONTEXT_CODE_DICTATION_MODEL ??
|
|
41
|
+
process.env.WHISPER_MODEL_PATH;
|
|
42
|
+
if (model?.trim()) {
|
|
43
|
+
return model.trim();
|
|
44
|
+
}
|
|
45
|
+
const stored = getStoredDictationConfig()?.modelPath;
|
|
46
|
+
return stored?.trim() ? stored.trim() : null;
|
|
47
|
+
}
|
|
48
|
+
function getConfiguredLanguage() {
|
|
49
|
+
const language = process.env.CONTEXT_CODE_DICTATION_LANGUAGE ?? process.env.WHISPER_LANGUAGE;
|
|
50
|
+
if (language?.trim()) {
|
|
51
|
+
return language.trim();
|
|
52
|
+
}
|
|
53
|
+
const stored = getStoredDictationConfig()?.language;
|
|
54
|
+
return stored?.trim() ? stored.trim() : 'es';
|
|
55
|
+
}
|
|
56
|
+
function getDictationConfig() {
|
|
57
|
+
const executable = getConfiguredExecutable();
|
|
58
|
+
const model = getConfiguredModel();
|
|
59
|
+
if (!executable || !model) {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
executable,
|
|
64
|
+
model,
|
|
65
|
+
language: getConfiguredLanguage(),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function getInstallRoot() {
|
|
69
|
+
return join(getClaudeConfigHomeDir(), 'dictation', 'whisper.cpp');
|
|
70
|
+
}
|
|
71
|
+
function normalizeModelName(modelName) {
|
|
72
|
+
const raw = (modelName ?? DEFAULT_MODEL_NAME).trim();
|
|
73
|
+
if (!raw) {
|
|
74
|
+
return DEFAULT_MODEL_NAME;
|
|
75
|
+
}
|
|
76
|
+
return raw.endsWith('.bin')
|
|
77
|
+
? raw.replace(/^ggml-/, '').replace(/\.bin$/i, '')
|
|
78
|
+
: raw;
|
|
79
|
+
}
|
|
80
|
+
function buildModelFileName(modelName) {
|
|
81
|
+
return `ggml-${normalizeModelName(modelName)}.bin`;
|
|
82
|
+
}
|
|
83
|
+
function getPlatformArchiveExtension() {
|
|
84
|
+
return process.platform === 'win32' ? '.zip' : '.tar.gz';
|
|
85
|
+
}
|
|
86
|
+
function escapePowerShellLiteral(value) {
|
|
87
|
+
return value.replace(/'/g, "''");
|
|
88
|
+
}
|
|
89
|
+
async function fetchBuffer(url, label) {
|
|
90
|
+
const response = await fetch(url, {
|
|
91
|
+
headers: {
|
|
92
|
+
'User-Agent': 'ContextCode-DictationInstaller',
|
|
93
|
+
Accept: 'application/octet-stream, application/json',
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
if (!response.ok) {
|
|
97
|
+
throw new Error(`No se pudo descargar ${label} (${response.status} ${response.statusText}).`);
|
|
98
|
+
}
|
|
99
|
+
return Buffer.from(await response.arrayBuffer());
|
|
100
|
+
}
|
|
101
|
+
async function fetchLatestRelease() {
|
|
102
|
+
const response = await fetch(WHISPER_RELEASE_API, {
|
|
103
|
+
headers: {
|
|
104
|
+
'User-Agent': 'ContextCode-DictationInstaller',
|
|
105
|
+
Accept: 'application/vnd.github+json',
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
if (!response.ok) {
|
|
109
|
+
throw new Error(`No se pudo consultar la ultima version de whisper.cpp (${response.status} ${response.statusText}).`);
|
|
110
|
+
}
|
|
111
|
+
return (await response.json());
|
|
112
|
+
}
|
|
113
|
+
function scoreReleaseAsset(asset) {
|
|
114
|
+
const name = asset.name.toLowerCase();
|
|
115
|
+
if (name.includes('source code')) {
|
|
116
|
+
return -100;
|
|
117
|
+
}
|
|
118
|
+
let score = 0;
|
|
119
|
+
const expectedExt = getPlatformArchiveExtension();
|
|
120
|
+
if (name.endsWith(expectedExt))
|
|
121
|
+
score += 10;
|
|
122
|
+
if (name.includes('bin'))
|
|
123
|
+
score += 5;
|
|
124
|
+
if (name.includes('build'))
|
|
125
|
+
score += 2;
|
|
126
|
+
switch (process.platform) {
|
|
127
|
+
case 'win32':
|
|
128
|
+
if (name.includes('win') || name.includes('windows') || name.includes('msvc')) {
|
|
129
|
+
score += 5;
|
|
130
|
+
}
|
|
131
|
+
if (name.includes('x64') || name.includes('amd64'))
|
|
132
|
+
score += 4;
|
|
133
|
+
if (name.includes('arm64'))
|
|
134
|
+
score -= 4;
|
|
135
|
+
break;
|
|
136
|
+
case 'darwin':
|
|
137
|
+
if (name.includes('mac') || name.includes('darwin') || name.includes('apple')) {
|
|
138
|
+
score += 5;
|
|
139
|
+
}
|
|
140
|
+
if (process.arch === 'arm64') {
|
|
141
|
+
if (name.includes('arm64'))
|
|
142
|
+
score += 4;
|
|
143
|
+
if (name.includes('x64') || name.includes('amd64'))
|
|
144
|
+
score -= 4;
|
|
145
|
+
}
|
|
146
|
+
break;
|
|
147
|
+
default:
|
|
148
|
+
if (name.includes('linux'))
|
|
149
|
+
score += 5;
|
|
150
|
+
if (process.arch === 'x64' && (name.includes('x64') || name.includes('amd64'))) {
|
|
151
|
+
score += 4;
|
|
152
|
+
}
|
|
153
|
+
if (process.arch === 'arm64' && name.includes('arm64'))
|
|
154
|
+
score += 4;
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
157
|
+
return score;
|
|
158
|
+
}
|
|
159
|
+
function pickReleaseAsset(assets) {
|
|
160
|
+
const ranked = [...assets]
|
|
161
|
+
.map(asset => ({ asset, score: scoreReleaseAsset(asset) }))
|
|
162
|
+
.filter(entry => entry.score > 0)
|
|
163
|
+
.sort((a, b) => b.score - a.score);
|
|
164
|
+
return ranked[0]?.asset ?? null;
|
|
165
|
+
}
|
|
166
|
+
async function extractArchive(archivePath, destination) {
|
|
167
|
+
if (archivePath.endsWith('.zip')) {
|
|
168
|
+
if (process.platform === 'win32') {
|
|
169
|
+
await execa('powershell', [
|
|
170
|
+
'-NoProfile',
|
|
171
|
+
'-NonInteractive',
|
|
172
|
+
'-Command',
|
|
173
|
+
`Expand-Archive -LiteralPath '${escapePowerShellLiteral(archivePath)}' -DestinationPath '${escapePowerShellLiteral(destination)}' -Force`,
|
|
174
|
+
], { windowsHide: true });
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
await execa('unzip', ['-o', archivePath, '-d', destination], {
|
|
178
|
+
windowsHide: true,
|
|
179
|
+
});
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
if (archivePath.endsWith('.tar.gz') || archivePath.endsWith('.tgz')) {
|
|
183
|
+
await execa('tar', ['-xzf', archivePath, '-C', destination], {
|
|
184
|
+
windowsHide: true,
|
|
185
|
+
});
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
throw new Error(`Formato de paquete no soportado: ${basename(archivePath)}`);
|
|
189
|
+
}
|
|
190
|
+
async function findFileRecursive(rootDir, predicate) {
|
|
191
|
+
const entries = await readdir(rootDir, { withFileTypes: true });
|
|
192
|
+
for (const entry of entries) {
|
|
193
|
+
const absolutePath = join(rootDir, entry.name);
|
|
194
|
+
if (entry.isDirectory()) {
|
|
195
|
+
const nested = await findFileRecursive(absolutePath, predicate);
|
|
196
|
+
if (nested) {
|
|
197
|
+
return nested;
|
|
198
|
+
}
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
if (predicate(absolutePath)) {
|
|
202
|
+
return absolutePath;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
async function findInstalledExecutable(installDir) {
|
|
208
|
+
const candidateNames = process.platform === 'win32'
|
|
209
|
+
? ['whisper-cli.exe', 'main.exe']
|
|
210
|
+
: ['whisper-cli', 'main'];
|
|
211
|
+
return findFileRecursive(installDir, path => candidateNames.some(name => path.toLowerCase().endsWith(name.toLowerCase())));
|
|
212
|
+
}
|
|
213
|
+
function persistInstalledDictationConfig(config) {
|
|
214
|
+
saveGlobalConfig(current => ({
|
|
215
|
+
...current,
|
|
216
|
+
dictationConfig: {
|
|
217
|
+
...(current.dictationConfig ?? {}),
|
|
218
|
+
...config,
|
|
219
|
+
},
|
|
220
|
+
}));
|
|
221
|
+
}
|
|
222
|
+
export function getLocalDictationSetupHint() {
|
|
223
|
+
return [
|
|
224
|
+
'Configura Whisper local antes de usar /dictar.',
|
|
225
|
+
'Opcion recomendada:',
|
|
226
|
+
'- Ejecuta /dictar install para descargar e instalar el backend automaticamente.',
|
|
227
|
+
'Tambien puedes configurarlo manualmente con estas variables:',
|
|
228
|
+
'- CONTEXT_CODE_DICTATION_EXECUTABLE o WHISPER_CPP_PATH',
|
|
229
|
+
'- CONTEXT_CODE_DICTATION_MODEL o WHISPER_MODEL_PATH',
|
|
230
|
+
'Ejemplo PowerShell:',
|
|
231
|
+
'setx CONTEXT_CODE_DICTATION_EXECUTABLE "C:\\whisper\\whisper-cli.exe"',
|
|
232
|
+
'setx CONTEXT_CODE_DICTATION_MODEL "C:\\whisper\\models\\ggml-base.bin"',
|
|
233
|
+
].join('\n');
|
|
234
|
+
}
|
|
235
|
+
export async function checkLocalDictationConfiguration() {
|
|
236
|
+
const config = getDictationConfig();
|
|
237
|
+
if (!config) {
|
|
238
|
+
return { available: false, error: getLocalDictationSetupHint() };
|
|
239
|
+
}
|
|
240
|
+
if (!fileExists(config.model)) {
|
|
241
|
+
return {
|
|
242
|
+
available: false,
|
|
243
|
+
error: `No se encontro el modelo de dictado: ${config.model}\n` +
|
|
244
|
+
getLocalDictationSetupHint(),
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
try {
|
|
248
|
+
await execa(config.executable, ['--help'], {
|
|
249
|
+
timeout: 10_000,
|
|
250
|
+
windowsHide: true,
|
|
251
|
+
reject: false,
|
|
252
|
+
});
|
|
253
|
+
return { available: true };
|
|
254
|
+
}
|
|
255
|
+
catch (error) {
|
|
256
|
+
return {
|
|
257
|
+
available: false,
|
|
258
|
+
error: `No se pudo ejecutar el backend de dictado (${config.executable}).\n` +
|
|
259
|
+
`${error instanceof Error ? error.message : String(error)}`,
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
export async function getLocalDictationStatus() {
|
|
264
|
+
const config = getDictationConfig();
|
|
265
|
+
const check = await checkLocalDictationConfiguration();
|
|
266
|
+
const stored = getStoredDictationConfig();
|
|
267
|
+
if (!config || !check.available) {
|
|
268
|
+
return check.error ?? 'El dictado local no esta configurado.';
|
|
269
|
+
}
|
|
270
|
+
const lines = [
|
|
271
|
+
'Dictado local configurado.',
|
|
272
|
+
`Backend: ${config.executable}`,
|
|
273
|
+
`Modelo: ${config.model}`,
|
|
274
|
+
];
|
|
275
|
+
if (stored?.releaseTag) {
|
|
276
|
+
lines.push(`Release instalada: ${stored.releaseTag}`);
|
|
277
|
+
}
|
|
278
|
+
if (stored?.installedAt) {
|
|
279
|
+
lines.push(`Instalado: ${stored.installedAt}`);
|
|
280
|
+
}
|
|
281
|
+
return lines.join('\n');
|
|
282
|
+
}
|
|
283
|
+
export async function installLocalDictation(modelName) {
|
|
284
|
+
const normalizedModelName = normalizeModelName(modelName);
|
|
285
|
+
const modelFileName = buildModelFileName(normalizedModelName);
|
|
286
|
+
const installDir = getInstallRoot();
|
|
287
|
+
const tempDir = await mkdtemp(join(tmpdir(), 'context-dictation-install-'));
|
|
288
|
+
try {
|
|
289
|
+
const release = await fetchLatestRelease();
|
|
290
|
+
const asset = pickReleaseAsset(release.assets);
|
|
291
|
+
if (!asset) {
|
|
292
|
+
throw new Error(`No encontre un binario compatible de whisper.cpp para ${process.platform}/${process.arch}.`);
|
|
293
|
+
}
|
|
294
|
+
const archivePath = join(tempDir, asset.name);
|
|
295
|
+
const modelPath = join(installDir, 'models', modelFileName);
|
|
296
|
+
await mkdir(installDir, { recursive: true });
|
|
297
|
+
await writeFile(archivePath, await fetchBuffer(asset.browser_download_url, `whisper.cpp (${asset.name})`));
|
|
298
|
+
await rm(installDir, { recursive: true, force: true });
|
|
299
|
+
await mkdir(installDir, { recursive: true });
|
|
300
|
+
await extractArchive(archivePath, installDir);
|
|
301
|
+
const executablePath = await findInstalledExecutable(installDir);
|
|
302
|
+
if (!executablePath) {
|
|
303
|
+
throw new Error('La descarga termino, pero no encontre whisper-cli dentro del paquete instalado.');
|
|
304
|
+
}
|
|
305
|
+
await mkdir(join(installDir, 'models'), { recursive: true });
|
|
306
|
+
await writeFile(modelPath, await fetchBuffer(`${WHISPER_MODEL_BASE_URL}/${modelFileName}?download=1`, `modelo ${modelFileName}`));
|
|
307
|
+
persistInstalledDictationConfig({
|
|
308
|
+
executablePath,
|
|
309
|
+
installDir,
|
|
310
|
+
installedAt: new Date().toISOString(),
|
|
311
|
+
modelName: normalizedModelName,
|
|
312
|
+
modelPath,
|
|
313
|
+
releaseTag: release.tag_name,
|
|
314
|
+
});
|
|
315
|
+
logForDebugging(`[dictation] Instalacion completada en ${installDir} usando ${asset.name}`);
|
|
316
|
+
return {
|
|
317
|
+
executablePath,
|
|
318
|
+
installDir,
|
|
319
|
+
modelPath,
|
|
320
|
+
releaseTag: release.tag_name,
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
finally {
|
|
324
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
function buildWavBuffer(chunks) {
|
|
328
|
+
const pcmData = Buffer.concat(chunks);
|
|
329
|
+
const byteRate = SAMPLE_RATE * CHANNELS * (BITS_PER_SAMPLE / 8);
|
|
330
|
+
const blockAlign = CHANNELS * (BITS_PER_SAMPLE / 8);
|
|
331
|
+
const header = Buffer.alloc(44);
|
|
332
|
+
header.write('RIFF', 0);
|
|
333
|
+
header.writeUInt32LE(36 + pcmData.length, 4);
|
|
334
|
+
header.write('WAVE', 8);
|
|
335
|
+
header.write('fmt ', 12);
|
|
336
|
+
header.writeUInt32LE(16, 16);
|
|
337
|
+
header.writeUInt16LE(1, 20);
|
|
338
|
+
header.writeUInt16LE(CHANNELS, 22);
|
|
339
|
+
header.writeUInt32LE(SAMPLE_RATE, 24);
|
|
340
|
+
header.writeUInt32LE(byteRate, 28);
|
|
341
|
+
header.writeUInt16LE(blockAlign, 32);
|
|
342
|
+
header.writeUInt16LE(BITS_PER_SAMPLE, 34);
|
|
343
|
+
header.write('data', 36);
|
|
344
|
+
header.writeUInt32LE(pcmData.length, 40);
|
|
345
|
+
return Buffer.concat([header, pcmData]);
|
|
346
|
+
}
|
|
347
|
+
export async function transcribePcmBuffers(chunks, opts) {
|
|
348
|
+
const config = getDictationConfig();
|
|
349
|
+
if (!config) {
|
|
350
|
+
throw new Error(getLocalDictationSetupHint());
|
|
351
|
+
}
|
|
352
|
+
const dir = await mkdtemp(join(tmpdir(), 'context-dictation-'));
|
|
353
|
+
const inputPath = join(dir, 'input.wav');
|
|
354
|
+
const outputPrefix = join(dir, 'result');
|
|
355
|
+
const outputTxtPath = `${outputPrefix}.txt`;
|
|
356
|
+
try {
|
|
357
|
+
await writeFile(inputPath, buildWavBuffer(chunks));
|
|
358
|
+
const args = ['-m', config.model, '-f', inputPath, '-otxt', '-of', outputPrefix];
|
|
359
|
+
const language = opts?.language || config.language;
|
|
360
|
+
if (language && language !== 'auto') {
|
|
361
|
+
args.push('-l', language);
|
|
362
|
+
}
|
|
363
|
+
logForDebugging(`[dictation] Running ${config.executable} ${args.map(part => JSON.stringify(part)).join(' ')}`);
|
|
364
|
+
const result = await execa(config.executable, args, {
|
|
365
|
+
timeout: 120_000,
|
|
366
|
+
windowsHide: true,
|
|
367
|
+
reject: false,
|
|
368
|
+
});
|
|
369
|
+
if (result.exitCode !== 0) {
|
|
370
|
+
throw new Error(result.stderr || result.stdout || 'Whisper finalizo con error.');
|
|
371
|
+
}
|
|
372
|
+
return (await readFile(outputTxtPath, 'utf8')).trim();
|
|
373
|
+
}
|
|
374
|
+
finally {
|
|
375
|
+
await rm(dir, { recursive: true, force: true });
|
|
376
|
+
}
|
|
377
|
+
}
|
|
@@ -16,7 +16,7 @@ let audioNapiPromise = null;
|
|
|
16
16
|
function loadAudioNapi() {
|
|
17
17
|
audioNapiPromise ??= (async () => {
|
|
18
18
|
const t0 = Date.now();
|
|
19
|
-
const mod = await import('audio-capture-
|
|
19
|
+
const mod = await import('../../vendor/audio-capture-src/index.js');
|
|
20
20
|
// vendor/audio-capture-src/index.ts defers require(...node) until the
|
|
21
21
|
// first function call — trigger it here so timing reflects real cost.
|
|
22
22
|
mod.isNativeAudioAvailable();
|
|
@@ -146,7 +146,9 @@ export async function checkVoiceDependencies() {
|
|
|
146
146
|
if (process.platform === 'win32') {
|
|
147
147
|
return {
|
|
148
148
|
available: false,
|
|
149
|
-
missing: [
|
|
149
|
+
missing: [
|
|
150
|
+
'El dictado local requiere el modulo nativo de audio y no se pudo cargar.',
|
|
151
|
+
],
|
|
150
152
|
installCommand: null,
|
|
151
153
|
};
|
|
152
154
|
}
|
|
@@ -189,7 +191,7 @@ export async function checkRecordingAvailability() {
|
|
|
189
191
|
if (isRunningOnHomespace() || isEnvTruthy(process.env.CLAUDE_CODE_REMOTE)) {
|
|
190
192
|
return {
|
|
191
193
|
available: false,
|
|
192
|
-
reason: '
|
|
194
|
+
reason: 'El dictado local necesita acceso al microfono, pero este entorno no tiene un dispositivo de audio disponible.\n\nPara usar dictado, ejecuta Context Code de forma local.',
|
|
193
195
|
};
|
|
194
196
|
}
|
|
195
197
|
// Native audio module (cpal) handles everything on macOS, Linux, and Windows
|
|
@@ -201,10 +203,10 @@ export async function checkRecordingAvailability() {
|
|
|
201
203
|
if (process.platform === 'win32') {
|
|
202
204
|
return {
|
|
203
205
|
available: false,
|
|
204
|
-
reason: '
|
|
206
|
+
reason: 'La grabacion para dictado requiere el modulo nativo de audio y no se pudo cargar.',
|
|
205
207
|
};
|
|
206
208
|
}
|
|
207
|
-
const wslNoAudioReason = '
|
|
209
|
+
const wslNoAudioReason = 'El dictado local no pudo acceder a un dispositivo de audio en WSL.\n\nWSL2 con WSLg (Windows 11) expone audio mediante PulseAudio. Si estas en Windows 10 o WSL1, ejecuta Context Code directamente en Windows.';
|
|
208
210
|
// On Linux (including WSL), probe arecord. hasCommand() is insufficient:
|
|
209
211
|
// the binary can exist while the device open() fails (WSL1, Win10-WSL2,
|
|
210
212
|
// headless Linux). WSL2+WSLg (Win11 default) works via PulseAudio RDP
|
|
@@ -239,8 +241,8 @@ export async function checkRecordingAvailability() {
|
|
|
239
241
|
return {
|
|
240
242
|
available: false,
|
|
241
243
|
reason: pm
|
|
242
|
-
? `
|
|
243
|
-
: '
|
|
244
|
+
? `El dictado local requiere SoX para grabar audio. Instalalo con: ${pm.displayCommand}`
|
|
245
|
+
: 'El dictado local requiere SoX para grabar audio. Instalalo manualmente:\n macOS: brew install sox\n Ubuntu/Debian: sudo apt-get install sox\n Fedora: sudo dnf install sox',
|
|
244
246
|
};
|
|
245
247
|
}
|
|
246
248
|
return { available: true, reason: null };
|