feat(ui): automatically dismiss ephemeral /btw display on input typing

By intercepting text input when the `/btw` query results are
visible (but not actively streaming), we can dismiss the ephemeral
BtwDisplay before the new text wraps to the next line. This fixes
the UI jumpiness and "ghost space" scrolling that occurs when the
terminal recalculates the tall rendered height of the previous
query dynamically. Also, includes test updates to mock the
spinner to eliminate `act(...)` asynchronous test warnings.
This commit is contained in:
Mahima Shanware
2026-03-27 23:41:11 +00:00
committed by Mahima Shanware
parent ee1bc7e209
commit 55a7a22471
3 changed files with 86 additions and 0 deletions
@@ -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: () => <Text></Text>,
}));
describe('BtwDisplay', () => {
const defaultMockUiState = {
renderMarkdown: true,
@@ -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(
<InputPrompt {...props} />,
{
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(
<InputPrompt {...props} />,
{
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();
});
});
});
@@ -694,6 +694,27 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
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<InputPromptProps> = ({
isHelpDismissKey,
settings,
btwState.isActive,
btwState.isStreaming,
dismissBtw,
],
);