diff --git a/packages/cli/src/ui/components/BtwDisplay.test.tsx b/packages/cli/src/ui/components/BtwDisplay.test.tsx index 88fe96db74..4ae5719bfe 100644 --- a/packages/cli/src/ui/components/BtwDisplay.test.tsx +++ b/packages/cli/src/ui/components/BtwDisplay.test.tsx @@ -10,6 +10,12 @@ import { BtwDisplay } from './BtwDisplay.js'; import { StreamingState } from '../types.js'; import type { UIState } from '../contexts/UIStateContext.js'; +import { Text } from 'ink'; + +vi.mock('./GeminiRespondingSpinner.js', () => ({ + GeminiRespondingSpinner: () => , +})); + describe('BtwDisplay', () => { const defaultMockUiState = { renderMarkdown: true, diff --git a/packages/cli/src/ui/components/InputPrompt.test.tsx b/packages/cli/src/ui/components/InputPrompt.test.tsx index b274d0decf..4ca133af30 100644 --- a/packages/cli/src/ui/components/InputPrompt.test.tsx +++ b/packages/cli/src/ui/components/InputPrompt.test.tsx @@ -5182,6 +5182,64 @@ describe('InputPrompt', () => { }); unmount(); }); + + it('dismisses Btw and accepts input on typing when not streaming', async () => { + const dismissBtw = vi.fn(); + const { stdin, stdout, unmount } = await renderWithProviders( + , + { + uiState: { + btwState: { + isActive: true, + query: '', + response: '', + isStreaming: false, + error: null, + }, + }, + uiActions: { dismissBtw }, + }, + ); + + await act(async () => { + stdin.write('a'); + }); + + await waitFor(() => { + expect(dismissBtw).toHaveBeenCalled(); + expect(clean(stdout.lastFrameRaw())).toContain('a'); + }); + unmount(); + }); + + it('blocks typing when Btw is streaming', async () => { + const dismissBtw = vi.fn(); + const { stdin, stdout, unmount } = await renderWithProviders( + , + { + uiState: { + btwState: { + isActive: true, + query: '', + response: '', + isStreaming: true, + error: null, + }, + }, + uiActions: { dismissBtw }, + }, + ); + + await act(async () => { + stdin.write('Z'); + }); + + await waitFor(() => { + expect(dismissBtw).not.toHaveBeenCalled(); + expect(clean(stdout.lastFrameRaw())).not.toContain('Z'); + }); + unmount(); + }); }); }); diff --git a/packages/cli/src/ui/components/InputPrompt.tsx b/packages/cli/src/ui/components/InputPrompt.tsx index 64dba15561..1ff100d0a3 100644 --- a/packages/cli/src/ui/components/InputPrompt.tsx +++ b/packages/cli/src/ui/components/InputPrompt.tsx @@ -694,6 +694,27 @@ export const InputPrompt: React.FC = ({ dismissBtw(); return true; } + + const isPrintable = + key.sequence && + !key.ctrl && + !key.cmd && + !key.alt && + key.sequence.length === 1; + + const isEditingKey = + isPrintable || + key.name === 'backspace' || + key.name === 'delete' || + key.name === 'paste'; + + if (isEditingKey) { + if (btwState.isStreaming) { + return true; + } else { + dismissBtw(); + } + } } // Reset completion suppression if the user performs any action other than @@ -1386,6 +1407,7 @@ export const InputPrompt: React.FC = ({ isHelpDismissKey, settings, btwState.isActive, + btwState.isStreaming, dismissBtw, ], );