From 7404949eff1632e67145205e3d5ef2460b05bba5 Mon Sep 17 00:00:00 2001 From: matt korwel Date: Thu, 4 Sep 2025 10:00:46 -0700 Subject: [PATCH] Patch 0.3.0 preview.4 (#7713) Co-authored-by: gemini-cli-robot Co-authored-by: christine betts Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> Co-authored-by: Bryan Morgan Co-authored-by: anthony bushong --- integration-tests/read_many_files.test.ts | 2 +- package-lock.json | 24 +++++++-------- package.json | 4 +-- packages/a2a-server/package.json | 2 +- packages/cli/package.json | 7 +++-- packages/cli/src/config/config.ts | 6 ++-- packages/cli/src/config/settingsSchema.ts | 2 +- .../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 ++++++++ packages/core/package.json | 2 +- .../core/src/services/loopDetectionService.ts | 30 ++++++++++++++++++- packages/test-utils/package.json | 2 +- packages/vscode-ide-companion/package.json | 2 +- 21 files changed, 115 insertions(+), 46 deletions(-) create mode 100644 packages/cli/src/ui/textConstants.ts diff --git a/integration-tests/read_many_files.test.ts b/integration-tests/read_many_files.test.ts index 8e839a6a..15f8fcbe 100644 --- a/integration-tests/read_many_files.test.ts +++ b/integration-tests/read_many_files.test.ts @@ -8,7 +8,7 @@ import { describe, it, expect } from 'vitest'; import { TestRig, printDebugInfo, validateModelOutput } from './test-helper.js'; describe('read_many_files', () => { - it('should be able to read multiple files', async () => { + it.skip('should be able to read multiple files', async () => { const rig = new TestRig(); await rig.setup('should be able to read multiple files'); rig.createFile('file1.txt', 'file 1 content'); diff --git a/package-lock.json b/package-lock.json index 1d1b4b9d..011f9d90 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@google/gemini-cli", - "version": "0.3.0-preview.1", + "version": "0.3.0-preview.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@google/gemini-cli", - "version": "0.3.0-preview.1", + "version": "0.3.0-preview.3", "workspaces": [ "packages/*" ], @@ -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", @@ -14236,7 +14235,7 @@ }, "packages/a2a-server": { "name": "@google/gemini-cli-a2a-server", - "version": "0.3.0-preview.1", + "version": "0.3.0-preview.3", "dependencies": { "@a2a-js/sdk": "^0.3.2", "@google-cloud/storage": "^7.16.0", @@ -14507,7 +14506,7 @@ }, "packages/cli": { "name": "@google/gemini-cli", - "version": "0.3.0-preview.1", + "version": "0.3.0-preview.3", "dependencies": { "@google/gemini-cli-core": "file:../core", "@google/genai": "1.13.0", @@ -14517,9 +14516,10 @@ "command-exists": "^1.2.9", "diff": "^7.0.0", "dotenv": "^17.1.0", + "fzf": "^0.5.2", "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", @@ -14690,7 +14690,7 @@ }, "packages/core": { "name": "@google/gemini-cli-core", - "version": "0.3.0-preview.1", + "version": "0.3.0-preview.3", "dependencies": { "@google/genai": "1.13.0", "@lvce-editor/ripgrep": "^1.6.0", @@ -14812,7 +14812,7 @@ }, "packages/test-utils": { "name": "@google/gemini-cli-test-utils", - "version": "0.3.0-preview.1", + "version": "0.3.0-preview.3", "license": "Apache-2.0", "devDependencies": { "typescript": "^5.3.3" @@ -14823,7 +14823,7 @@ }, "packages/vscode-ide-companion": { "name": "gemini-cli-vscode-ide-companion", - "version": "0.3.0-preview.1", + "version": "0.3.0-preview.3", "license": "LICENSE", "dependencies": { "@modelcontextprotocol/sdk": "^1.15.1", diff --git a/package.json b/package.json index cfd51056..8d5655f6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@google/gemini-cli", - "version": "0.3.0-preview.1", + "version": "0.3.0-preview.3", "engines": { "node": ">=20.0.0" }, @@ -14,7 +14,7 @@ "url": "git+https://github.com/google-gemini/gemini-cli.git" }, "config": { - "sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.3.0-preview.1" + "sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.3.0-preview.3" }, "scripts": { "start": "node scripts/start.js", diff --git a/packages/a2a-server/package.json b/packages/a2a-server/package.json index 8254fc21..e27caf43 100644 --- a/packages/a2a-server/package.json +++ b/packages/a2a-server/package.json @@ -1,6 +1,6 @@ { "name": "@google/gemini-cli-a2a-server", - "version": "0.3.0-preview.1", + "version": "0.3.0-preview.3", "private": true, "description": "Gemini CLI A2A Server", "repository": { diff --git a/packages/cli/package.json b/packages/cli/package.json index c7dc200a..b0b9d0f9 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@google/gemini-cli", - "version": "0.3.0-preview.1", + "version": "0.3.0-preview.3", "description": "Gemini CLI", "repository": { "type": "git", @@ -25,7 +25,7 @@ "dist" ], "config": { - "sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.3.0-preview.1" + "sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.3.0-preview.3" }, "dependencies": { "@google/gemini-cli-core": "file:../core", @@ -36,9 +36,10 @@ "command-exists": "^1.2.9", "diff": "^7.0.0", "dotenv": "^17.1.0", + "fzf": "^0.5.2", "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 dc34f50c..ddb3cc08 100755 --- a/packages/cli/src/config/config.ts +++ b/packages/cli/src/config/config.ts @@ -482,10 +482,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/config/settingsSchema.ts b/packages/cli/src/config/settingsSchema.ts index c27d1327..eb9850ce 100644 --- a/packages/cli/src/config/settingsSchema.ts +++ b/packages/cli/src/config/settingsSchema.ts @@ -242,7 +242,7 @@ export const SETTINGS_SCHEMA = { label: 'Screen Reader Mode', category: 'UI', requiresRestart: true, - default: false, + default: undefined as boolean | undefined, description: 'Render output in plain-text to be more screen reader accessible', showInDialog: true, diff --git a/packages/cli/src/ui/components/GeminiRespondingSpinner.tsx b/packages/cli/src/ui/components/GeminiRespondingSpinner.tsx index f4ed235f..caf774e2 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 09897885..59516489 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 993ec303..7663172e 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 33a566b9..f855c97f 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 3476a44d..9473c128 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 203ab986..c4e5b6ba 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 a05964f3..4f279a74 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 921c954a..719d263b 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 38a0f162..9c95a29d 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 00000000..53236cfe --- /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'; diff --git a/packages/core/package.json b/packages/core/package.json index 6333e776..889340e6 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@google/gemini-cli-core", - "version": "0.3.0-preview.1", + "version": "0.3.0-preview.3", "description": "Gemini CLI Core", "repository": { "type": "git", diff --git a/packages/core/src/services/loopDetectionService.ts b/packages/core/src/services/loopDetectionService.ts index 36f3169f..97cb94dc 100644 --- a/packages/core/src/services/loopDetectionService.ts +++ b/packages/core/src/services/loopDetectionService.ts @@ -4,6 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ +import type { Content } from '@google/genai'; import { createHash } from 'node:crypto'; import type { ServerGeminiStreamEvent } from '../core/turn.js'; import { GeminiEventType } from '../core/turn.js'; @@ -11,6 +12,10 @@ import { logLoopDetected } from '../telemetry/loggers.js'; import { LoopDetectedEvent, LoopType } from '../telemetry/types.js'; import type { Config } from '../config/config.js'; import { DEFAULT_GEMINI_FLASH_MODEL } from '../config/config.js'; +import { + isFunctionCall, + isFunctionResponse, +} from '../utils/messageInspectors.js'; const TOOL_CALL_LOOP_THRESHOLD = 5; const CONTENT_LOOP_THRESHOLD = 10; @@ -328,12 +333,35 @@ export class LoopDetectionService { return originalChunk === currentChunk; } + private trimRecentHistory(recentHistory: Content[]): Content[] { + // A function response must be preceded by a function call. + // Continuously removes dangling function calls from the end of the history + // until the last turn is not a function call. + while ( + recentHistory.length > 0 && + isFunctionCall(recentHistory[recentHistory.length - 1]) + ) { + recentHistory.pop(); + } + + // A function response should follow a function call. + // Continuously removes leading function responses from the beginning of history + // until the first turn is not a function response. + while (recentHistory.length > 0 && isFunctionResponse(recentHistory[0])) { + recentHistory.shift(); + } + + return recentHistory; + } + private async checkForLoopWithLLM(signal: AbortSignal) { const recentHistory = this.config .getGeminiClient() .getHistory() .slice(-LLM_LOOP_CHECK_HISTORY_COUNT); + const trimmedHistory = this.trimRecentHistory(recentHistory); + const prompt = `You are a sophisticated AI diagnostic agent specializing in identifying when a conversational AI is stuck in an unproductive state. Your task is to analyze the provided conversation history and determine if the assistant has ceased to make meaningful progress. An unproductive state is characterized by one or more of the following patterns over the last 5 or more assistant turns: @@ -347,7 +375,7 @@ For example, a series of 'tool_A' or 'tool_B' tool calls that make small, distin Please analyze the conversation history to determine the possibility that the conversation is stuck in a repetitive, non-productive state.`; const contents = [ - ...recentHistory, + ...trimmedHistory, { role: 'user', parts: [{ text: prompt }] }, ]; const schema: Record = { diff --git a/packages/test-utils/package.json b/packages/test-utils/package.json index 5fd20390..dd1484e0 100644 --- a/packages/test-utils/package.json +++ b/packages/test-utils/package.json @@ -1,6 +1,6 @@ { "name": "@google/gemini-cli-test-utils", - "version": "0.3.0-preview.1", + "version": "0.3.0-preview.3", "private": true, "main": "src/index.ts", "license": "Apache-2.0", diff --git a/packages/vscode-ide-companion/package.json b/packages/vscode-ide-companion/package.json index eca6e8a0..59f47ba1 100644 --- a/packages/vscode-ide-companion/package.json +++ b/packages/vscode-ide-companion/package.json @@ -2,7 +2,7 @@ "name": "gemini-cli-vscode-ide-companion", "displayName": "Gemini CLI Companion", "description": "Enable Gemini CLI with direct access to your IDE workspace.", - "version": "0.3.0-preview.1", + "version": "0.3.0-preview.3", "publisher": "google", "icon": "assets/icon.png", "repository": {