Compare commits

...

8 Commits

Author SHA1 Message Date
gemini-cli-robot 4c9746561c chore(release): v0.3.2 2025-09-04 23:55:40 +00:00
Shreya Keshive ad3bc17e45 fix(process-utils): fix bug that prevented start-up when running process walking command fails (#7757) 2025-09-04 16:25:09 -07:00
gemini-cli-robot 9e574773c9 chore(release): v0.3.1 2025-09-04 19:50:25 +00:00
Shreya Keshive 2fb14ead1f Hotfix for issue #7730 (#7739)
Co-authored-by: Taylor Mullen <ntaylormullen@google.com>
Co-authored-by: Arya Gummadi <aryagummadi@google.com>
2025-09-04 12:46:02 -07:00
gemini-cli-robot 76553622f6 chore(release): v0.3.0 2025-09-04 17:49:18 +00:00
matt korwel 7404949eff 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>
2025-09-04 17:00:46 +00:00
Gal Zahavi 52cc0f6feb Fix setting migration nosiness and merging (#7571) 2025-09-02 01:23:35 +00:00
gemini-cli-robot 0c0309abdc chore(release): v0.3.0-preview.1 2025-08-28 04:33:56 +00:00
27 changed files with 361 additions and 93 deletions
+1 -1
View File
@@ -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');
+14 -12
View File
@@ -1,12 +1,12 @@
{
"name": "@google/gemini-cli",
"version": "0.2.1",
"version": "0.3.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@google/gemini-cli",
"version": "0.2.1",
"version": "0.3.2",
"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.1.0",
"version": "0.3.2",
"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.2.1",
"version": "0.3.2",
"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",
@@ -14529,6 +14529,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",
@@ -14689,9 +14690,10 @@
},
"packages/core": {
"name": "@google/gemini-cli-core",
"version": "0.2.1",
"version": "0.3.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",
@@ -14810,7 +14812,7 @@
},
"packages/test-utils": {
"name": "@google/gemini-cli-test-utils",
"version": "0.2.1",
"version": "0.3.2",
"license": "Apache-2.0",
"devDependencies": {
"typescript": "^5.3.3"
@@ -14821,7 +14823,7 @@
},
"packages/vscode-ide-companion": {
"name": "gemini-cli-vscode-ide-companion",
"version": "0.2.1",
"version": "0.3.2",
"license": "LICENSE",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.15.1",
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@google/gemini-cli",
"version": "0.2.1",
"version": "0.3.2",
"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.2.1"
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.3.2"
},
"scripts": {
"start": "node scripts/start.js",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@google/gemini-cli-a2a-server",
"version": "0.1.0",
"version": "0.3.2",
"private": true,
"description": "Gemini CLI A2A Server",
"repository": {
+4 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@google/gemini-cli",
"version": "0.2.1",
"version": "0.3.2",
"description": "Gemini CLI",
"repository": {
"type": "git",
@@ -25,7 +25,7 @@
"dist"
],
"config": {
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.2.1"
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.3.2"
},
"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",
+1 -1
View File
@@ -83,7 +83,7 @@ vi.mock('@google/gemini-cli-core', async () => {
return {
...actualServer,
IdeClient: {
getInstance: vi.fn().mockReturnValue({
getInstance: vi.fn().mockResolvedValue({
getConnectionStatus: vi.fn(),
initialize: vi.fn(),
shutdown: vi.fn(),
+3 -3
View File
@@ -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,
+54 -9
View File
@@ -142,6 +142,11 @@ describe('Settings Loading and Merging', () => {
workspacesWithMigrationNudge: [],
},
security: {},
general: {},
privacy: {},
telemetry: {},
tools: {},
ide: {},
});
expect(settings.errors.length).toBe(0);
});
@@ -197,6 +202,13 @@ describe('Settings Loading and Merging', () => {
workspacesWithMigrationNudge: [],
},
security: {},
general: {},
privacy: {},
telemetry: {},
tools: {
sandbox: false,
},
ide: {},
});
});
@@ -253,6 +265,11 @@ describe('Settings Loading and Merging', () => {
workspacesWithMigrationNudge: [],
},
security: {},
general: {},
privacy: {},
telemetry: {},
tools: {},
ide: {},
});
});
@@ -308,6 +325,10 @@ describe('Settings Loading and Merging', () => {
workspacesWithMigrationNudge: [],
},
security: {},
general: {},
privacy: {},
telemetry: {},
ide: {},
});
});
@@ -374,6 +395,10 @@ describe('Settings Loading and Merging', () => {
chatCompression: {},
},
security: {},
general: {},
privacy: {},
telemetry: {},
ide: {},
});
});
@@ -439,6 +464,7 @@ describe('Settings Loading and Merging', () => {
},
tools: {
sandbox: false,
core: ['tool1'],
},
telemetry: { enabled: false },
context: {
@@ -460,6 +486,9 @@ describe('Settings Loading and Merging', () => {
chatCompression: {},
},
security: {},
general: {},
privacy: {},
ide: {},
});
});
@@ -538,6 +567,10 @@ describe('Settings Loading and Merging', () => {
workspacesWithMigrationNudge: [],
},
security: {},
privacy: {},
telemetry: {},
tools: {},
ide: {},
});
});
@@ -668,7 +701,7 @@ describe('Settings Loading and Merging', () => {
chatCompression: {},
},
security: {},
telemetry: false,
telemetry: {},
tools: {
sandbox: false,
},
@@ -676,6 +709,9 @@ describe('Settings Loading and Merging', () => {
customThemes: {},
theme: 'system-theme',
},
general: {},
privacy: {},
ide: {},
});
});
@@ -901,7 +937,7 @@ describe('Settings Loading and Merging', () => {
(mockFsExistsSync as Mock).mockImplementation(
(p: fs.PathLike) => p === USER_SETTINGS_PATH,
);
const userSettingsContent = { telemetry: true };
const userSettingsContent = { telemetry: { enabled: true } };
(fs.readFileSync as Mock).mockImplementation(
(p: fs.PathOrFileDescriptor) => {
if (p === USER_SETTINGS_PATH)
@@ -910,14 +946,14 @@ describe('Settings Loading and Merging', () => {
},
);
const settings = loadSettings(MOCK_WORKSPACE_DIR);
expect(settings.merged.telemetry).toBe(true);
expect(settings.merged.telemetry?.enabled).toBe(true);
});
it('should load telemetry setting from workspace settings', () => {
(mockFsExistsSync as Mock).mockImplementation(
(p: fs.PathLike) => p === MOCK_WORKSPACE_SETTINGS_PATH,
);
const workspaceSettingsContent = { telemetry: false };
const workspaceSettingsContent = { telemetry: { enabled: false } };
(fs.readFileSync as Mock).mockImplementation(
(p: fs.PathOrFileDescriptor) => {
if (p === MOCK_WORKSPACE_SETTINGS_PATH)
@@ -926,13 +962,13 @@ describe('Settings Loading and Merging', () => {
},
);
const settings = loadSettings(MOCK_WORKSPACE_DIR);
expect(settings.merged.telemetry).toBe(false);
expect(settings.merged.telemetry?.enabled).toBe(false);
});
it('should prioritize workspace telemetry setting over user setting', () => {
(mockFsExistsSync as Mock).mockReturnValue(true);
const userSettingsContent = { telemetry: true };
const workspaceSettingsContent = { telemetry: false };
const userSettingsContent = { telemetry: { enabled: true } };
const workspaceSettingsContent = { telemetry: { enabled: false } };
(fs.readFileSync as Mock).mockImplementation(
(p: fs.PathOrFileDescriptor) => {
if (p === USER_SETTINGS_PATH)
@@ -943,14 +979,14 @@ describe('Settings Loading and Merging', () => {
},
);
const settings = loadSettings(MOCK_WORKSPACE_DIR);
expect(settings.merged.telemetry).toBe(false);
expect(settings.merged.telemetry?.enabled).toBe(false);
});
it('should have telemetry as undefined if not in any settings file', () => {
(mockFsExistsSync as Mock).mockReturnValue(false); // No settings files exist
(fs.readFileSync as Mock).mockReturnValue('{}');
const settings = loadSettings(MOCK_WORKSPACE_DIR);
expect(settings.merged.telemetry).toBeUndefined();
expect(settings.merged.telemetry).toEqual({});
expect(settings.merged.ui?.customThemes).toEqual({});
expect(settings.merged.mcpServers).toEqual({});
});
@@ -1400,6 +1436,11 @@ describe('Settings Loading and Merging', () => {
workspacesWithMigrationNudge: [],
},
security: {},
general: {},
privacy: {},
tools: {},
telemetry: {},
ide: {},
});
// Check that error objects are populated in settings.errors
@@ -1828,6 +1869,10 @@ describe('Settings Loading and Merging', () => {
workspacesWithMigrationNudge: [],
},
security: {},
general: {},
privacy: {},
telemetry: {},
ide: {},
});
});
});
+31 -8
View File
@@ -30,7 +30,6 @@ export const DEFAULT_EXCLUDED_ENV_VARS = ['DEBUG', 'DEBUG_MODE'];
const MIGRATE_V2_OVERWRITE = false;
// As defined in spec.md
const MIGRATION_MAP: Record<string, string> = {
preferredEditor: 'general.preferredEditor',
vimMode: 'general.vimMode',
@@ -63,6 +62,7 @@ const MIGRATION_MAP: Record<string, string> = {
fileFiltering: 'context.fileFiltering',
sandbox: 'tools.sandbox',
shouldUseNodePtyShell: 'tools.usePty',
allowedTools: 'tools.allowed',
coreTools: 'tools.core',
excludeTools: 'tools.exclude',
toolDiscoveryCommand: 'tools.discoveryCommand',
@@ -299,6 +299,12 @@ function mergeSettings(
...user,
...safeWorkspaceWithoutFolderTrust,
...system,
general: {
...(systemDefaults.general || {}),
...(user.general || {}),
...(safeWorkspaceWithoutFolderTrust.general || {}),
...(system.general || {}),
},
ui: {
...(systemDefaults.ui || {}),
...(user.ui || {}),
@@ -311,6 +317,24 @@ function mergeSettings(
...(system.ui?.customThemes || {}),
},
},
ide: {
...(systemDefaults.ide || {}),
...(user.ide || {}),
...(safeWorkspaceWithoutFolderTrust.ide || {}),
...(system.ide || {}),
},
privacy: {
...(systemDefaults.privacy || {}),
...(user.privacy || {}),
...(safeWorkspaceWithoutFolderTrust.privacy || {}),
...(system.privacy || {}),
},
telemetry: {
...(systemDefaults.telemetry || {}),
...(user.telemetry || {}),
...(safeWorkspaceWithoutFolderTrust.telemetry || {}),
...(system.telemetry || {}),
},
security: {
...(systemDefaults.security || {}),
...(user.security || {}),
@@ -329,6 +353,12 @@ function mergeSettings(
...(safeWorkspaceWithoutFolderTrust.mcpServers || {}),
...(system.mcpServers || {}),
},
tools: {
...(systemDefaults.tools || {}),
...(user.tools || {}),
...(safeWorkspaceWithoutFolderTrust.tools || {}),
...(system.tools || {}),
},
context: {
...(systemDefaults.context || {}),
...(user.context || {}),
@@ -667,7 +697,6 @@ export function loadSettings(workspaceDir: string): LoadedSettings {
let settingsObject = rawSettings as Record<string, unknown>;
if (needsMigration(settingsObject)) {
console.error(`Legacy settings file detected at: ${filePath}`);
const migratedSettings = migrateSettingsToV2(settingsObject);
if (migratedSettings) {
if (MIGRATE_V2_OVERWRITE) {
@@ -678,9 +707,6 @@ export function loadSettings(workspaceDir: string): LoadedSettings {
JSON.stringify(migratedSettings, null, 2),
'utf-8',
);
console.log(
`Successfully migrated and saved settings file: ${filePath}`,
);
} catch (e) {
console.error(
`Error migrating settings file on disk: ${getErrorMessage(
@@ -689,9 +715,6 @@ export function loadSettings(workspaceDir: string): LoadedSettings {
);
}
} else {
console.log(
`Successfully migrated settings for ${filePath} in-memory for the current session.`,
);
migratedInMemorScopes.add(scope);
}
settingsObject = migratedSettings;
+1 -1
View File
@@ -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,
@@ -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 <Spinner type={spinnerType} />;
return isScreenReaderEnabled ? (
<Text>{SCREEN_READER_RESPONDING}</Text>
) : (
<Spinner type={spinnerType} />
);
} else if (nonRespondingDisplay) {
return <Text>{nonRespondingDisplay}</Text>;
return isScreenReaderEnabled ? (
<Text>{SCREEN_READER_LOADING}</Text>
) : (
<Text>{nonRespondingDisplay}</Text>
);
}
return null;
};
@@ -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;
@@ -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;
@@ -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<DiffRendererProps> = ({
terminalWidth,
theme,
}) => {
const screenReaderEnabled = useIsScreenReaderEnabled();
if (!diffContent || typeof diffContent !== 'string') {
return <Text color={Colors.AccentYellow}>No diff content.</Text>;
}
@@ -120,6 +121,17 @@ export const DiffRenderer: React.FC<DiffRendererProps> = ({
</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)
const isNewFile = parsedLines.every(
@@ -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;
@@ -129,18 +129,22 @@ const ToolStatusIndicator: React.FC<ToolStatusIndicatorProps> = ({
/>
)}
{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 && (
<Text color={Colors.AccentYellow}>{TOOL_STATUS.CONFIRMING}</Text>
<Text color={Colors.AccentYellow} aria-label={'Confirming:'}>
{TOOL_STATUS.CONFIRMING}
</Text>
)}
{status === ToolCallStatus.Canceled && (
<Text color={Colors.AccentYellow} bold>
<Text color={Colors.AccentYellow} aria-label={'Canceled:'} bold>
{TOOL_STATUS.CANCELED}
</Text>
)}
{status === ToolCallStatus.Error && (
<Text color={Colors.AccentRed} bold>
<Text color={Colors.AccentRed} aria-label={'Error:'} bold>
{TOOL_STATUS.ERROR}
</Text>
)}
@@ -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 {
@@ -65,7 +65,6 @@ export function RadioButtonSelect<T>({
const [scrollOffset, setScrollOffset] = useState(0);
const [numberInput, setNumberInput] = useState('');
const numberInputTimer = useRef<NodeJS.Timeout | null>(null);
useEffect(() => {
const newScrollOffset = Math.max(
0,
@@ -195,7 +194,10 @@ export function RadioButtonSelect<T>({
return (
<Box key={item.label} alignItems="center">
<Box minWidth={2} flexShrink={0}>
<Text color={isSelected ? Colors.AccentGreen : Colors.Foreground}>
<Text
color={isSelected ? Colors.AccentGreen : Colors.Foreground}
aria-hidden
>
{isSelected ? '●' : ' '}
</Text>
</Box>
@@ -203,6 +205,7 @@ export function RadioButtonSelect<T>({
marginRight={1}
flexShrink={0}
minWidth={itemNumberText.length}
aria-state={{ checked: isSelected }}
>
<Text color={numberColor}>{itemNumberText}</Text>
</Box>
-4
View File
@@ -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: '✓',
+13
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';
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@google/gemini-cli-core",
"version": "0.2.1",
"version": "0.3.2",
"description": "Gemini CLI Core",
"repository": {
"type": "git",
+10
View File
@@ -100,6 +100,16 @@ vi.mock('../services/gitService.js', () => {
return { GitService: GitServiceMock };
});
vi.mock('../ide/ide-client.js', () => ({
IdeClient: {
getInstance: vi.fn().mockResolvedValue({
getConnectionStatus: vi.fn(),
initialize: vi.fn(),
shutdown: vi.fn(),
}),
},
}));
describe('Server Config (config.ts)', () => {
const MODEL = 'gemini-pro';
const SANDBOX: SandboxConfig = {
+110 -4
View File
@@ -66,13 +66,34 @@ describe('getIdeProcessInfo', () => {
it('should traverse up and find the great-grandchild of the root process', async () => {
(os.platform as Mock).mockReturnValue('win32');
const processInfoMap = new Map([
[1000, { stdout: 'ParentProcessId=900\r\nCommandLine=node.exe\r\n' }],
[
1000,
{
stdout:
'{"Name":"node.exe","ParentProcessId":900,"CommandLine":"node.exe"}',
},
],
[
900,
{ stdout: 'ParentProcessId=800\r\nCommandLine=powershell.exe\r\n' },
{
stdout:
'{"Name":"powershell.exe","ParentProcessId":800,"CommandLine":"powershell.exe"}',
},
],
[
800,
{
stdout:
'{"Name":"code.exe","ParentProcessId":700,"CommandLine":"code.exe"}',
},
],
[
700,
{
stdout:
'{"Name":"wininit.exe","ParentProcessId":0,"CommandLine":"wininit.exe"}',
},
],
[800, { stdout: 'ParentProcessId=700\r\nCommandLine=code.exe\r\n' }],
[700, { stdout: 'ParentProcessId=0\r\nCommandLine=wininit.exe\r\n' }],
]);
mockedExec.mockImplementation((command: string) => {
const pidMatch = command.match(/ProcessId=(\d+)/);
@@ -86,5 +107,90 @@ describe('getIdeProcessInfo', () => {
const result = await getIdeProcessInfo();
expect(result).toEqual({ pid: 900, command: 'powershell.exe' });
});
it('should handle non-existent process gracefully', async () => {
(os.platform as Mock).mockReturnValue('win32');
mockedExec
.mockResolvedValueOnce({ stdout: '' }) // Non-existent PID returns empty due to -ErrorAction SilentlyContinue
.mockResolvedValueOnce({
stdout:
'{"Name":"fallback.exe","ParentProcessId":0,"CommandLine":"fallback.exe"}',
}); // Fallback call
const result = await getIdeProcessInfo();
expect(result).toEqual({ pid: 1000, command: 'fallback.exe' });
});
it('should handle malformed JSON output gracefully', async () => {
(os.platform as Mock).mockReturnValue('win32');
mockedExec
.mockResolvedValueOnce({ stdout: '{"invalid":json}' }) // Malformed JSON
.mockResolvedValueOnce({
stdout:
'{"Name":"fallback.exe","ParentProcessId":0,"CommandLine":"fallback.exe"}',
}); // Fallback call
const result = await getIdeProcessInfo();
expect(result).toEqual({ pid: 1000, command: 'fallback.exe' });
});
it('should handle PowerShell errors without crashing the process chain', async () => {
(os.platform as Mock).mockReturnValue('win32');
const processInfoMap = new Map([
[1000, { stdout: '' }], // First process doesn't exist (empty due to -ErrorAction)
[
1001,
{
stdout:
'{"Name":"parent.exe","ParentProcessId":800,"CommandLine":"parent.exe"}',
},
],
[
800,
{
stdout:
'{"Name":"ide.exe","ParentProcessId":0,"CommandLine":"ide.exe"}',
},
],
]);
// Mock the process.pid to test traversal with missing processes
Object.defineProperty(process, 'pid', {
value: 1001,
configurable: true,
});
mockedExec.mockImplementation((command: string) => {
const pidMatch = command.match(/ProcessId=(\d+)/);
if (pidMatch) {
const pid = parseInt(pidMatch[1], 10);
return Promise.resolve(processInfoMap.get(pid) || { stdout: '' });
}
return Promise.reject(new Error('Invalid command for mock'));
});
const result = await getIdeProcessInfo();
// Should return the current process command since traversal continues despite missing processes
expect(result).toEqual({ pid: 1001, command: 'parent.exe' });
// Reset process.pid
Object.defineProperty(process, 'pid', {
value: 1000,
configurable: true,
});
});
it('should handle partial JSON data with defaults', async () => {
(os.platform as Mock).mockReturnValue('win32');
mockedExec
.mockResolvedValueOnce({ stdout: '{"Name":"partial.exe"}' }) // Missing ParentProcessId, defaults to 0
.mockResolvedValueOnce({
stdout:
'{"Name":"root.exe","ParentProcessId":0,"CommandLine":"root.exe"}',
}); // Get grandparent info
const result = await getIdeProcessInfo();
expect(result).toEqual({ pid: 1000, command: 'root.exe' });
});
});
});
+38 -25
View File
@@ -24,30 +24,44 @@ async function getProcessInfo(pid: number): Promise<{
name: string;
command: string;
}> {
const platform = os.platform();
if (platform === 'win32') {
const command = `wmic process where "ProcessId=${pid}" get Name,ParentProcessId,CommandLine /value`;
const { stdout } = await execAsync(command);
const nameMatch = stdout.match(/Name=([^\n]*)/);
const processName = nameMatch ? nameMatch[1].trim() : '';
const ppidMatch = stdout.match(/ParentProcessId=(\d+)/);
const parentPid = ppidMatch ? parseInt(ppidMatch[1], 10) : 0;
const commandLineMatch = stdout.match(/CommandLine=([^\n]*)/);
const commandLine = commandLineMatch ? commandLineMatch[1].trim() : '';
return { parentPid, name: processName, command: commandLine };
} else {
const command = `ps -o ppid=,command= -p ${pid}`;
const { stdout } = await execAsync(command);
const trimmedStdout = stdout.trim();
const ppidString = trimmedStdout.split(/\s+/)[0];
const parentPid = parseInt(ppidString, 10);
const fullCommand = trimmedStdout.substring(ppidString.length).trim();
const processName = path.basename(fullCommand.split(' ')[0]);
return {
parentPid: isNaN(parentPid) ? 1 : parentPid,
name: processName,
command: fullCommand,
};
try {
const platform = os.platform();
if (platform === 'win32') {
const powershellCommand = [
'$p = Get-CimInstance Win32_Process',
`-Filter 'ProcessId=${pid}'`,
'-ErrorAction SilentlyContinue;',
'if ($p) {',
'@{Name=$p.Name;ParentProcessId=$p.ParentProcessId;CommandLine=$p.CommandLine}',
'| ConvertTo-Json',
'}',
].join(' ');
const { stdout } = await execAsync(`powershell "${powershellCommand}"`);
const output = stdout.trim();
if (!output) return { parentPid: 0, name: '', command: '' };
const {
Name = '',
ParentProcessId = 0,
CommandLine = '',
} = JSON.parse(output);
return { parentPid: ParentProcessId, name: Name, command: CommandLine };
} else {
const command = `ps -o ppid=,command= -p ${pid}`;
const { stdout } = await execAsync(command);
const trimmedStdout = stdout.trim();
const ppidString = trimmedStdout.split(/\s+/)[0];
const parentPid = parseInt(ppidString, 10);
const fullCommand = trimmedStdout.substring(ppidString.length).trim();
const processName = path.basename(fullCommand.split(' ')[0]);
return {
parentPid: isNaN(parentPid) ? 1 : parentPid,
name: processName,
command: fullCommand,
};
}
} catch (_e) {
console.debug(`Failed to get process info for pid ${pid}:`, _e);
return { parentPid: 0, name: '', command: '' };
}
}
@@ -160,7 +174,6 @@ async function getIdeProcessInfoForWindows(): Promise<{
* top-level ancestor process ID and command as a fallback.
*
* @returns A promise that resolves to the PID and command of the IDE process.
* @throws Will throw an error if the underlying shell commands fail.
*/
export async function getIdeProcessInfo(): Promise<{
pid: number;
@@ -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<string, unknown> = {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@google/gemini-cli-test-utils",
"version": "0.2.1",
"version": "0.3.2",
"private": true,
"main": "src/index.ts",
"license": "Apache-2.0",
+1 -1
View File
@@ -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.2.1",
"version": "0.3.2",
"publisher": "google",
"icon": "assets/icon.png",
"repository": {