From 92c99d7873ad78ad9046995474a8fcc1dd58e674 Mon Sep 17 00:00:00 2001 From: Akhil Appana Date: Thu, 18 Sep 2025 11:54:09 -0700 Subject: [PATCH] refactor(ui): extract QueuedMessageDisplay into separate component (#8374) Co-authored-by: Abhi <43648792+abhipatel12@users.noreply.github.com> --- .../cli/src/ui/components/Composer.test.tsx | 42 +++++----- packages/cli/src/ui/components/Composer.tsx | 31 +------- .../components/QueuedMessageDisplay.test.tsx | 76 +++++++++++++++++++ .../ui/components/QueuedMessageDisplay.tsx | 47 ++++++++++++ 4 files changed, 144 insertions(+), 52 deletions(-) create mode 100644 packages/cli/src/ui/components/QueuedMessageDisplay.test.tsx create mode 100644 packages/cli/src/ui/components/QueuedMessageDisplay.tsx diff --git a/packages/cli/src/ui/components/Composer.test.tsx b/packages/cli/src/ui/components/Composer.test.tsx index c9b7dd0a52..6d1f5372c5 100644 --- a/packages/cli/src/ui/components/Composer.test.tsx +++ b/packages/cli/src/ui/components/Composer.test.tsx @@ -65,6 +65,21 @@ vi.mock('./ShowMoreLines.js', () => ({ ShowMoreLines: () => ShowMoreLines, })); +vi.mock('./QueuedMessageDisplay.js', () => ({ + QueuedMessageDisplay: ({ messageQueue }: { messageQueue: string[] }) => { + if (messageQueue.length === 0) { + return null; + } + return ( + <> + {messageQueue.map((message, index) => ( + {message} + ))} + + ); + }, +})); + // Mock contexts vi.mock('../contexts/OverflowContext.js', () => ({ OverflowProvider: ({ children }: { children: React.ReactNode }) => children, @@ -289,36 +304,17 @@ describe('Composer', () => { expect(output).toContain('Third queued message'); }); - it('shows overflow indicator when more than 3 messages are queued', () => { - const uiState = createMockUIState({ - messageQueue: [ - 'Message 1', - 'Message 2', - 'Message 3', - 'Message 4', - 'Message 5', - ], - }); - - const { lastFrame } = renderComposer(uiState); - - const output = lastFrame(); - expect(output).toContain('Message 1'); - expect(output).toContain('Message 2'); - expect(output).toContain('Message 3'); - expect(output).toContain('... (+2 more)'); - }); - - it('does not display message queue section when empty', () => { + it('renders QueuedMessageDisplay with empty message queue', () => { const uiState = createMockUIState({ messageQueue: [], }); const { lastFrame } = renderComposer(uiState); - // Should not contain queued message indicators + // The component should render but return null for empty queue + // This test verifies that the component receives the correct prop const output = lastFrame(); - expect(output).not.toContain('more)'); + expect(output).toContain('InputPrompt'); // Verify basic Composer rendering }); }); diff --git a/packages/cli/src/ui/components/Composer.tsx b/packages/cli/src/ui/components/Composer.tsx index db7255afb0..37c095c6d1 100644 --- a/packages/cli/src/ui/components/Composer.tsx +++ b/packages/cli/src/ui/components/Composer.tsx @@ -14,6 +14,7 @@ import { DetailedMessagesDisplay } from './DetailedMessagesDisplay.js'; import { InputPrompt, calculatePromptWidths } from './InputPrompt.js'; import { Footer, type FooterProps } from './Footer.js'; import { ShowMoreLines } from './ShowMoreLines.js'; +import { QueuedMessageDisplay } from './QueuedMessageDisplay.js'; import { OverflowProvider } from '../contexts/OverflowContext.js'; import { theme } from '../semantic-colors.js'; import { isNarrowWidth } from '../utils/isNarrowWidth.js'; @@ -27,8 +28,6 @@ import { ApprovalMode } from '@google/gemini-cli-core'; import { StreamingState } from '../types.js'; import { ConfigInitDisplay } from '../components/ConfigInitDisplay.js'; -const MAX_DISPLAYED_QUEUED_MESSAGES = 3; - export const Composer = () => { const config = useConfig(); const settings = useSettings(); @@ -89,33 +88,7 @@ export const Composer = () => { {!uiState.isConfigInitialized && } - {uiState.messageQueue.length > 0 && ( - - {uiState.messageQueue - .slice(0, MAX_DISPLAYED_QUEUED_MESSAGES) - .map((message, index) => { - const preview = message.replace(/\s+/g, ' '); - - return ( - - - {preview} - - - ); - })} - {uiState.messageQueue.length > MAX_DISPLAYED_QUEUED_MESSAGES && ( - - - ... (+ - {uiState.messageQueue.length - - MAX_DISPLAYED_QUEUED_MESSAGES}{' '} - more) - - - )} - - )} + { + it('renders nothing when message queue is empty', () => { + const { lastFrame } = render(); + + expect(lastFrame()).toBe(''); + }); + + it('displays single queued message', () => { + const { lastFrame } = render( + , + ); + + const output = lastFrame(); + expect(output).toContain('First message'); + }); + + it('displays multiple queued messages', () => { + const messageQueue = [ + 'First queued message', + 'Second queued message', + 'Third queued message', + ]; + + const { lastFrame } = render( + , + ); + + const output = lastFrame(); + expect(output).toContain('First queued message'); + expect(output).toContain('Second queued message'); + expect(output).toContain('Third queued message'); + }); + + it('shows overflow indicator when more than 3 messages are queued', () => { + const messageQueue = [ + 'Message 1', + 'Message 2', + 'Message 3', + 'Message 4', + 'Message 5', + ]; + + const { lastFrame } = render( + , + ); + + const output = lastFrame(); + expect(output).toContain('Message 1'); + expect(output).toContain('Message 2'); + expect(output).toContain('Message 3'); + expect(output).toContain('... (+2 more)'); + expect(output).not.toContain('Message 4'); + expect(output).not.toContain('Message 5'); + }); + + it('normalizes whitespace in messages', () => { + const messageQueue = ['Message with\tmultiple\n whitespace']; + + const { lastFrame } = render( + , + ); + + const output = lastFrame(); + expect(output).toContain('Message with multiple whitespace'); + }); +}); diff --git a/packages/cli/src/ui/components/QueuedMessageDisplay.tsx b/packages/cli/src/ui/components/QueuedMessageDisplay.tsx new file mode 100644 index 0000000000..a42e9feab1 --- /dev/null +++ b/packages/cli/src/ui/components/QueuedMessageDisplay.tsx @@ -0,0 +1,47 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Box, Text } from 'ink'; + +const MAX_DISPLAYED_QUEUED_MESSAGES = 3; + +export interface QueuedMessageDisplayProps { + messageQueue: string[]; +} + +export const QueuedMessageDisplay = ({ + messageQueue, +}: QueuedMessageDisplayProps) => { + if (messageQueue.length === 0) { + return null; + } + + return ( + + {messageQueue + .slice(0, MAX_DISPLAYED_QUEUED_MESSAGES) + .map((message, index) => { + const preview = message.replace(/\s+/g, ' '); + + return ( + + + {preview} + + + ); + })} + {messageQueue.length > MAX_DISPLAYED_QUEUED_MESSAGES && ( + + + ... (+ + {messageQueue.length - MAX_DISPLAYED_QUEUED_MESSAGES} more) + + + )} + + ); +};