mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-07-23 00:01:24 -07:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4c9746561c | |||
| ad3bc17e45 | |||
| 9e574773c9 | |||
| 2fb14ead1f | |||
| 76553622f6 | |||
| 7404949eff | |||
| 52cc0f6feb | |||
| 0c0309abdc |
@@ -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');
|
||||
|
||||
Generated
+14
-12
@@ -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
@@ -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,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": {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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: {},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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: '✓',
|
||||
|
||||
@@ -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,6 +1,6 @@
|
||||
{
|
||||
"name": "@google/gemini-cli-core",
|
||||
"version": "0.2.1",
|
||||
"version": "0.3.2",
|
||||
"description": "Gemini CLI Core",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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' });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,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",
|
||||
|
||||
@@ -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": {
|
||||
|
||||
Reference in New Issue
Block a user