@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.
Files changed (134) hide show
  1. package/dist/package.json +3 -3
  2. package/dist/src/acp/commands/restore.test.d.ts +6 -0
  3. package/dist/src/acp/commands/restore.test.js +179 -0
  4. package/dist/src/acp/commands/restore.test.js.map +1 -0
  5. package/dist/src/commands/mcp/list.js +14 -4
  6. package/dist/src/commands/mcp/list.js.map +1 -1
  7. package/dist/src/commands/mcp/list.test.js +48 -0
  8. package/dist/src/commands/mcp/list.test.js.map +1 -1
  9. package/dist/src/config/config.d.ts +1 -0
  10. package/dist/src/config/config.js +41 -7
  11. package/dist/src/config/config.js.map +1 -1
  12. package/dist/src/config/config.test.js +126 -71
  13. package/dist/src/config/config.test.js.map +1 -1
  14. package/dist/src/config/settings-validation.js +24 -6
  15. package/dist/src/config/settings-validation.js.map +1 -1
  16. package/dist/src/config/settings-validation.test.js +96 -0
  17. package/dist/src/config/settings-validation.test.js.map +1 -1
  18. package/dist/src/config/settings.js +30 -60
  19. package/dist/src/config/settings.js.map +1 -1
  20. package/dist/src/config/settings.test.js +78 -0
  21. package/dist/src/config/settings.test.js.map +1 -1
  22. package/dist/src/config/settingsSchema.d.ts +101 -0
  23. package/dist/src/config/settingsSchema.js +98 -0
  24. package/dist/src/config/settingsSchema.js.map +1 -1
  25. package/dist/src/gemini.d.ts +1 -1
  26. package/dist/src/gemini.js +24 -17
  27. package/dist/src/gemini.js.map +1 -1
  28. package/dist/src/gemini.test.js +42 -4
  29. package/dist/src/gemini.test.js.map +1 -1
  30. package/dist/src/gemini_cleanup.test.js +103 -1
  31. package/dist/src/gemini_cleanup.test.js.map +1 -1
  32. package/dist/src/generated/git-commit.d.ts +2 -2
  33. package/dist/src/generated/git-commit.js +2 -2
  34. package/dist/src/services/BuiltinCommandLoader.js +2 -0
  35. package/dist/src/services/BuiltinCommandLoader.js.map +1 -1
  36. package/dist/src/services/BuiltinCommandLoader.test.js +2 -0
  37. package/dist/src/services/BuiltinCommandLoader.test.js.map +1 -1
  38. package/dist/src/services/prompt-processors/shellProcessor.test.js +1 -0
  39. package/dist/src/services/prompt-processors/shellProcessor.test.js.map +1 -1
  40. package/dist/src/test-utils/mockConfig.js +1 -0
  41. package/dist/src/test-utils/mockConfig.js.map +1 -1
  42. package/dist/src/test-utils/render.js +3 -0
  43. package/dist/src/test-utils/render.js.map +1 -1
  44. package/dist/src/ui/AppContainer.js +19 -0
  45. package/dist/src/ui/AppContainer.js.map +1 -1
  46. package/dist/src/ui/commands/types.d.ts +2 -1
  47. package/dist/src/ui/commands/types.js.map +1 -1
  48. package/dist/src/ui/commands/voiceCommand.d.ts +7 -0
  49. package/dist/src/ui/commands/voiceCommand.js +29 -0
  50. package/dist/src/ui/commands/voiceCommand.js.map +1 -0
  51. package/dist/src/ui/components/AsciiArt.d.ts +6 -6
  52. package/dist/src/ui/components/AsciiArt.js +6 -6
  53. package/dist/src/ui/components/DialogManager.js +4 -0
  54. package/dist/src/ui/components/DialogManager.js.map +1 -1
  55. package/dist/src/ui/components/InputPrompt.js +34 -13
  56. package/dist/src/ui/components/InputPrompt.js.map +1 -1
  57. package/dist/src/ui/components/InputPrompt.test.js +289 -0
  58. package/dist/src/ui/components/InputPrompt.test.js.map +1 -1
  59. package/dist/src/ui/components/ModelDialog.js +17 -5
  60. package/dist/src/ui/components/ModelDialog.js.map +1 -1
  61. package/dist/src/ui/components/ModelDialog.test.js +1 -0
  62. package/dist/src/ui/components/ModelDialog.test.js.map +1 -1
  63. package/dist/src/ui/components/SessionBrowser.test.js +1 -0
  64. package/dist/src/ui/components/SessionBrowser.test.js.map +1 -1
  65. package/dist/src/ui/components/SettingsDialog.test.js +22 -0
  66. package/dist/src/ui/components/SettingsDialog.test.js.map +1 -1
  67. package/dist/src/ui/components/VoiceModelDialog.d.ts +11 -0
  68. package/dist/src/ui/components/VoiceModelDialog.js +118 -0
  69. package/dist/src/ui/components/VoiceModelDialog.js.map +1 -0
  70. package/dist/src/ui/components/messages/HintMessage.js +2 -1
  71. package/dist/src/ui/components/messages/HintMessage.js.map +1 -1
  72. package/dist/src/ui/components/messages/HintMessage.test.d.ts +6 -0
  73. package/dist/src/ui/components/messages/HintMessage.test.js +42 -0
  74. package/dist/src/ui/components/messages/HintMessage.test.js.map +1 -0
  75. package/dist/src/ui/components/messages/UserMessage.js +2 -1
  76. package/dist/src/ui/components/messages/UserMessage.js.map +1 -1
  77. package/dist/src/ui/components/messages/UserMessage.test.js +24 -0
  78. package/dist/src/ui/components/messages/UserMessage.test.js.map +1 -1
  79. package/dist/src/ui/components/messages/UserShellMessage.js +2 -1
  80. package/dist/src/ui/components/messages/UserShellMessage.js.map +1 -1
  81. package/dist/src/ui/components/messages/UserShellMessage.test.d.ts +6 -0
  82. package/dist/src/ui/components/messages/UserShellMessage.test.js +40 -0
  83. package/dist/src/ui/components/messages/UserShellMessage.test.js.map +1 -0
  84. package/dist/src/ui/components/shared/BaseSettingsDialog.js +1 -1
  85. package/dist/src/ui/components/shared/BaseSettingsDialog.js.map +1 -1
  86. package/dist/src/ui/contexts/KeypressContext.test.js +5 -3
  87. package/dist/src/ui/contexts/KeypressContext.test.js.map +1 -1
  88. package/dist/src/ui/contexts/UIActionsContext.d.ts +3 -0
  89. package/dist/src/ui/contexts/UIActionsContext.js.map +1 -1
  90. package/dist/src/ui/contexts/UIStateContext.d.ts +2 -0
  91. package/dist/src/ui/contexts/UIStateContext.js.map +1 -1
  92. package/dist/src/ui/hooks/slashCommandProcessor.d.ts +2 -0
  93. package/dist/src/ui/hooks/slashCommandProcessor.js +4 -0
  94. package/dist/src/ui/hooks/slashCommandProcessor.js.map +1 -1
  95. package/dist/src/ui/hooks/slashCommandProcessor.test.js +2 -0
  96. package/dist/src/ui/hooks/slashCommandProcessor.test.js.map +1 -1
  97. package/dist/src/ui/hooks/useSlashCompletion.js +15 -11
  98. package/dist/src/ui/hooks/useSlashCompletion.js.map +1 -1
  99. package/dist/src/ui/hooks/useSlashCompletion.test.js +40 -0
  100. package/dist/src/ui/hooks/useSlashCompletion.test.js.map +1 -1
  101. package/dist/src/ui/hooks/useVoiceMode.d.ts +28 -0
  102. package/dist/src/ui/hooks/useVoiceMode.js +333 -0
  103. package/dist/src/ui/hooks/useVoiceMode.js.map +1 -0
  104. package/dist/src/ui/hooks/useVoiceModelCommand.d.ts +12 -0
  105. package/dist/src/ui/hooks/useVoiceModelCommand.js +21 -0
  106. package/dist/src/ui/hooks/useVoiceModelCommand.js.map +1 -0
  107. package/dist/src/ui/key/keyBindings.d.ts +1 -0
  108. package/dist/src/ui/key/keyBindings.js +7 -3
  109. package/dist/src/ui/key/keyBindings.js.map +1 -1
  110. package/dist/src/ui/noninteractive/nonInteractiveUi.js +1 -0
  111. package/dist/src/ui/noninteractive/nonInteractiveUi.js.map +1 -1
  112. package/dist/src/utils/activityLogger.js +19 -9
  113. package/dist/src/utils/activityLogger.js.map +1 -1
  114. package/dist/src/utils/activityLogger.test.js +76 -1
  115. package/dist/src/utils/activityLogger.test.js.map +1 -1
  116. package/dist/src/utils/handleAutoUpdate.js +4 -3
  117. package/dist/src/utils/handleAutoUpdate.js.map +1 -1
  118. package/dist/src/utils/handleAutoUpdate.test.js +8 -6
  119. package/dist/src/utils/handleAutoUpdate.test.js.map +1 -1
  120. package/dist/src/utils/sandbox.js +23 -10
  121. package/dist/src/utils/sandbox.js.map +1 -1
  122. package/dist/src/utils/sandbox.test.js +46 -0
  123. package/dist/src/utils/sandbox.test.js.map +1 -1
  124. package/dist/src/utils/sessionCleanup.test.js +1 -0
  125. package/dist/src/utils/sessionCleanup.test.js.map +1 -1
  126. package/dist/src/utils/sessionUtils.d.ts +4 -0
  127. package/dist/src/utils/sessionUtils.js +24 -0
  128. package/dist/src/utils/sessionUtils.js.map +1 -1
  129. package/dist/src/utils/sessionUtils.test.js +27 -0
  130. package/dist/src/utils/sessionUtils.test.js.map +1 -1
  131. package/dist/src/utils/userStartupWarnings.test.js +14 -0
  132. package/dist/src/utils/userStartupWarnings.test.js.map +1 -1
  133. package/dist/tsconfig.tsbuildinfo +1 -1
  134. 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)