summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorchristine betts <[email protected]>2025-08-21 22:29:15 +0000
committerGitHub <[email protected]>2025-08-21 22:29:15 +0000
commit10286934e6a549dcad557adecfc087552e13c983 (patch)
treefe6a61cf3158918253d18d891dab52f4c9b7a33b
parent679acc45b222986032a01aebbffcddf756573f14 (diff)
Introduce initial screen reader mode handling and flag (#6653)
-rw-r--r--docs/cli/configuration.md16
-rw-r--r--package-lock.json88
-rw-r--r--packages/cli/src/config/config.ts14
-rw-r--r--packages/cli/src/config/settings.ts1
-rw-r--r--packages/cli/src/config/settingsSchema.ts10
-rw-r--r--packages/cli/src/gemini.tsx2
-rw-r--r--packages/cli/src/ui/App.test.tsx2
-rw-r--r--packages/cli/src/ui/App.tsx12
-rw-r--r--packages/cli/src/ui/components/InputPrompt.tsx8
-rw-r--r--packages/cli/src/ui/components/messages/CompressionMessage.tsx2
-rw-r--r--packages/cli/src/ui/components/messages/GeminiMessage.tsx8
-rw-r--r--packages/cli/src/ui/components/messages/UserMessage.tsx5
-rw-r--r--packages/cli/src/ui/constants.ts4
-rw-r--r--packages/core/src/config/config.ts5
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<CliArgs> {
@@ -229,6 +230,11 @@ export async function parseArguments(): Promise<CliArgs> {
// 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() {
/>
</SettingsContext.Provider>
</React.StrictMode>,
- { 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<string | undefined>>;
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={[
<Box flexDirection="column" key="header">
- {!settings.merged.hideBanner && (
+ {!(settings.merged.hideBanner || config.getScreenReader()) && (
<Header version={version} nightly={nightly} />
)}
- {!settings.merged.hideTips && <Tips config={config} />}
+ {!(settings.merged.hideTips || config.getScreenReader()) && (
+ <Tips config={config} />
+ )}
</Box>,
...history.map((h) => (
<HistoryItemDisplay
@@ -1093,12 +1095,14 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
<LoadingIndicator
thought={
streamingState === StreamingState.WaitingForConfirmation ||
- config.getAccessibility()?.disableLoadingPhrases
+ config.getAccessibility()?.disableLoadingPhrases ||
+ config.getScreenReader()
? undefined
: thought
}
currentLoadingPhrase={
- config.getAccessibility()?.disableLoadingPhrases
+ config.getAccessibility()?.disableLoadingPhrases ||
+ config.getScreenReader()
? undefined
: currentLoadingPhrase
}
diff --git a/packages/cli/src/ui/components/InputPrompt.tsx b/packages/cli/src/ui/components/InputPrompt.tsx
index 01666c66..215962ce 100644
--- a/packages/cli/src/ui/components/InputPrompt.tsx
+++ b/packages/cli/src/ui/components/InputPrompt.tsx
@@ -26,6 +26,7 @@ import {
cleanupOldClipboardImages,
} from '../utils/clipboardUtils.js';
import * as path from 'path';
+import { SCREEN_READER_USER_PREFIX } from '../constants.js';
export interface InputPromptProps {
buffer: TextBuffer;
@@ -688,7 +689,12 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
>
{shellModeActive ? (
reverseSearchActive ? (
- <Text color={theme.text.link}>(r:) </Text>
+ <Text
+ color={theme.text.link}
+ aria-label={SCREEN_READER_USER_PREFIX}
+ >
+ (r:){' '}
+ </Text>
) : (
'! '
)
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<CompressionDisplayProps> = ({
color={
compression.isPending ? Colors.AccentPurple : Colors.AccentGreen
}
+ aria-label={SCREEN_READER_MODEL_PREFIX}
>
{text}
</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<GeminiMessageProps> = ({
return (
<Box flexDirection="row">
<Box width={prefixWidth}>
- <Text color={Colors.AccentPurple}>{prefix}</Text>
+ <Text
+ color={Colors.AccentPurple}
+ aria-label={SCREEN_READER_MODEL_PREFIX}
+ >
+ {prefix}
+ </Text>
</Box>
<Box flexGrow={1} flexDirection="column">
<MarkdownDisplay
diff --git a/packages/cli/src/ui/components/messages/UserMessage.tsx b/packages/cli/src/ui/components/messages/UserMessage.tsx
index 332cb0f4..1be41be9 100644
--- a/packages/cli/src/ui/components/messages/UserMessage.tsx
+++ b/packages/cli/src/ui/components/messages/UserMessage.tsx
@@ -7,6 +7,7 @@
import React from 'react';
import { Text, Box } from 'ink';
import { Colors } from '../../colors.js';
+import { SCREEN_READER_USER_PREFIX } from '../../constants.js';
interface UserMessageProps {
text: string;
@@ -31,7 +32,9 @@ export const UserMessage: React.FC<UserMessageProps> = ({ text }) => {
alignSelf="flex-start"
>
<Box width={prefixWidth}>
- <Text color={textColor}>{prefix}</Text>
+ <Text color={textColor} aria-label={SCREEN_READER_USER_PREFIX}>
+ {prefix}
+ </Text>
</Box>
<Box flexGrow={1}>
<Text wrap="wrap" color={textColor}>
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;
}