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';