diff --git a/packages/cli/src/ui/App.test.tsx b/packages/cli/src/ui/App.test.tsx index aad5e51211..d8883e6735 100644 --- a/packages/cli/src/ui/App.test.tsx +++ b/packages/cli/src/ui/App.test.tsx @@ -6,12 +6,19 @@ import { describe, it, expect, vi } from 'vitest'; import { render } from 'ink-testing-library'; -import { Text } from 'ink'; +import { Text, useIsScreenReaderEnabled } from 'ink'; import { App } from './App.js'; import { UIStateContext, type UIState } from './contexts/UIStateContext.js'; import { StreamingState } from './types.js'; -// Mock components to isolate App component testing +vi.mock('ink', async (importOriginal) => { + const original = await importOriginal(); + return { + ...original, + useIsScreenReaderEnabled: vi.fn(), + }; +}); + vi.mock('./components/MainContent.js', () => ({ MainContent: () => MainContent, })); @@ -32,6 +39,10 @@ vi.mock('./components/QuittingDisplay.js', () => ({ QuittingDisplay: () => Quitting..., })); +vi.mock('./components/Footer.js', () => ({ + Footer: () => Footer, +})); + describe('App', () => { const mockUIState: Partial = { streamingState: StreamingState.Idle, @@ -122,4 +133,30 @@ describe('App', () => { expect(lastFrame()).toContain('Press Ctrl+D again to exit.'); }); + + it('should render ScreenReaderAppLayout when screen reader is enabled', () => { + (useIsScreenReaderEnabled as vi.Mock).mockReturnValue(true); + + const { lastFrame } = render( + + + , + ); + + expect(lastFrame()).toContain( + 'Notifications\nFooter\nMainContent\nComposer', + ); + }); + + it('should render DefaultAppLayout when screen reader is not enabled', () => { + (useIsScreenReaderEnabled as vi.Mock).mockReturnValue(false); + + const { lastFrame } = render( + + + , + ); + + expect(lastFrame()).toContain('MainContent\nNotifications\nComposer'); + }); }); diff --git a/packages/cli/src/ui/App.tsx b/packages/cli/src/ui/App.tsx index 8a582be7f7..54684a8c2c 100644 --- a/packages/cli/src/ui/App.tsx +++ b/packages/cli/src/ui/App.tsx @@ -4,18 +4,16 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Box, Text } from 'ink'; -import { StreamingContext } from './contexts/StreamingContext.js'; -import { Notifications } from './components/Notifications.js'; -import { MainContent } from './components/MainContent.js'; -import { DialogManager } from './components/DialogManager.js'; -import { Composer } from './components/Composer.js'; +import { useIsScreenReaderEnabled } from 'ink'; import { useUIState } from './contexts/UIStateContext.js'; +import { StreamingContext } from './contexts/StreamingContext.js'; import { QuittingDisplay } from './components/QuittingDisplay.js'; -import { theme } from './semantic-colors.js'; +import { ScreenReaderAppLayout } from './layouts/ScreenReaderAppLayout.js'; +import { DefaultAppLayout } from './layouts/DefaultAppLayout.js'; export const App = () => { const uiState = useUIState(); + const isScreenReaderEnabled = useIsScreenReaderEnabled(); if (uiState.quittingMessages) { return ; @@ -23,35 +21,7 @@ export const App = () => { return ( - - - - - - - {uiState.dialogsVisible ? ( - - ) : ( - - )} - - {uiState.dialogsVisible && uiState.ctrlCPressedOnce && ( - - - Press Ctrl+C again to exit. - - - )} - - {uiState.dialogsVisible && uiState.ctrlDPressedOnce && ( - - - Press Ctrl+D again to exit. - - - )} - - + {isScreenReaderEnabled ? : } ); }; diff --git a/packages/cli/src/ui/components/Composer.tsx b/packages/cli/src/ui/components/Composer.tsx index 9acc49a44d..2c5d0f16b7 100644 --- a/packages/cli/src/ui/components/Composer.tsx +++ b/packages/cli/src/ui/components/Composer.tsx @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Box, Text } from 'ink'; +import { Box, Text, useIsScreenReaderEnabled } from 'ink'; import { useMemo } from 'react'; import { LoadingIndicator } from './LoadingIndicator.js'; import { ContextSummaryDisplay } from './ContextSummaryDisplay.js'; @@ -12,7 +12,7 @@ import { AutoAcceptIndicator } from './AutoAcceptIndicator.js'; import { ShellModeIndicator } from './ShellModeIndicator.js'; import { DetailedMessagesDisplay } from './DetailedMessagesDisplay.js'; import { InputPrompt, calculatePromptWidths } from './InputPrompt.js'; -import { Footer, type FooterProps } from './Footer.js'; +import { Footer } from './Footer.js'; import { ShowMoreLines } from './ShowMoreLines.js'; import { QueuedMessageDisplay } from './QueuedMessageDisplay.js'; import { OverflowProvider } from '../contexts/OverflowContext.js'; @@ -30,9 +30,10 @@ import { ConfigInitDisplay } from '../components/ConfigInitDisplay.js'; export const Composer = () => { const config = useConfig(); const settings = useSettings(); + const isScreenReaderEnabled = useIsScreenReaderEnabled(); const uiState = useUIState(); const uiActions = useUIActions(); - const { vimEnabled, vimMode } = useVimMode(); + const { vimEnabled } = useVimMode(); const terminalWidth = process.stdout.columns; const isNarrow = isNarrowWidth(terminalWidth); const debugConsoleMaxHeight = Math.floor(Math.max(terminalWidth * 0.2, 5)); @@ -45,26 +46,6 @@ export const Composer = () => { [uiState.terminalWidth], ); - // Build footer props from context values - const footerProps: Omit = { - model: config.getModel(), - targetDir: config.getTargetDir(), - debugMode: config.getDebugMode(), - branchName: uiState.branchName, - debugMessage: uiState.debugMessage, - corgiMode: uiState.corgiMode, - errorCount: uiState.errorCount, - showErrorDetails: uiState.showErrorDetails, - showMemoryUsage: - config.getDebugMode() || settings.merged.ui?.showMemoryUsage || false, - promptTokenCount: uiState.sessionStats.lastPromptTokenCount, - nightly: uiState.nightly, - isTrustedFolder: uiState.isTrustedFolder, - hideCWD: settings.merged.ui?.footer?.hideCWD || false, - hideSandboxStatus: settings.merged.ui?.footer?.hideSandboxStatus || false, - hideModelInfo: settings.merged.ui?.footer?.hideModelInfo || false, - }; - return ( {!uiState.embeddedShellFocused && ( @@ -176,9 +157,7 @@ export const Composer = () => { /> )} - {!settings.merged.ui?.hideFooter && ( -