@machina.ai/cell-cli 1.40.1-rc2 → 1.41.1-rc1
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/dist/package.json +3 -3
- package/dist/src/acp/commands/restore.test.d.ts +6 -0
- package/dist/src/acp/commands/restore.test.js +179 -0
- package/dist/src/acp/commands/restore.test.js.map +1 -0
- package/dist/src/commands/mcp/list.js +14 -4
- package/dist/src/commands/mcp/list.js.map +1 -1
- package/dist/src/commands/mcp/list.test.js +48 -0
- package/dist/src/commands/mcp/list.test.js.map +1 -1
- package/dist/src/config/config.d.ts +1 -0
- package/dist/src/config/config.js +41 -7
- package/dist/src/config/config.js.map +1 -1
- package/dist/src/config/config.test.js +126 -71
- package/dist/src/config/config.test.js.map +1 -1
- package/dist/src/config/settings-validation.js +24 -6
- package/dist/src/config/settings-validation.js.map +1 -1
- package/dist/src/config/settings-validation.test.js +96 -0
- package/dist/src/config/settings-validation.test.js.map +1 -1
- package/dist/src/config/settings.js +30 -60
- package/dist/src/config/settings.js.map +1 -1
- package/dist/src/config/settings.test.js +78 -0
- package/dist/src/config/settings.test.js.map +1 -1
- package/dist/src/config/settingsSchema.d.ts +101 -0
- package/dist/src/config/settingsSchema.js +98 -0
- package/dist/src/config/settingsSchema.js.map +1 -1
- package/dist/src/gemini.d.ts +1 -1
- package/dist/src/gemini.js +24 -17
- package/dist/src/gemini.js.map +1 -1
- package/dist/src/gemini.test.js +42 -4
- package/dist/src/gemini.test.js.map +1 -1
- package/dist/src/gemini_cleanup.test.js +103 -1
- package/dist/src/gemini_cleanup.test.js.map +1 -1
- package/dist/src/generated/git-commit.d.ts +2 -2
- package/dist/src/generated/git-commit.js +2 -2
- package/dist/src/services/BuiltinCommandLoader.js +2 -0
- package/dist/src/services/BuiltinCommandLoader.js.map +1 -1
- package/dist/src/services/BuiltinCommandLoader.test.js +2 -0
- package/dist/src/services/BuiltinCommandLoader.test.js.map +1 -1
- package/dist/src/services/prompt-processors/shellProcessor.test.js +1 -0
- package/dist/src/services/prompt-processors/shellProcessor.test.js.map +1 -1
- package/dist/src/test-utils/mockConfig.js +1 -0
- package/dist/src/test-utils/mockConfig.js.map +1 -1
- package/dist/src/test-utils/render.js +3 -0
- package/dist/src/test-utils/render.js.map +1 -1
- package/dist/src/ui/AppContainer.js +19 -0
- package/dist/src/ui/AppContainer.js.map +1 -1
- package/dist/src/ui/commands/types.d.ts +2 -1
- package/dist/src/ui/commands/types.js.map +1 -1
- package/dist/src/ui/commands/voiceCommand.d.ts +7 -0
- package/dist/src/ui/commands/voiceCommand.js +29 -0
- package/dist/src/ui/commands/voiceCommand.js.map +1 -0
- package/dist/src/ui/components/AsciiArt.d.ts +6 -6
- package/dist/src/ui/components/AsciiArt.js +6 -6
- package/dist/src/ui/components/DialogManager.js +4 -0
- package/dist/src/ui/components/DialogManager.js.map +1 -1
- package/dist/src/ui/components/InputPrompt.js +34 -13
- package/dist/src/ui/components/InputPrompt.js.map +1 -1
- package/dist/src/ui/components/InputPrompt.test.js +289 -0
- package/dist/src/ui/components/InputPrompt.test.js.map +1 -1
- package/dist/src/ui/components/ModelDialog.js +17 -5
- package/dist/src/ui/components/ModelDialog.js.map +1 -1
- package/dist/src/ui/components/ModelDialog.test.js +1 -0
- package/dist/src/ui/components/ModelDialog.test.js.map +1 -1
- package/dist/src/ui/components/SessionBrowser.test.js +1 -0
- package/dist/src/ui/components/SessionBrowser.test.js.map +1 -1
- package/dist/src/ui/components/SettingsDialog.test.js +22 -0
- package/dist/src/ui/components/SettingsDialog.test.js.map +1 -1
- package/dist/src/ui/components/VoiceModelDialog.d.ts +11 -0
- package/dist/src/ui/components/VoiceModelDialog.js +118 -0
- package/dist/src/ui/components/VoiceModelDialog.js.map +1 -0
- package/dist/src/ui/components/messages/HintMessage.js +2 -1
- package/dist/src/ui/components/messages/HintMessage.js.map +1 -1
- package/dist/src/ui/components/messages/HintMessage.test.d.ts +6 -0
- package/dist/src/ui/components/messages/HintMessage.test.js +42 -0
- package/dist/src/ui/components/messages/HintMessage.test.js.map +1 -0
- package/dist/src/ui/components/messages/UserMessage.js +2 -1
- package/dist/src/ui/components/messages/UserMessage.js.map +1 -1
- package/dist/src/ui/components/messages/UserMessage.test.js +24 -0
- package/dist/src/ui/components/messages/UserMessage.test.js.map +1 -1
- package/dist/src/ui/components/messages/UserShellMessage.js +2 -1
- package/dist/src/ui/components/messages/UserShellMessage.js.map +1 -1
- package/dist/src/ui/components/messages/UserShellMessage.test.d.ts +6 -0
- package/dist/src/ui/components/messages/UserShellMessage.test.js +40 -0
- package/dist/src/ui/components/messages/UserShellMessage.test.js.map +1 -0
- package/dist/src/ui/components/shared/BaseSettingsDialog.js +1 -1
- package/dist/src/ui/components/shared/BaseSettingsDialog.js.map +1 -1
- package/dist/src/ui/contexts/KeypressContext.test.js +5 -3
- package/dist/src/ui/contexts/KeypressContext.test.js.map +1 -1
- package/dist/src/ui/contexts/UIActionsContext.d.ts +3 -0
- package/dist/src/ui/contexts/UIActionsContext.js.map +1 -1
- package/dist/src/ui/contexts/UIStateContext.d.ts +2 -0
- package/dist/src/ui/contexts/UIStateContext.js.map +1 -1
- package/dist/src/ui/hooks/slashCommandProcessor.d.ts +2 -0
- package/dist/src/ui/hooks/slashCommandProcessor.js +4 -0
- package/dist/src/ui/hooks/slashCommandProcessor.js.map +1 -1
- package/dist/src/ui/hooks/slashCommandProcessor.test.js +2 -0
- package/dist/src/ui/hooks/slashCommandProcessor.test.js.map +1 -1
- package/dist/src/ui/hooks/useSlashCompletion.js +15 -11
- package/dist/src/ui/hooks/useSlashCompletion.js.map +1 -1
- package/dist/src/ui/hooks/useSlashCompletion.test.js +40 -0
- package/dist/src/ui/hooks/useSlashCompletion.test.js.map +1 -1
- package/dist/src/ui/hooks/useVoiceMode.d.ts +28 -0
- package/dist/src/ui/hooks/useVoiceMode.js +333 -0
- package/dist/src/ui/hooks/useVoiceMode.js.map +1 -0
- package/dist/src/ui/hooks/useVoiceModelCommand.d.ts +12 -0
- package/dist/src/ui/hooks/useVoiceModelCommand.js +21 -0
- package/dist/src/ui/hooks/useVoiceModelCommand.js.map +1 -0
- package/dist/src/ui/key/keyBindings.d.ts +1 -0
- package/dist/src/ui/key/keyBindings.js +7 -3
- package/dist/src/ui/key/keyBindings.js.map +1 -1
- package/dist/src/ui/noninteractive/nonInteractiveUi.js +1 -0
- package/dist/src/ui/noninteractive/nonInteractiveUi.js.map +1 -1
- package/dist/src/utils/activityLogger.js +19 -9
- package/dist/src/utils/activityLogger.js.map +1 -1
- package/dist/src/utils/activityLogger.test.js +76 -1
- package/dist/src/utils/activityLogger.test.js.map +1 -1
- package/dist/src/utils/handleAutoUpdate.js +4 -3
- package/dist/src/utils/handleAutoUpdate.js.map +1 -1
- package/dist/src/utils/handleAutoUpdate.test.js +8 -6
- package/dist/src/utils/handleAutoUpdate.test.js.map +1 -1
- package/dist/src/utils/sandbox.js +23 -10
- package/dist/src/utils/sandbox.js.map +1 -1
- package/dist/src/utils/sandbox.test.js +46 -0
- package/dist/src/utils/sandbox.test.js.map +1 -1
- package/dist/src/utils/sessionCleanup.test.js +1 -0
- package/dist/src/utils/sessionCleanup.test.js.map +1 -1
- package/dist/src/utils/sessionUtils.d.ts +4 -0
- package/dist/src/utils/sessionUtils.js +24 -0
- package/dist/src/utils/sessionUtils.js.map +1 -1
- package/dist/src/utils/sessionUtils.test.js +27 -0
- package/dist/src/utils/sessionUtils.test.js.map +1 -1
- package/dist/src/utils/userStartupWarnings.test.js +14 -0
- package/dist/src/utils/userStartupWarnings.test.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -3
|
@@ -9,8 +9,33 @@ import { createMockSettings } from '../../test-utils/settings.js';
|
|
|
9
9
|
import { makeFakeConfig } from '@google/gemini-cli-core';
|
|
10
10
|
import { waitFor } from '../../test-utils/async.js';
|
|
11
11
|
import { act, useState, useMemo } from 'react';
|
|
12
|
+
const { fakeTranscriptionProvider } = vi.hoisted(() => {
|
|
13
|
+
// Use require within hoisted block for immediate synchronous access
|
|
14
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports, no-restricted-syntax
|
|
15
|
+
const { EventEmitter } = require('node:events');
|
|
16
|
+
class FakeTranscriptionProvider extends EventEmitter {
|
|
17
|
+
connect = vi.fn().mockResolvedValue(undefined);
|
|
18
|
+
disconnect = vi.fn();
|
|
19
|
+
sendAudioChunk = vi.fn();
|
|
20
|
+
getTranscription = vi.fn().mockReturnValue('');
|
|
21
|
+
}
|
|
22
|
+
return {
|
|
23
|
+
fakeTranscriptionProvider: new FakeTranscriptionProvider(),
|
|
24
|
+
};
|
|
25
|
+
});
|
|
26
|
+
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
|
27
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
28
|
+
const actual = (await importOriginal());
|
|
29
|
+
return {
|
|
30
|
+
...actual,
|
|
31
|
+
TranscriptionFactory: {
|
|
32
|
+
createProvider: vi.fn(() => fakeTranscriptionProvider),
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
});
|
|
12
36
|
import { InputPrompt, tryTogglePasteExpansion, } from './InputPrompt.js';
|
|
13
37
|
import { InputContext } from '../contexts/InputContext.js';
|
|
38
|
+
import {} from '../contexts/UIStateContext.js';
|
|
14
39
|
import { calculateTransformationsForLine, calculateTransformedLine, } from './shared/text-buffer.js';
|
|
15
40
|
import { ApprovalMode, debugLogger, coreEvents, } from '@google/gemini-cli-core';
|
|
16
41
|
import * as path from 'node:path';
|
|
@@ -335,6 +360,7 @@ describe('InputPrompt', () => {
|
|
|
335
360
|
getWorkspaceContext: () => ({
|
|
336
361
|
getDirectories: () => ['/test/project/src'],
|
|
337
362
|
}),
|
|
363
|
+
getContentGeneratorConfig: () => ({ apiKey: 'test-api-key' }),
|
|
338
364
|
},
|
|
339
365
|
slashCommands: mockSlashCommands,
|
|
340
366
|
commandContext: mockCommandContext,
|
|
@@ -3723,6 +3749,269 @@ describe('InputPrompt', () => {
|
|
|
3723
3749
|
unmount();
|
|
3724
3750
|
});
|
|
3725
3751
|
});
|
|
3752
|
+
describe('Voice Mode', () => {
|
|
3753
|
+
beforeEach(() => {
|
|
3754
|
+
fakeTranscriptionProvider.removeAllListeners();
|
|
3755
|
+
vi.clearAllMocks();
|
|
3756
|
+
});
|
|
3757
|
+
it('should start recording when space is pressed and voice mode is enabled (toggle)', async () => {
|
|
3758
|
+
await act(async () => {
|
|
3759
|
+
mockBuffer.setText('');
|
|
3760
|
+
});
|
|
3761
|
+
const { stdin, unmount, lastFrame } = await renderWithProviders(_jsx(TestInputPrompt, { ...props, focus: true, buffer: mockBuffer }), {
|
|
3762
|
+
uiState: { isVoiceModeEnabled: true },
|
|
3763
|
+
settings: createMockSettings({
|
|
3764
|
+
experimental: { voice: { activationMode: 'toggle' } },
|
|
3765
|
+
}),
|
|
3766
|
+
});
|
|
3767
|
+
// Initially not recording
|
|
3768
|
+
expect(lastFrame()).not.toContain('🎙️ Listening...');
|
|
3769
|
+
expect(lastFrame()).toContain('Voice mode: Space to start/stop recording');
|
|
3770
|
+
// Press space to start
|
|
3771
|
+
await act(async () => {
|
|
3772
|
+
stdin.write(' ');
|
|
3773
|
+
});
|
|
3774
|
+
// Now should show listening
|
|
3775
|
+
await waitFor(() => {
|
|
3776
|
+
expect(lastFrame()).toContain('🎙️ Listening...');
|
|
3777
|
+
});
|
|
3778
|
+
unmount();
|
|
3779
|
+
});
|
|
3780
|
+
it('should toggle recording off when space is pressed again (toggle)', async () => {
|
|
3781
|
+
await act(async () => {
|
|
3782
|
+
mockBuffer.setText('');
|
|
3783
|
+
});
|
|
3784
|
+
const { stdin, unmount, lastFrame } = await renderWithProviders(_jsx(TestInputPrompt, { ...props, focus: true, buffer: mockBuffer }), {
|
|
3785
|
+
uiState: { isVoiceModeEnabled: true },
|
|
3786
|
+
settings: createMockSettings({
|
|
3787
|
+
experimental: { voice: { activationMode: 'toggle' } },
|
|
3788
|
+
}),
|
|
3789
|
+
});
|
|
3790
|
+
// Start recording
|
|
3791
|
+
await act(async () => {
|
|
3792
|
+
stdin.write(' ');
|
|
3793
|
+
});
|
|
3794
|
+
await waitFor(() => {
|
|
3795
|
+
expect(lastFrame()).toContain('🎙️ Listening...');
|
|
3796
|
+
});
|
|
3797
|
+
// Stop recording
|
|
3798
|
+
await act(async () => {
|
|
3799
|
+
stdin.write(' ');
|
|
3800
|
+
});
|
|
3801
|
+
await waitFor(() => {
|
|
3802
|
+
expect(lastFrame()).not.toContain('🎙️ Listening...');
|
|
3803
|
+
expect(lastFrame()).toContain('Voice mode: Space to start/stop recording');
|
|
3804
|
+
});
|
|
3805
|
+
unmount();
|
|
3806
|
+
});
|
|
3807
|
+
it('should resume recording when space is pressed even if buffer is not empty (toggle)', async () => {
|
|
3808
|
+
await act(async () => {
|
|
3809
|
+
mockBuffer.setText('some existing text');
|
|
3810
|
+
});
|
|
3811
|
+
const { stdin, unmount, lastFrame } = await renderWithProviders(_jsx(TestInputPrompt, { ...props, focus: true, buffer: mockBuffer }), {
|
|
3812
|
+
uiState: { isVoiceModeEnabled: true },
|
|
3813
|
+
settings: createMockSettings({
|
|
3814
|
+
experimental: { voice: { activationMode: 'toggle' } },
|
|
3815
|
+
}),
|
|
3816
|
+
});
|
|
3817
|
+
// Should show voice mode hint even if buffer is not empty (new behavior)
|
|
3818
|
+
expect(lastFrame()).toContain('Voice mode: Space to start/stop recording');
|
|
3819
|
+
expect(lastFrame()).toContain('some existing text');
|
|
3820
|
+
// Press space to start recording again
|
|
3821
|
+
await act(async () => {
|
|
3822
|
+
stdin.write(' ');
|
|
3823
|
+
});
|
|
3824
|
+
await waitFor(() => {
|
|
3825
|
+
expect(lastFrame()).toContain('🎙️ Listening...');
|
|
3826
|
+
});
|
|
3827
|
+
unmount();
|
|
3828
|
+
});
|
|
3829
|
+
it('should not start recording if voice mode is disabled (toggle)', async () => {
|
|
3830
|
+
await act(async () => {
|
|
3831
|
+
mockBuffer.setText('');
|
|
3832
|
+
});
|
|
3833
|
+
const { stdin, unmount, lastFrame } = await renderWithProviders(_jsx(TestInputPrompt, { ...props, focus: true, buffer: mockBuffer }), {
|
|
3834
|
+
uiState: { isVoiceModeEnabled: false },
|
|
3835
|
+
settings: createMockSettings({
|
|
3836
|
+
experimental: { voice: { activationMode: 'toggle' } },
|
|
3837
|
+
}),
|
|
3838
|
+
});
|
|
3839
|
+
// Press space
|
|
3840
|
+
await act(async () => {
|
|
3841
|
+
stdin.write(' ');
|
|
3842
|
+
});
|
|
3843
|
+
// Should NOT show listening, instead should call handleInput which handles space
|
|
3844
|
+
expect(lastFrame()).not.toContain('🎙️ Listening...');
|
|
3845
|
+
expect(mockBuffer.handleInput).toHaveBeenCalled();
|
|
3846
|
+
unmount();
|
|
3847
|
+
});
|
|
3848
|
+
it('should append transcription correctly across multiple turn updates (toggle)', async () => {
|
|
3849
|
+
await act(async () => {
|
|
3850
|
+
mockBuffer.setText('initial');
|
|
3851
|
+
});
|
|
3852
|
+
const { stdin, unmount } = await renderWithProviders(_jsx(TestInputPrompt, { ...props, focus: true, buffer: mockBuffer }), {
|
|
3853
|
+
uiState: { isVoiceModeEnabled: true },
|
|
3854
|
+
settings: createMockSettings({
|
|
3855
|
+
experimental: { voice: { activationMode: 'toggle' } },
|
|
3856
|
+
}),
|
|
3857
|
+
});
|
|
3858
|
+
// Start recording
|
|
3859
|
+
await act(async () => {
|
|
3860
|
+
stdin.write(' ');
|
|
3861
|
+
});
|
|
3862
|
+
// Emit first transcription
|
|
3863
|
+
await act(async () => {
|
|
3864
|
+
fakeTranscriptionProvider.emit('transcription', 'hello');
|
|
3865
|
+
});
|
|
3866
|
+
await waitFor(() => {
|
|
3867
|
+
expect(mockBuffer.setText).toHaveBeenCalledWith('initial hello', 'end');
|
|
3868
|
+
});
|
|
3869
|
+
// Emit turnComplete (Gemini Live starts over after this)
|
|
3870
|
+
await act(async () => {
|
|
3871
|
+
fakeTranscriptionProvider.emit('turnComplete');
|
|
3872
|
+
});
|
|
3873
|
+
// Emit second part (Gemini Live sends new turn text starting from empty)
|
|
3874
|
+
await act(async () => {
|
|
3875
|
+
fakeTranscriptionProvider.emit('transcription', 'world');
|
|
3876
|
+
});
|
|
3877
|
+
await waitFor(() => {
|
|
3878
|
+
// Should have appended 'world' to the baseline 'initial hello'
|
|
3879
|
+
expect(mockBuffer.setText).toHaveBeenCalledWith('initial hello world', 'end');
|
|
3880
|
+
});
|
|
3881
|
+
unmount();
|
|
3882
|
+
});
|
|
3883
|
+
it('should append transcription correctly when resuming voice mode (toggle)', async () => {
|
|
3884
|
+
await act(async () => {
|
|
3885
|
+
mockBuffer.setText('First turn.');
|
|
3886
|
+
});
|
|
3887
|
+
const { stdin, unmount } = await renderWithProviders(_jsx(TestInputPrompt, { ...props, focus: true, buffer: mockBuffer }), {
|
|
3888
|
+
uiState: { isVoiceModeEnabled: true },
|
|
3889
|
+
settings: createMockSettings({
|
|
3890
|
+
experimental: { voice: { activationMode: 'toggle' } },
|
|
3891
|
+
}),
|
|
3892
|
+
});
|
|
3893
|
+
// Start recording (resumed)
|
|
3894
|
+
await act(async () => {
|
|
3895
|
+
stdin.write(' ');
|
|
3896
|
+
});
|
|
3897
|
+
// Emit transcription
|
|
3898
|
+
await act(async () => {
|
|
3899
|
+
fakeTranscriptionProvider.emit('transcription', 'Second turn.');
|
|
3900
|
+
});
|
|
3901
|
+
await waitFor(() => {
|
|
3902
|
+
expect(mockBuffer.setText).toHaveBeenCalledWith('First turn. Second turn.', 'end');
|
|
3903
|
+
});
|
|
3904
|
+
unmount();
|
|
3905
|
+
});
|
|
3906
|
+
describe('push-to-talk', () => {
|
|
3907
|
+
beforeEach(() => {
|
|
3908
|
+
vi.useFakeTimers();
|
|
3909
|
+
});
|
|
3910
|
+
afterEach(() => {
|
|
3911
|
+
vi.useRealTimers();
|
|
3912
|
+
});
|
|
3913
|
+
it('should insert a space on a single tap', async () => {
|
|
3914
|
+
const { stdin, unmount, lastFrame } = await renderWithProviders(_jsx(TestInputPrompt, { ...props, focus: true, buffer: mockBuffer }), {
|
|
3915
|
+
uiState: { isVoiceModeEnabled: true },
|
|
3916
|
+
settings: createMockSettings({
|
|
3917
|
+
experimental: { voice: { activationMode: 'push-to-talk' } },
|
|
3918
|
+
}),
|
|
3919
|
+
});
|
|
3920
|
+
expect(lastFrame()).toContain('Voice mode: Hold Space to record');
|
|
3921
|
+
// Press space once
|
|
3922
|
+
await act(async () => {
|
|
3923
|
+
stdin.write(' ');
|
|
3924
|
+
});
|
|
3925
|
+
// Should insert space optimistically
|
|
3926
|
+
expect(mockBuffer.insert).toHaveBeenCalledWith(' ');
|
|
3927
|
+
expect(lastFrame()).not.toContain('🎙️ Listening...');
|
|
3928
|
+
// Advance timer past HOLD_DELAY_MS
|
|
3929
|
+
await act(async () => {
|
|
3930
|
+
vi.advanceTimersByTime(700);
|
|
3931
|
+
});
|
|
3932
|
+
expect(lastFrame()).not.toContain('🎙️ Listening...');
|
|
3933
|
+
unmount();
|
|
3934
|
+
});
|
|
3935
|
+
it('should start recording on hold (simulated by repeat spaces)', async () => {
|
|
3936
|
+
const { stdin, unmount, lastFrame } = await renderWithProviders(_jsx(TestInputPrompt, { ...props, focus: true, buffer: mockBuffer }), {
|
|
3937
|
+
uiState: { isVoiceModeEnabled: true },
|
|
3938
|
+
settings: createMockSettings({
|
|
3939
|
+
experimental: { voice: { activationMode: 'push-to-talk' } },
|
|
3940
|
+
}),
|
|
3941
|
+
});
|
|
3942
|
+
// First space
|
|
3943
|
+
await act(async () => {
|
|
3944
|
+
stdin.write(' ');
|
|
3945
|
+
});
|
|
3946
|
+
expect(mockBuffer.insert).toHaveBeenCalledWith(' ');
|
|
3947
|
+
// Second space (repeat)
|
|
3948
|
+
await act(async () => {
|
|
3949
|
+
stdin.write(' ');
|
|
3950
|
+
});
|
|
3951
|
+
await waitFor(() => {
|
|
3952
|
+
// Should have backspaced the optimistic space
|
|
3953
|
+
expect(mockBuffer.backspace).toHaveBeenCalled();
|
|
3954
|
+
// Should show listening
|
|
3955
|
+
expect(lastFrame()).toContain('🎙️ Listening...');
|
|
3956
|
+
});
|
|
3957
|
+
unmount();
|
|
3958
|
+
});
|
|
3959
|
+
it('should stop recording when space heartbeat stops (release)', async () => {
|
|
3960
|
+
const { stdin, unmount, lastFrame } = await renderWithProviders(_jsx(TestInputPrompt, { ...props, focus: true, buffer: mockBuffer }), {
|
|
3961
|
+
uiState: { isVoiceModeEnabled: true },
|
|
3962
|
+
settings: createMockSettings({
|
|
3963
|
+
experimental: { voice: { activationMode: 'push-to-talk' } },
|
|
3964
|
+
}),
|
|
3965
|
+
});
|
|
3966
|
+
// Start hold
|
|
3967
|
+
await act(async () => {
|
|
3968
|
+
stdin.write(' ');
|
|
3969
|
+
stdin.write(' ');
|
|
3970
|
+
});
|
|
3971
|
+
// Use a short interval in waitFor to prevent advancing fake timers past the 300ms RELEASE_DELAY_MS
|
|
3972
|
+
await waitFor(() => {
|
|
3973
|
+
expect(lastFrame()).toContain('🎙️ Listening...');
|
|
3974
|
+
}, { interval: 10 });
|
|
3975
|
+
// Simulate heartbeat (held key) - send space first to reset timer, then advance
|
|
3976
|
+
await act(async () => {
|
|
3977
|
+
stdin.write(' ');
|
|
3978
|
+
vi.advanceTimersByTime(100);
|
|
3979
|
+
});
|
|
3980
|
+
expect(lastFrame()).toContain('🎙️ Listening...');
|
|
3981
|
+
// Stop heartbeat (release)
|
|
3982
|
+
await act(async () => {
|
|
3983
|
+
vi.advanceTimersByTime(400); // Past RELEASE_DELAY_MS
|
|
3984
|
+
});
|
|
3985
|
+
await waitFor(() => {
|
|
3986
|
+
expect(lastFrame()).not.toContain('🎙️ Listening...');
|
|
3987
|
+
});
|
|
3988
|
+
unmount();
|
|
3989
|
+
});
|
|
3990
|
+
it('should cancel hold state if non-space key is pressed after first space', async () => {
|
|
3991
|
+
const { stdin, unmount } = await renderWithProviders(_jsx(TestInputPrompt, { ...props, focus: true, buffer: mockBuffer }), {
|
|
3992
|
+
uiState: { isVoiceModeEnabled: true },
|
|
3993
|
+
settings: createMockSettings({
|
|
3994
|
+
experimental: { voice: { activationMode: 'push-to-talk' } },
|
|
3995
|
+
}),
|
|
3996
|
+
});
|
|
3997
|
+
// First space
|
|
3998
|
+
await act(async () => {
|
|
3999
|
+
stdin.write(' ');
|
|
4000
|
+
});
|
|
4001
|
+
// Type 'a'
|
|
4002
|
+
await act(async () => {
|
|
4003
|
+
stdin.write('a');
|
|
4004
|
+
});
|
|
4005
|
+
// Should NOT start recording on next space even if fast
|
|
4006
|
+
await act(async () => {
|
|
4007
|
+
stdin.write(' ');
|
|
4008
|
+
});
|
|
4009
|
+
expect(mockBuffer.insert).toHaveBeenCalledTimes(2); // Two spaces inserted
|
|
4010
|
+
expect(mockBuffer.handleInput).toHaveBeenCalledWith(expect.objectContaining({ name: 'a' }));
|
|
4011
|
+
unmount();
|
|
4012
|
+
});
|
|
4013
|
+
});
|
|
4014
|
+
});
|
|
3726
4015
|
});
|
|
3727
4016
|
function clean(str) {
|
|
3728
4017
|
if (!str)
|