Patch 0.3.0 preview.4 (#7713)

Co-authored-by: gemini-cli-robot <gemini-cli-robot@google.com>
Co-authored-by: christine betts <chrstn@uw.edu>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Co-authored-by: Bryan Morgan <bryanmorgan@google.com>
Co-authored-by: anthony bushong <agmsb@users.noreply.github.com>
This commit is contained in:
matt korwel 2025-09-04 10:00:46 -07:00 committed by GitHub
parent 52cc0f6feb
commit 7404949eff
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 115 additions and 46 deletions

View file

@ -8,7 +8,7 @@ import { describe, it, expect } from 'vitest';
import { TestRig, printDebugInfo, validateModelOutput } from './test-helper.js'; import { TestRig, printDebugInfo, validateModelOutput } from './test-helper.js';
describe('read_many_files', () => { 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(); const rig = new TestRig();
await rig.setup('should be able to read multiple files'); await rig.setup('should be able to read multiple files');
rig.createFile('file1.txt', 'file 1 content'); rig.createFile('file1.txt', 'file 1 content');

24
package-lock.json generated
View file

@ -1,12 +1,12 @@
{ {
"name": "@google/gemini-cli", "name": "@google/gemini-cli",
"version": "0.3.0-preview.1", "version": "0.3.0-preview.3",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@google/gemini-cli", "name": "@google/gemini-cli",
"version": "0.3.0-preview.1", "version": "0.3.0-preview.3",
"workspaces": [ "workspaces": [
"packages/*" "packages/*"
], ],
@ -8085,9 +8085,9 @@
} }
}, },
"node_modules/ink": { "node_modules/ink": {
"version": "6.2.2", "version": "6.2.3",
"resolved": "https://registry.npmjs.org/ink/-/ink-6.2.2.tgz", "resolved": "https://registry.npmjs.org/ink/-/ink-6.2.3.tgz",
"integrity": "sha512-LN1f+/D8KKqMqRux08fIfA9wsEAJ9Bu9CiI3L6ih7bnqNSDUXT/JVJ0rUIc4NkjPiPaeI3BVNREcLYLz9ePSEg==", "integrity": "sha512-fQkfEJjKbLXIcVWEE3MvpYSnwtbbmRsmeNDNz1pIuOFlwE+UF2gsy228J36OXKZGWJWZJKUigphBSqCNMcARtg==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@alcalzone/ansi-tokenize": "^0.2.0", "@alcalzone/ansi-tokenize": "^0.2.0",
@ -8104,7 +8104,6 @@
"is-in-ci": "^2.0.0", "is-in-ci": "^2.0.0",
"patch-console": "^2.0.0", "patch-console": "^2.0.0",
"react-reconciler": "^0.32.0", "react-reconciler": "^0.32.0",
"scheduler": "^0.26.0",
"signal-exit": "^3.0.7", "signal-exit": "^3.0.7",
"slice-ansi": "^7.1.0", "slice-ansi": "^7.1.0",
"stack-utils": "^2.0.6", "stack-utils": "^2.0.6",
@ -14236,7 +14235,7 @@
}, },
"packages/a2a-server": { "packages/a2a-server": {
"name": "@google/gemini-cli-a2a-server", "name": "@google/gemini-cli-a2a-server",
"version": "0.3.0-preview.1", "version": "0.3.0-preview.3",
"dependencies": { "dependencies": {
"@a2a-js/sdk": "^0.3.2", "@a2a-js/sdk": "^0.3.2",
"@google-cloud/storage": "^7.16.0", "@google-cloud/storage": "^7.16.0",
@ -14507,7 +14506,7 @@
}, },
"packages/cli": { "packages/cli": {
"name": "@google/gemini-cli", "name": "@google/gemini-cli",
"version": "0.3.0-preview.1", "version": "0.3.0-preview.3",
"dependencies": { "dependencies": {
"@google/gemini-cli-core": "file:../core", "@google/gemini-cli-core": "file:../core",
"@google/genai": "1.13.0", "@google/genai": "1.13.0",
@ -14517,9 +14516,10 @@
"command-exists": "^1.2.9", "command-exists": "^1.2.9",
"diff": "^7.0.0", "diff": "^7.0.0",
"dotenv": "^17.1.0", "dotenv": "^17.1.0",
"fzf": "^0.5.2",
"glob": "^10.4.1", "glob": "^10.4.1",
"highlight.js": "^11.11.1", "highlight.js": "^11.11.1",
"ink": "^6.1.1", "ink": "^6.2.3",
"ink-gradient": "^3.0.0", "ink-gradient": "^3.0.0",
"ink-spinner": "^5.0.0", "ink-spinner": "^5.0.0",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
@ -14690,7 +14690,7 @@
}, },
"packages/core": { "packages/core": {
"name": "@google/gemini-cli-core", "name": "@google/gemini-cli-core",
"version": "0.3.0-preview.1", "version": "0.3.0-preview.3",
"dependencies": { "dependencies": {
"@google/genai": "1.13.0", "@google/genai": "1.13.0",
"@lvce-editor/ripgrep": "^1.6.0", "@lvce-editor/ripgrep": "^1.6.0",
@ -14812,7 +14812,7 @@
}, },
"packages/test-utils": { "packages/test-utils": {
"name": "@google/gemini-cli-test-utils", "name": "@google/gemini-cli-test-utils",
"version": "0.3.0-preview.1", "version": "0.3.0-preview.3",
"license": "Apache-2.0", "license": "Apache-2.0",
"devDependencies": { "devDependencies": {
"typescript": "^5.3.3" "typescript": "^5.3.3"
@ -14823,7 +14823,7 @@
}, },
"packages/vscode-ide-companion": { "packages/vscode-ide-companion": {
"name": "gemini-cli-vscode-ide-companion", "name": "gemini-cli-vscode-ide-companion",
"version": "0.3.0-preview.1", "version": "0.3.0-preview.3",
"license": "LICENSE", "license": "LICENSE",
"dependencies": { "dependencies": {
"@modelcontextprotocol/sdk": "^1.15.1", "@modelcontextprotocol/sdk": "^1.15.1",

View file

@ -1,6 +1,6 @@
{ {
"name": "@google/gemini-cli", "name": "@google/gemini-cli",
"version": "0.3.0-preview.1", "version": "0.3.0-preview.3",
"engines": { "engines": {
"node": ">=20.0.0" "node": ">=20.0.0"
}, },
@ -14,7 +14,7 @@
"url": "git+https://github.com/google-gemini/gemini-cli.git" "url": "git+https://github.com/google-gemini/gemini-cli.git"
}, },
"config": { "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": { "scripts": {
"start": "node scripts/start.js", "start": "node scripts/start.js",

View file

@ -1,6 +1,6 @@
{ {
"name": "@google/gemini-cli-a2a-server", "name": "@google/gemini-cli-a2a-server",
"version": "0.3.0-preview.1", "version": "0.3.0-preview.3",
"private": true, "private": true,
"description": "Gemini CLI A2A Server", "description": "Gemini CLI A2A Server",
"repository": { "repository": {

View file

@ -1,6 +1,6 @@
{ {
"name": "@google/gemini-cli", "name": "@google/gemini-cli",
"version": "0.3.0-preview.1", "version": "0.3.0-preview.3",
"description": "Gemini CLI", "description": "Gemini CLI",
"repository": { "repository": {
"type": "git", "type": "git",
@ -25,7 +25,7 @@
"dist" "dist"
], ],
"config": { "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": { "dependencies": {
"@google/gemini-cli-core": "file:../core", "@google/gemini-cli-core": "file:../core",
@ -36,9 +36,10 @@
"command-exists": "^1.2.9", "command-exists": "^1.2.9",
"diff": "^7.0.0", "diff": "^7.0.0",
"dotenv": "^17.1.0", "dotenv": "^17.1.0",
"fzf": "^0.5.2",
"glob": "^10.4.1", "glob": "^10.4.1",
"highlight.js": "^11.11.1", "highlight.js": "^11.11.1",
"ink": "^6.1.1", "ink": "^6.2.3",
"ink-gradient": "^3.0.0", "ink-gradient": "^3.0.0",
"ink-spinner": "^5.0.0", "ink-spinner": "^5.0.0",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",

View file

@ -482,10 +482,10 @@ export async function loadCliConfig(
} }
const sandboxConfig = await loadSandboxConfig(settings, argv); const sandboxConfig = await loadSandboxConfig(settings, argv);
// The screen reader argument takes precedence over the accessibility setting.
const screenReader = const screenReader =
argv.screenReader ?? settings.ui?.accessibility?.screenReader ?? false; argv.screenReader !== undefined
? argv.screenReader
: (settings.ui?.accessibility?.screenReader ?? false);
return new Config({ return new Config({
sessionId, sessionId,
embeddingModel: DEFAULT_GEMINI_EMBEDDING_MODEL, embeddingModel: DEFAULT_GEMINI_EMBEDDING_MODEL,

View file

@ -242,7 +242,7 @@ export const SETTINGS_SCHEMA = {
label: 'Screen Reader Mode', label: 'Screen Reader Mode',
category: 'UI', category: 'UI',
requiresRestart: true, requiresRestart: true,
default: false, default: undefined as boolean | undefined,
description: description:
'Render output in plain-text to be more screen reader accessible', 'Render output in plain-text to be more screen reader accessible',
showInDialog: true, showInDialog: true,

View file

@ -5,11 +5,15 @@
*/ */
import type React from 'react'; import type React from 'react';
import { Text } from 'ink'; import { Text, useIsScreenReaderEnabled } from 'ink';
import Spinner from 'ink-spinner'; import Spinner from 'ink-spinner';
import type { SpinnerName } from 'cli-spinners'; import type { SpinnerName } from 'cli-spinners';
import { useStreamingContext } from '../contexts/StreamingContext.js'; import { useStreamingContext } from '../contexts/StreamingContext.js';
import { StreamingState } from '../types.js'; import { StreamingState } from '../types.js';
import {
SCREEN_READER_LOADING,
SCREEN_READER_RESPONDING,
} from '../textConstants.js';
interface GeminiRespondingSpinnerProps { interface GeminiRespondingSpinnerProps {
/** /**
@ -24,11 +28,19 @@ export const GeminiRespondingSpinner: React.FC<
GeminiRespondingSpinnerProps GeminiRespondingSpinnerProps
> = ({ nonRespondingDisplay, spinnerType = 'dots' }) => { > = ({ nonRespondingDisplay, spinnerType = 'dots' }) => {
const streamingState = useStreamingContext(); const streamingState = useStreamingContext();
const isScreenReaderEnabled = useIsScreenReaderEnabled();
if (streamingState === StreamingState.Responding) { if (streamingState === StreamingState.Responding) {
return <Spinner type={spinnerType} />; return isScreenReaderEnabled ? (
<Text>{SCREEN_READER_RESPONDING}</Text>
) : (
<Spinner type={spinnerType} />
);
} else if (nonRespondingDisplay) { } else if (nonRespondingDisplay) {
return <Text>{nonRespondingDisplay}</Text>; return isScreenReaderEnabled ? (
<Text>{SCREEN_READER_LOADING}</Text>
) : (
<Text>{nonRespondingDisplay}</Text>
);
} }
return null; return null;
}; };

View file

@ -29,7 +29,7 @@ import {
cleanupOldClipboardImages, cleanupOldClipboardImages,
} from '../utils/clipboardUtils.js'; } from '../utils/clipboardUtils.js';
import * as path from 'node:path'; 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 { export interface InputPromptProps {
buffer: TextBuffer; buffer: TextBuffer;

View file

@ -9,7 +9,7 @@ import { Box, Text } from 'ink';
import type { CompressionProps } from '../../types.js'; import type { CompressionProps } from '../../types.js';
import Spinner from 'ink-spinner'; import Spinner from 'ink-spinner';
import { Colors } from '../../colors.js'; import { Colors } from '../../colors.js';
import { SCREEN_READER_MODEL_PREFIX } from '../../constants.js'; import { SCREEN_READER_MODEL_PREFIX } from '../../textConstants.js';
export interface CompressionDisplayProps { export interface CompressionDisplayProps {
compression: CompressionProps; compression: CompressionProps;

View file

@ -5,7 +5,7 @@
*/ */
import type React from 'react'; import type React from 'react';
import { Box, Text } from 'ink'; import { Box, Text, useIsScreenReaderEnabled } from 'ink';
import { Colors } from '../../colors.js'; import { Colors } from '../../colors.js';
import crypto from 'node:crypto'; import crypto from 'node:crypto';
import { colorizeCode, colorizeLine } from '../../utils/CodeColorizer.js'; import { colorizeCode, colorizeLine } from '../../utils/CodeColorizer.js';
@ -107,6 +107,7 @@ export const DiffRenderer: React.FC<DiffRendererProps> = ({
terminalWidth, terminalWidth,
theme, theme,
}) => { }) => {
const screenReaderEnabled = useIsScreenReaderEnabled();
if (!diffContent || typeof diffContent !== 'string') { if (!diffContent || typeof diffContent !== 'string') {
return <Text color={Colors.AccentYellow}>No diff content.</Text>; return <Text color={Colors.AccentYellow}>No diff content.</Text>;
} }
@ -120,6 +121,17 @@ export const DiffRenderer: React.FC<DiffRendererProps> = ({
</Box> </Box>
); );
} }
if (screenReaderEnabled) {
return (
<Box flexDirection="column">
{parsedLines.map((line, index) => (
<Text key={index}>
{line.type}: {line.content}
</Text>
))}
</Box>
);
}
// Check if the diff represents a new file (only additions and header lines) // Check if the diff represents a new file (only additions and header lines)
const isNewFile = parsedLines.every( const isNewFile = parsedLines.every(

View file

@ -8,7 +8,7 @@ import type React from 'react';
import { Text, Box } from 'ink'; import { Text, Box } from 'ink';
import { MarkdownDisplay } from '../../utils/MarkdownDisplay.js'; import { MarkdownDisplay } from '../../utils/MarkdownDisplay.js';
import { Colors } from '../../colors.js'; import { Colors } from '../../colors.js';
import { SCREEN_READER_MODEL_PREFIX } from '../../constants.js'; import { SCREEN_READER_MODEL_PREFIX } from '../../textConstants.js';
interface GeminiMessageProps { interface GeminiMessageProps {
text: string; text: string;

View file

@ -129,18 +129,22 @@ const ToolStatusIndicator: React.FC<ToolStatusIndicatorProps> = ({
/> />
)} )}
{status === ToolCallStatus.Success && ( {status === ToolCallStatus.Success && (
<Text color={Colors.AccentGreen}>{TOOL_STATUS.SUCCESS}</Text> <Text color={Colors.AccentGreen} aria-label={'Success:'}>
{TOOL_STATUS.SUCCESS}
</Text>
)} )}
{status === ToolCallStatus.Confirming && ( {status === ToolCallStatus.Confirming && (
<Text color={Colors.AccentYellow}>{TOOL_STATUS.CONFIRMING}</Text> <Text color={Colors.AccentYellow} aria-label={'Confirming:'}>
{TOOL_STATUS.CONFIRMING}
</Text>
)} )}
{status === ToolCallStatus.Canceled && ( {status === ToolCallStatus.Canceled && (
<Text color={Colors.AccentYellow} bold> <Text color={Colors.AccentYellow} aria-label={'Canceled:'} bold>
{TOOL_STATUS.CANCELED} {TOOL_STATUS.CANCELED}
</Text> </Text>
)} )}
{status === ToolCallStatus.Error && ( {status === ToolCallStatus.Error && (
<Text color={Colors.AccentRed} bold> <Text color={Colors.AccentRed} aria-label={'Error:'} bold>
{TOOL_STATUS.ERROR} {TOOL_STATUS.ERROR}
</Text> </Text>
)} )}

View file

@ -7,7 +7,7 @@
import type React from 'react'; import type React from 'react';
import { Text, Box } from 'ink'; import { Text, Box } from 'ink';
import { Colors } from '../../colors.js'; 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'; import { isSlashCommand as checkIsSlashCommand } from '../../utils/commandUtils.js';
interface UserMessageProps { interface UserMessageProps {

View file

@ -65,7 +65,6 @@ export function RadioButtonSelect<T>({
const [scrollOffset, setScrollOffset] = useState(0); const [scrollOffset, setScrollOffset] = useState(0);
const [numberInput, setNumberInput] = useState(''); const [numberInput, setNumberInput] = useState('');
const numberInputTimer = useRef<NodeJS.Timeout | null>(null); const numberInputTimer = useRef<NodeJS.Timeout | null>(null);
useEffect(() => { useEffect(() => {
const newScrollOffset = Math.max( const newScrollOffset = Math.max(
0, 0,
@ -195,7 +194,10 @@ export function RadioButtonSelect<T>({
return ( return (
<Box key={item.label} alignItems="center"> <Box key={item.label} alignItems="center">
<Box minWidth={2} flexShrink={0}> <Box minWidth={2} flexShrink={0}>
<Text color={isSelected ? Colors.AccentGreen : Colors.Foreground}> <Text
color={isSelected ? Colors.AccentGreen : Colors.Foreground}
aria-hidden
>
{isSelected ? '●' : ' '} {isSelected ? '●' : ' '}
</Text> </Text>
</Box> </Box>
@ -203,6 +205,7 @@ export function RadioButtonSelect<T>({
marginRight={1} marginRight={1}
flexShrink={0} flexShrink={0}
minWidth={itemNumberText.length} minWidth={itemNumberText.length}
aria-state={{ checked: isSelected }}
> >
<Text color={numberColor}>{itemNumberText}</Text> <Text color={numberColor}>{itemNumberText}</Text>
</Box> </Box>

View file

@ -16,10 +16,6 @@ export const STREAM_DEBOUNCE_MS = 100;
export const SHELL_COMMAND_NAME = 'Shell Command'; 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 // Tool status symbols used in ToolMessage component
export const TOOL_STATUS = { export const TOOL_STATUS = {
SUCCESS: '✓', SUCCESS: '✓',

View file

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

View file

@ -1,6 +1,6 @@
{ {
"name": "@google/gemini-cli-core", "name": "@google/gemini-cli-core",
"version": "0.3.0-preview.1", "version": "0.3.0-preview.3",
"description": "Gemini CLI Core", "description": "Gemini CLI Core",
"repository": { "repository": {
"type": "git", "type": "git",

View file

@ -4,6 +4,7 @@
* SPDX-License-Identifier: Apache-2.0 * SPDX-License-Identifier: Apache-2.0
*/ */
import type { Content } from '@google/genai';
import { createHash } from 'node:crypto'; import { createHash } from 'node:crypto';
import type { ServerGeminiStreamEvent } from '../core/turn.js'; import type { ServerGeminiStreamEvent } from '../core/turn.js';
import { GeminiEventType } 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 { LoopDetectedEvent, LoopType } from '../telemetry/types.js';
import type { Config } from '../config/config.js'; import type { Config } from '../config/config.js';
import { DEFAULT_GEMINI_FLASH_MODEL } 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 TOOL_CALL_LOOP_THRESHOLD = 5;
const CONTENT_LOOP_THRESHOLD = 10; const CONTENT_LOOP_THRESHOLD = 10;
@ -328,12 +333,35 @@ export class LoopDetectionService {
return originalChunk === currentChunk; 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) { private async checkForLoopWithLLM(signal: AbortSignal) {
const recentHistory = this.config const recentHistory = this.config
.getGeminiClient() .getGeminiClient()
.getHistory() .getHistory()
.slice(-LLM_LOOP_CHECK_HISTORY_COUNT); .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. 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: 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.`; Please analyze the conversation history to determine the possibility that the conversation is stuck in a repetitive, non-productive state.`;
const contents = [ const contents = [
...recentHistory, ...trimmedHistory,
{ role: 'user', parts: [{ text: prompt }] }, { role: 'user', parts: [{ text: prompt }] },
]; ];
const schema: Record<string, unknown> = { const schema: Record<string, unknown> = {

View file

@ -1,6 +1,6 @@
{ {
"name": "@google/gemini-cli-test-utils", "name": "@google/gemini-cli-test-utils",
"version": "0.3.0-preview.1", "version": "0.3.0-preview.3",
"private": true, "private": true,
"main": "src/index.ts", "main": "src/index.ts",
"license": "Apache-2.0", "license": "Apache-2.0",

View file

@ -2,7 +2,7 @@
"name": "gemini-cli-vscode-ide-companion", "name": "gemini-cli-vscode-ide-companion",
"displayName": "Gemini CLI Companion", "displayName": "Gemini CLI Companion",
"description": "Enable Gemini CLI with direct access to your IDE workspace.", "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", "publisher": "google",
"icon": "assets/icon.png", "icon": "assets/icon.png",
"repository": { "repository": {