fix(ui): preventing empty history items from being added (#19014)

This commit is contained in:
Dev Randalpura
2026-02-18 12:53:06 -08:00
committed by GitHub
parent 758d419e33
commit 3099df1b7c
3 changed files with 98 additions and 11 deletions

View File

@@ -46,6 +46,7 @@ import type { SlashCommandProcessorResult } from '../types.js';
import { MessageType, StreamingState } from '../types.js';
import type { LoadedSettings } from '../../config/settings.js';
import { findLastSafeSplitPoint } from '../utils/markdownUtilities.js';
// --- MOCKS ---
const mockSendMessageStream = vi
@@ -3320,4 +3321,88 @@ describe('useGeminiStream', () => {
});
});
});
describe('Stream Splitting', () => {
it('should not add empty history item when splitting message results in empty or whitespace-only beforeText', async () => {
// Mock split point to always be 0, causing beforeText to be empty
vi.mocked(findLastSafeSplitPoint).mockReturnValue(0);
mockSendMessageStream.mockReturnValue(
(async function* () {
yield { type: ServerGeminiEventType.Content, value: 'test content' };
})(),
);
const { result } = renderTestHook();
await act(async () => {
await result.current.submitQuery('user query');
});
await waitFor(() => {
// We expect the stream to be processed.
// Since beforeText is empty (0 split), addItem should NOT be called for it.
// addItem IS called for the user query "user query".
});
// Check addItem calls.
// It should be called for user query and for the content.
expect(mockAddItem).toHaveBeenCalledTimes(2);
expect(mockAddItem).toHaveBeenCalledWith(
expect.objectContaining({ type: 'user', text: 'user query' }),
expect.any(Number),
);
expect(mockAddItem).toHaveBeenLastCalledWith(
expect.objectContaining({
type: 'gemini_content',
text: 'test content',
}),
expect.any(Number),
);
// Verify that pendingHistoryItem is empty after (afterText).
expect(result.current.pendingHistoryItems.length).toEqual(0);
// Reset mock
vi.mocked(findLastSafeSplitPoint).mockReset();
vi.mocked(findLastSafeSplitPoint).mockImplementation(
(s: string) => s.length,
);
});
it('should add whitespace-only history item when splitting message', async () => {
// Input: " content"
// Split at 3 -> before: " ", after: "content"
vi.mocked(findLastSafeSplitPoint).mockReturnValue(3);
mockSendMessageStream.mockReturnValue(
(async function* () {
yield { type: ServerGeminiEventType.Content, value: ' content' };
})(),
);
const { result } = renderTestHook();
await act(async () => {
await result.current.submitQuery('user query');
});
await waitFor(() => {});
expect(mockAddItem).toHaveBeenCalledTimes(3);
expect(mockAddItem).toHaveBeenCalledWith(
expect.objectContaining({ type: 'user', text: 'user query' }),
expect.any(Number),
);
expect(mockAddItem).toHaveBeenLastCalledWith(
expect.objectContaining({
type: 'gemini_content',
text: 'content',
}),
expect.any(Number),
);
expect(result.current.pendingHistoryItems.length).toEqual(0);
});
});
});

View File

@@ -819,16 +819,18 @@ export const useGeminiStream = (
// broken up so that there are more "statically" rendered.
const beforeText = newGeminiMessageBuffer.substring(0, splitPoint);
const afterText = newGeminiMessageBuffer.substring(splitPoint);
addItem(
{
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
type: pendingHistoryItemRef.current?.type as
| 'gemini'
| 'gemini_content',
text: beforeText,
},
userMessageTimestamp,
);
if (beforeText.length > 0) {
addItem(
{
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
type: pendingHistoryItemRef.current?.type as
| 'gemini'
| 'gemini_content',
text: beforeText,
},
userMessageTimestamp,
);
}
setPendingHistoryItem({ type: 'gemini_content', text: afterText });
newGeminiMessageBuffer = afterText;
}

View File

@@ -337,7 +337,7 @@ const RenderCodeBlockInternal: React.FC<RenderCodeBlockProps> = ({
const RESERVED_LINES = 2; // Lines reserved for the message itself and potential padding
// When not in alternate buffer mode we need to be careful that we don't
// trigger flicker when the pending code is to long to fit in the terminal
// trigger flicker when the pending code is too long to fit in the terminal
if (
!isAlternateBuffer &&
isPending &&