From 10286934e6a549dcad557adecfc087552e13c983 Mon Sep 17 00:00:00 2001 From: christine betts Date: Thu, 21 Aug 2025 22:29:15 +0000 Subject: [PATCH] Introduce initial screen reader mode handling and flag (#6653) --- docs/cli/configuration.md | 16 ++++ package-lock.json | 88 ++++++++++--------- packages/cli/src/config/config.ts | 14 ++- packages/cli/src/config/settings.ts | 1 + packages/cli/src/config/settingsSchema.ts | 10 +++ packages/cli/src/gemini.tsx | 2 +- packages/cli/src/ui/App.test.tsx | 2 + packages/cli/src/ui/App.tsx | 12 ++- .../cli/src/ui/components/InputPrompt.tsx | 8 +- .../messages/CompressionMessage.tsx | 2 + .../ui/components/messages/GeminiMessage.tsx | 8 +- .../ui/components/messages/UserMessage.tsx | 5 +- packages/cli/src/ui/constants.ts | 4 + packages/core/src/config/config.ts | 5 ++ 14 files changed, 125 insertions(+), 52 deletions(-) diff --git a/docs/cli/configuration.md b/docs/cli/configuration.md index fd6f69f0..af73661b 100644 --- a/docs/cli/configuration.md +++ b/docs/cli/configuration.md @@ -308,6 +308,20 @@ In addition to a project settings file, a project's `.gemini` directory can cont "showLineNumbers": false ``` +- **`accessibility`** (object): + - **Description:** Configures accessibility features for the CLI. + - **Properties:** + - **`screenReader`** (boolean): Enables screen reader mode, which adjusts the TUI for better compatibility with screen readers. This can also be enabled with the `--screen-reader` command-line flag, which will take precedence over the setting. + - **`disableLoadingPhrases`** (boolean): Disables the display of loading phrases during operations. + - **Default:** `{"screenReader": false, "disableLoadingPhrases": false}` + - **Example:** + ```json + "accessibility": { + "screenReader": true, + "disableLoadingPhrases": true + } + ``` + ### Example `settings.json`: ```json @@ -475,6 +489,8 @@ Arguments passed directly when running the CLI can override other configurations - Can be specified multiple times or as comma-separated values. - 5 directories can be added at maximum. - Example: `--include-directories /path/to/project1,/path/to/project2` or `--include-directories /path/to/project1 --include-directories /path/to/project2` +- **`--screen-reader`**: + - Enables screen reader mode for accessibility. - **`--version`**: - Displays the version of the CLI. diff --git a/package-lock.json b/package-lock.json index 70a8cfa7..797a80c6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -63,16 +63,16 @@ } }, "node_modules/@alcalzone/ansi-tokenize": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@alcalzone/ansi-tokenize/-/ansi-tokenize-0.1.3.tgz", - "integrity": "sha512-3yWxPTq3UQ/FY9p1ErPxIyfT64elWaMvM9lIHnaqpyft63tkxodF5aUElYHrdisWve5cETkh1+KBw1yJuW0aRw==", + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@alcalzone/ansi-tokenize/-/ansi-tokenize-0.2.0.tgz", + "integrity": "sha512-qI/5TaaaCZE4yeSZ83lu0+xi1r88JSxUjnH4OP/iZF7+KKZ75u3ee5isd0LxX+6N8U0npL61YrpbthILHB6BnA==", "license": "MIT", "dependencies": { "ansi-styles": "^6.2.1", - "is-fullwidth-code-point": "^4.0.0" + "is-fullwidth-code-point": "^5.0.0" }, "engines": { - "node": ">=14.13.1" + "node": ">=18" } }, "node_modules/@alcalzone/ansi-tokenize/node_modules/ansi-styles": { @@ -88,12 +88,15 @@ } }, "node_modules/@alcalzone/ansi-tokenize/node_modules/is-fullwidth-code-point": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", - "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.0.0.tgz", + "integrity": "sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==", "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.0.0" + }, "engines": { - "node": ">=12" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -5175,9 +5178,9 @@ } }, "node_modules/es-toolkit": { - "version": "1.39.5", - "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.39.5.tgz", - "integrity": "sha512-z9V0qU4lx1TBXDNFWfAASWk6RNU6c6+TJBKE+FLIg8u0XJ6Yw58Hi0yX8ftEouj6p1QARRlXLFfHbIli93BdQQ==", + "version": "1.39.10", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.39.10.tgz", + "integrity": "sha512-E0iGnTtbDhkeczB0T+mxmoVlT4YNweEKBLq7oaU4p11mecdsZpNWOglI4895Vh4usbQ+LsJiuLuI2L0Vdmfm2w==", "license": "MIT", "workspaces": [ "docs", @@ -6859,26 +6862,26 @@ } }, "node_modules/ink": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/ink/-/ink-6.1.1.tgz", - "integrity": "sha512-Bqw78FX+1TSIGxs6bdvohgoy6mTfqjFJVNyYzXn8HIyZyVmwLX8XdnhUtUwyaelLCqLz8uuFseCbomRZWjyo5g==", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ink/-/ink-6.2.2.tgz", + "integrity": "sha512-LN1f+/D8KKqMqRux08fIfA9wsEAJ9Bu9CiI3L6ih7bnqNSDUXT/JVJ0rUIc4NkjPiPaeI3BVNREcLYLz9ePSEg==", "license": "MIT", "dependencies": { - "@alcalzone/ansi-tokenize": "^0.1.3", + "@alcalzone/ansi-tokenize": "^0.2.0", "ansi-escapes": "^7.0.0", "ansi-styles": "^6.2.1", "auto-bind": "^5.0.1", - "chalk": "^5.3.0", + "chalk": "^5.6.0", "cli-boxes": "^3.0.0", "cli-cursor": "^4.0.0", "cli-truncate": "^4.0.0", "code-excerpt": "^4.0.0", - "es-toolkit": "^1.22.0", + "es-toolkit": "^1.39.10", "indent-string": "^5.0.0", - "is-in-ci": "^1.0.0", + "is-in-ci": "^2.0.0", "patch-console": "^2.0.0", "react-reconciler": "^0.32.0", - "scheduler": "^0.23.0", + "scheduler": "^0.26.0", "signal-exit": "^3.0.7", "slice-ansi": "^7.1.0", "stack-utils": "^2.0.6", @@ -7030,9 +7033,9 @@ } }, "node_modules/ink/node_modules/chalk": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", - "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.0.tgz", + "integrity": "sha512-46QrSQFyVSEyYAgQ22hQ+zDa60YHA4fBstHmtSApj1Y5vKtG27fWowW03jCk5KcbXEWPZUIR894aARCA/G1kfQ==", "license": "MIT", "engines": { "node": "^12.17.0 || ^14.13 || >=16.0.0" @@ -7047,6 +7050,21 @@ "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", "license": "MIT" }, + "node_modules/ink/node_modules/is-in-ci": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-in-ci/-/is-in-ci-2.0.0.tgz", + "integrity": "sha512-cFeerHriAnhrQSbpAxL37W1wcJKUUX07HyLWZCW1URJT/ra3GyUTzBgUnh24TMVfNTV2Hij2HLxkPHFZfOZy5w==", + "license": "MIT", + "bin": { + "is-in-ci": "cli.js" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/ink/node_modules/signal-exit": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", @@ -9733,13 +9751,6 @@ "react": "^19.1.0" } }, - "node_modules/react-dom/node_modules/scheduler": { - "version": "0.26.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", - "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", - "dev": true, - "license": "MIT" - }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", @@ -9761,12 +9772,6 @@ "react": "^19.1.0" } }, - "node_modules/react-reconciler/node_modules/scheduler": { - "version": "0.26.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", - "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", - "license": "MIT" - }, "node_modules/read-package-up": { "version": "11.0.0", "resolved": "https://registry.npmjs.org/read-package-up/-/read-package-up-11.0.0.tgz", @@ -10224,13 +10229,10 @@ } }, "node_modules/scheduler": { - "version": "0.23.2", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", - "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0" - } + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", + "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", + "license": "MIT" }, "node_modules/selderee": { "version": "0.11.0", diff --git a/packages/cli/src/config/config.ts b/packages/cli/src/config/config.ts index 0b21ff2e..aaaf293d 100644 --- a/packages/cli/src/config/config.ts +++ b/packages/cli/src/config/config.ts @@ -73,6 +73,7 @@ export interface CliArgs { listExtensions: boolean | undefined; proxy: string | undefined; includeDirectories: string[] | undefined; + screenReader: boolean | undefined; } export async function parseArguments(): Promise { @@ -229,6 +230,11 @@ export async function parseArguments(): Promise { // Handle comma-separated values dirs.flatMap((dir) => dir.split(',').map((d) => d.trim())), }) + .option('screen-reader', { + type: 'boolean', + description: 'Enable screen reader mode for accessibility.', + default: false, + }) .check((argv) => { if (argv.prompt && argv['promptInteractive']) { @@ -465,6 +471,9 @@ 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.accessibility?.screenReader ?? false; return new Config({ sessionId, embeddingModel: DEFAULT_GEMINI_EMBEDDING_MODEL, @@ -490,7 +499,10 @@ export async function loadCliConfig( argv.show_memory_usage || settings.showMemoryUsage || false, - accessibility: settings.accessibility, + accessibility: { + ...settings.accessibility, + screenReader, + }, telemetry: { enabled: argv.telemetry ?? settings.telemetry?.enabled, target: (argv.telemetryTarget ?? diff --git a/packages/cli/src/config/settings.ts b/packages/cli/src/config/settings.ts index 3f94fe65..5f8c706d 100644 --- a/packages/cli/src/config/settings.ts +++ b/packages/cli/src/config/settings.ts @@ -58,6 +58,7 @@ export interface SummarizeToolOutputSettings { export interface AccessibilitySettings { disableLoadingPhrases?: boolean; + screenReader?: boolean; } export interface SettingsError { diff --git a/packages/cli/src/config/settingsSchema.ts b/packages/cli/src/config/settingsSchema.ts index 7f28b698..5f939b56 100644 --- a/packages/cli/src/config/settingsSchema.ts +++ b/packages/cli/src/config/settingsSchema.ts @@ -206,6 +206,16 @@ export const SETTINGS_SCHEMA = { description: 'Disable loading phrases for accessibility', showInDialog: true, }, + screenReader: { + type: 'boolean', + label: 'Screen Reader Mode', + category: 'Accessibility', + requiresRestart: true, + default: false, + description: + 'Render output in plain-text to be more screen reader accessible', + showInDialog: true, + }, }, }, checkpointing: { diff --git a/packages/cli/src/gemini.tsx b/packages/cli/src/gemini.tsx index 6661d3ef..b285a5af 100644 --- a/packages/cli/src/gemini.tsx +++ b/packages/cli/src/gemini.tsx @@ -316,7 +316,7 @@ export async function main() { /> , - { exitOnCtrlC: false }, + { exitOnCtrlC: false, isScreenReaderEnabled: config.getScreenReader() }, ); checkForUpdates() diff --git a/packages/cli/src/ui/App.test.tsx b/packages/cli/src/ui/App.test.tsx index 9f8a681f..d5f2ba82 100644 --- a/packages/cli/src/ui/App.test.tsx +++ b/packages/cli/src/ui/App.test.tsx @@ -87,6 +87,7 @@ interface MockServerConfig { getGeminiClient: Mock<() => GeminiClient | undefined>; getUserTier: Mock<() => Promise>; getIdeClient: Mock<() => { getCurrentIde: Mock<() => string | undefined> }>; + getScreenReader: Mock<() => boolean>; } // Mock @google/gemini-cli-core and its Config class @@ -168,6 +169,7 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => { getConnectionStatus: vi.fn(() => 'connected'), })), isTrustedFolder: vi.fn(() => true), + getScreenReader: vi.fn(() => false), }; }); diff --git a/packages/cli/src/ui/App.tsx b/packages/cli/src/ui/App.tsx index 6bf4f770..b4ec6a57 100644 --- a/packages/cli/src/ui/App.tsx +++ b/packages/cli/src/ui/App.tsx @@ -923,10 +923,12 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => { key={staticKey} items={[ - {!settings.merged.hideBanner && ( + {!(settings.merged.hideBanner || config.getScreenReader()) && (
)} - {!settings.merged.hideTips && } + {!(settings.merged.hideTips || config.getScreenReader()) && ( + + )} , ...history.map((h) => ( { = ({ > {shellModeActive ? ( reverseSearchActive ? ( - (r:) + + (r:){' '} + ) : ( '! ' ) diff --git a/packages/cli/src/ui/components/messages/CompressionMessage.tsx b/packages/cli/src/ui/components/messages/CompressionMessage.tsx index c7ef122b..6aededbb 100644 --- a/packages/cli/src/ui/components/messages/CompressionMessage.tsx +++ b/packages/cli/src/ui/components/messages/CompressionMessage.tsx @@ -9,6 +9,7 @@ import { Box, Text } from 'ink'; import { CompressionProps } from '../../types.js'; import Spinner from 'ink-spinner'; import { Colors } from '../../colors.js'; +import { SCREEN_READER_MODEL_PREFIX } from '../../constants.js'; export interface CompressionDisplayProps { compression: CompressionProps; @@ -40,6 +41,7 @@ export const CompressionMessage: React.FC = ({ color={ compression.isPending ? Colors.AccentPurple : Colors.AccentGreen } + aria-label={SCREEN_READER_MODEL_PREFIX} > {text} diff --git a/packages/cli/src/ui/components/messages/GeminiMessage.tsx b/packages/cli/src/ui/components/messages/GeminiMessage.tsx index 9863acd6..cfc3a297 100644 --- a/packages/cli/src/ui/components/messages/GeminiMessage.tsx +++ b/packages/cli/src/ui/components/messages/GeminiMessage.tsx @@ -8,6 +8,7 @@ import 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'; interface GeminiMessageProps { text: string; @@ -28,7 +29,12 @@ export const GeminiMessage: React.FC = ({ return ( - {prefix} + + {prefix} + = ({ text }) => { alignSelf="flex-start" > - {prefix} + + {prefix} + diff --git a/packages/cli/src/ui/constants.ts b/packages/cli/src/ui/constants.ts index 6a77631c..6a1047dc 100644 --- a/packages/cli/src/ui/constants.ts +++ b/packages/cli/src/ui/constants.ts @@ -15,3 +15,7 @@ export const UI_WIDTH = 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: '; diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts index 44df13a8..227a17c0 100644 --- a/packages/core/src/config/config.ts +++ b/packages/core/src/config/config.ts @@ -62,6 +62,7 @@ export enum ApprovalMode { export interface AccessibilitySettings { disableLoadingPhrases?: boolean; + screenReader?: boolean; } export interface BugCommandSettings { @@ -734,6 +735,10 @@ export class Config { return this.skipNextSpeakerCheck; } + getScreenReader(): boolean { + return this.accessibility.screenReader ?? false; + } + getEnablePromptCompletion(): boolean { return this.enablePromptCompletion; }