Inline thinking bubbles with summary/full modes (#18033)

Co-authored-by: Jacob Richman <jacob314@gmail.com>
This commit is contained in:
Dmitry Lyalin
2026-02-09 19:24:41 -08:00
committed by GitHub
parent 7a132512cf
commit d3cfbdb3b7
26 changed files with 719 additions and 26 deletions

View File

@@ -12,6 +12,7 @@ import { StreamingContext } from '../contexts/StreamingContext.js';
import { StreamingState } from '../types.js';
import { vi } from 'vitest';
import * as useTerminalSize from '../hooks/useTerminalSize.js';
import * as terminalUtils from '../utils/terminalUtils.js';
// Mock GeminiRespondingSpinner
vi.mock('./GeminiRespondingSpinner.js', () => ({
@@ -34,7 +35,12 @@ vi.mock('../hooks/useTerminalSize.js', () => ({
useTerminalSize: vi.fn(),
}));
vi.mock('../utils/terminalUtils.js', () => ({
shouldUseEmoji: vi.fn(() => true),
}));
const useTerminalSizeMock = vi.mocked(useTerminalSize.useTerminalSize);
const shouldUseEmojiMock = vi.mocked(terminalUtils.shouldUseEmoji);
const renderWithContext = (
ui: React.ReactElement,
@@ -217,12 +223,33 @@ describe('<LoadingIndicator />', () => {
const output = lastFrame();
expect(output).toBeDefined();
if (output) {
expect(output).toContain('💬');
expect(output).toContain('Thinking about something...');
expect(output).not.toContain('and other stuff.');
}
unmount();
});
it('should use ASCII fallback thought indicator when emoji is unavailable', () => {
shouldUseEmojiMock.mockReturnValue(false);
const props = {
thought: {
subject: 'Thinking with fallback',
description: 'details',
},
elapsedTime: 5,
};
const { lastFrame, unmount } = renderWithContext(
<LoadingIndicator {...props} />,
StreamingState.Responding,
);
const output = lastFrame();
expect(output).toContain('o Thinking with fallback');
expect(output).not.toContain('💬');
shouldUseEmojiMock.mockReturnValue(true);
unmount();
});
it('should prioritize thought.subject over currentLoadingPhrase', () => {
const props = {
thought: {
@@ -237,11 +264,24 @@ describe('<LoadingIndicator />', () => {
StreamingState.Responding,
);
const output = lastFrame();
expect(output).toContain('💬');
expect(output).toContain('This should be displayed');
expect(output).not.toContain('This should not be displayed');
unmount();
});
it('should not display thought icon for non-thought loading phrases', () => {
const { lastFrame, unmount } = renderWithContext(
<LoadingIndicator
currentLoadingPhrase="some random tip..."
elapsedTime={3}
/>,
StreamingState.Responding,
);
expect(lastFrame()).not.toContain('💬');
unmount();
});
it('should truncate long primary text instead of wrapping', () => {
const { lastFrame, unmount } = renderWithContext(
<LoadingIndicator