From fe5bb6694e6afc1602109b4abebcebcf59567666 Mon Sep 17 00:00:00 2001 From: christine betts Date: Thu, 28 Aug 2025 20:52:14 +0000 Subject: [PATCH] Screen reader updates (#7307) --- package-lock.json | 11 +++++----- packages/cli/package.json | 2 +- packages/cli/src/config/config.ts | 6 +++--- .../ui/components/GeminiRespondingSpinner.tsx | 20 +++++++++++++++---- .../cli/src/ui/components/InputPrompt.tsx | 2 +- .../messages/CompressionMessage.tsx | 2 +- .../ui/components/messages/DiffRenderer.tsx | 14 ++++++++++++- .../ui/components/messages/GeminiMessage.tsx | 2 +- .../ui/components/messages/ToolMessage.tsx | 12 +++++++---- .../ui/components/messages/UserMessage.tsx | 2 +- .../components/shared/RadioButtonSelect.tsx | 7 +++++-- packages/cli/src/ui/constants.ts | 4 ---- packages/cli/src/ui/textConstants.ts | 13 ++++++++++++ 13 files changed, 69 insertions(+), 28 deletions(-) create mode 100644 packages/cli/src/ui/textConstants.ts diff --git a/package-lock.json b/package-lock.json index 59498349e3..0bd6785408 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8085,9 +8085,9 @@ } }, "node_modules/ink": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ink/-/ink-6.2.2.tgz", - "integrity": "sha512-LN1f+/D8KKqMqRux08fIfA9wsEAJ9Bu9CiI3L6ih7bnqNSDUXT/JVJ0rUIc4NkjPiPaeI3BVNREcLYLz9ePSEg==", + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ink/-/ink-6.2.3.tgz", + "integrity": "sha512-fQkfEJjKbLXIcVWEE3MvpYSnwtbbmRsmeNDNz1pIuOFlwE+UF2gsy228J36OXKZGWJWZJKUigphBSqCNMcARtg==", "license": "MIT", "dependencies": { "@alcalzone/ansi-tokenize": "^0.2.0", @@ -8104,7 +8104,6 @@ "is-in-ci": "^2.0.0", "patch-console": "^2.0.0", "react-reconciler": "^0.32.0", - "scheduler": "^0.26.0", "signal-exit": "^3.0.7", "slice-ansi": "^7.1.0", "stack-utils": "^2.0.6", @@ -14519,7 +14518,7 @@ "dotenv": "^17.1.0", "glob": "^10.4.1", "highlight.js": "^11.11.1", - "ink": "^6.1.1", + "ink": "^6.2.3", "ink-gradient": "^3.0.0", "ink-spinner": "^5.0.0", "lodash-es": "^4.17.21", @@ -14529,6 +14528,7 @@ "react": "^19.1.0", "read-package-up": "^11.0.0", "shell-quote": "^1.8.3", + "simple-git": "^3.28.0", "string-width": "^7.1.0", "strip-ansi": "^7.1.0", "strip-json-comments": "^3.1.1", @@ -14692,6 +14692,7 @@ "version": "0.2.2", "dependencies": { "@google/genai": "1.13.0", + "@lvce-editor/ripgrep": "^1.6.0", "@modelcontextprotocol/sdk": "^1.11.0", "@opentelemetry/api": "^1.9.0", "@opentelemetry/exporter-logs-otlp-grpc": "^0.203.0", diff --git a/packages/cli/package.json b/packages/cli/package.json index 55e467ab94..a4f4ac0506 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -38,7 +38,7 @@ "dotenv": "^17.1.0", "glob": "^10.4.1", "highlight.js": "^11.11.1", - "ink": "^6.1.1", + "ink": "^6.2.3", "ink-gradient": "^3.0.0", "ink-spinner": "^5.0.0", "lodash-es": "^4.17.21", diff --git a/packages/cli/src/config/config.ts b/packages/cli/src/config/config.ts index 2cbeb00975..b36da1a0af 100755 --- a/packages/cli/src/config/config.ts +++ b/packages/cli/src/config/config.ts @@ -483,10 +483,10 @@ export async function loadCliConfig( } const sandboxConfig = await loadSandboxConfig(settings, argv); - - // The screen reader argument takes precedence over the accessibility setting. const screenReader = - argv.screenReader ?? settings.ui?.accessibility?.screenReader ?? false; + argv.screenReader !== undefined + ? argv.screenReader + : (settings.ui?.accessibility?.screenReader ?? false); return new Config({ sessionId, embeddingModel: DEFAULT_GEMINI_EMBEDDING_MODEL, diff --git a/packages/cli/src/ui/components/GeminiRespondingSpinner.tsx b/packages/cli/src/ui/components/GeminiRespondingSpinner.tsx index f4ed235f8f..caf774e2a8 100644 --- a/packages/cli/src/ui/components/GeminiRespondingSpinner.tsx +++ b/packages/cli/src/ui/components/GeminiRespondingSpinner.tsx @@ -5,11 +5,15 @@ */ import type React from 'react'; -import { Text } from 'ink'; +import { Text, useIsScreenReaderEnabled } from 'ink'; import Spinner from 'ink-spinner'; import type { SpinnerName } from 'cli-spinners'; import { useStreamingContext } from '../contexts/StreamingContext.js'; import { StreamingState } from '../types.js'; +import { + SCREEN_READER_LOADING, + SCREEN_READER_RESPONDING, +} from '../textConstants.js'; interface GeminiRespondingSpinnerProps { /** @@ -24,11 +28,19 @@ export const GeminiRespondingSpinner: React.FC< GeminiRespondingSpinnerProps > = ({ nonRespondingDisplay, spinnerType = 'dots' }) => { const streamingState = useStreamingContext(); - + const isScreenReaderEnabled = useIsScreenReaderEnabled(); if (streamingState === StreamingState.Responding) { - return ; + return isScreenReaderEnabled ? ( + {SCREEN_READER_RESPONDING} + ) : ( + + ); } else if (nonRespondingDisplay) { - return {nonRespondingDisplay}; + return isScreenReaderEnabled ? ( + {SCREEN_READER_LOADING} + ) : ( + {nonRespondingDisplay} + ); } return null; }; diff --git a/packages/cli/src/ui/components/InputPrompt.tsx b/packages/cli/src/ui/components/InputPrompt.tsx index 09897885a2..59516489b8 100644 --- a/packages/cli/src/ui/components/InputPrompt.tsx +++ b/packages/cli/src/ui/components/InputPrompt.tsx @@ -29,7 +29,7 @@ import { cleanupOldClipboardImages, } from '../utils/clipboardUtils.js'; import * as path from 'node:path'; -import { SCREEN_READER_USER_PREFIX } from '../constants.js'; +import { SCREEN_READER_USER_PREFIX } from '../textConstants.js'; export interface InputPromptProps { buffer: TextBuffer; diff --git a/packages/cli/src/ui/components/messages/CompressionMessage.tsx b/packages/cli/src/ui/components/messages/CompressionMessage.tsx index 993ec3039b..7663172e23 100644 --- a/packages/cli/src/ui/components/messages/CompressionMessage.tsx +++ b/packages/cli/src/ui/components/messages/CompressionMessage.tsx @@ -9,7 +9,7 @@ import { Box, Text } from 'ink'; import type { CompressionProps } from '../../types.js'; import Spinner from 'ink-spinner'; import { Colors } from '../../colors.js'; -import { SCREEN_READER_MODEL_PREFIX } from '../../constants.js'; +import { SCREEN_READER_MODEL_PREFIX } from '../../textConstants.js'; export interface CompressionDisplayProps { compression: CompressionProps; diff --git a/packages/cli/src/ui/components/messages/DiffRenderer.tsx b/packages/cli/src/ui/components/messages/DiffRenderer.tsx index 33a566b984..f855c97f56 100644 --- a/packages/cli/src/ui/components/messages/DiffRenderer.tsx +++ b/packages/cli/src/ui/components/messages/DiffRenderer.tsx @@ -5,7 +5,7 @@ */ import type React from 'react'; -import { Box, Text } from 'ink'; +import { Box, Text, useIsScreenReaderEnabled } from 'ink'; import { Colors } from '../../colors.js'; import crypto from 'node:crypto'; import { colorizeCode, colorizeLine } from '../../utils/CodeColorizer.js'; @@ -107,6 +107,7 @@ export const DiffRenderer: React.FC = ({ terminalWidth, theme, }) => { + const screenReaderEnabled = useIsScreenReaderEnabled(); if (!diffContent || typeof diffContent !== 'string') { return No diff content.; } @@ -120,6 +121,17 @@ export const DiffRenderer: React.FC = ({ ); } + if (screenReaderEnabled) { + return ( + + {parsedLines.map((line, index) => ( + + {line.type}: {line.content} + + ))} + + ); + } // Check if the diff represents a new file (only additions and header lines) const isNewFile = parsedLines.every( diff --git a/packages/cli/src/ui/components/messages/GeminiMessage.tsx b/packages/cli/src/ui/components/messages/GeminiMessage.tsx index 3476a44d21..9473c12885 100644 --- a/packages/cli/src/ui/components/messages/GeminiMessage.tsx +++ b/packages/cli/src/ui/components/messages/GeminiMessage.tsx @@ -8,7 +8,7 @@ import type React from 'react'; import { Text, Box } from 'ink'; import { MarkdownDisplay } from '../../utils/MarkdownDisplay.js'; import { Colors } from '../../colors.js'; -import { SCREEN_READER_MODEL_PREFIX } from '../../constants.js'; +import { SCREEN_READER_MODEL_PREFIX } from '../../textConstants.js'; interface GeminiMessageProps { text: string; diff --git a/packages/cli/src/ui/components/messages/ToolMessage.tsx b/packages/cli/src/ui/components/messages/ToolMessage.tsx index 203ab9867f..c4e5b6baf4 100644 --- a/packages/cli/src/ui/components/messages/ToolMessage.tsx +++ b/packages/cli/src/ui/components/messages/ToolMessage.tsx @@ -129,18 +129,22 @@ const ToolStatusIndicator: React.FC = ({ /> )} {status === ToolCallStatus.Success && ( - {TOOL_STATUS.SUCCESS} + + {TOOL_STATUS.SUCCESS} + )} {status === ToolCallStatus.Confirming && ( - {TOOL_STATUS.CONFIRMING} + + {TOOL_STATUS.CONFIRMING} + )} {status === ToolCallStatus.Canceled && ( - + {TOOL_STATUS.CANCELED} )} {status === ToolCallStatus.Error && ( - + {TOOL_STATUS.ERROR} )} diff --git a/packages/cli/src/ui/components/messages/UserMessage.tsx b/packages/cli/src/ui/components/messages/UserMessage.tsx index a05964f344..4f279a747f 100644 --- a/packages/cli/src/ui/components/messages/UserMessage.tsx +++ b/packages/cli/src/ui/components/messages/UserMessage.tsx @@ -7,7 +7,7 @@ import type React from 'react'; import { Text, Box } from 'ink'; import { Colors } from '../../colors.js'; -import { SCREEN_READER_USER_PREFIX } from '../../constants.js'; +import { SCREEN_READER_USER_PREFIX } from '../../textConstants.js'; import { isSlashCommand as checkIsSlashCommand } from '../../utils/commandUtils.js'; interface UserMessageProps { diff --git a/packages/cli/src/ui/components/shared/RadioButtonSelect.tsx b/packages/cli/src/ui/components/shared/RadioButtonSelect.tsx index 921c954a1e..719d263b96 100644 --- a/packages/cli/src/ui/components/shared/RadioButtonSelect.tsx +++ b/packages/cli/src/ui/components/shared/RadioButtonSelect.tsx @@ -65,7 +65,6 @@ export function RadioButtonSelect({ const [scrollOffset, setScrollOffset] = useState(0); const [numberInput, setNumberInput] = useState(''); const numberInputTimer = useRef(null); - useEffect(() => { const newScrollOffset = Math.max( 0, @@ -195,7 +194,10 @@ export function RadioButtonSelect({ return ( - + {isSelected ? '●' : ' '} @@ -203,6 +205,7 @@ export function RadioButtonSelect({ marginRight={1} flexShrink={0} minWidth={itemNumberText.length} + aria-state={{ checked: isSelected }} > {itemNumberText} diff --git a/packages/cli/src/ui/constants.ts b/packages/cli/src/ui/constants.ts index 38a0f16248..9c95a29d10 100644 --- a/packages/cli/src/ui/constants.ts +++ b/packages/cli/src/ui/constants.ts @@ -16,10 +16,6 @@ export const STREAM_DEBOUNCE_MS = 100; export const SHELL_COMMAND_NAME = 'Shell Command'; -export const SCREEN_READER_USER_PREFIX = 'User: '; - -export const SCREEN_READER_MODEL_PREFIX = 'Model: '; - // Tool status symbols used in ToolMessage component export const TOOL_STATUS = { SUCCESS: '✓', diff --git a/packages/cli/src/ui/textConstants.ts b/packages/cli/src/ui/textConstants.ts new file mode 100644 index 0000000000..53236cfed6 --- /dev/null +++ b/packages/cli/src/ui/textConstants.ts @@ -0,0 +1,13 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +export const SCREEN_READER_USER_PREFIX = 'User: '; + +export const SCREEN_READER_MODEL_PREFIX = 'Model: '; + +export const SCREEN_READER_LOADING = 'loading'; + +export const SCREEN_READER_RESPONDING = 'responding';