Merge remote-tracking branch 'origin/main' into st/chore/clean-up-memory

# Conflicts:
#	packages/cli/src/config/config.ts
This commit is contained in:
Sandy Tao
2026-05-13 10:14:30 -07:00
91 changed files with 2099 additions and 1197 deletions
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@google/gemini-cli",
"version": "0.42.0-nightly.20260428.g59b2dea0e",
"version": "0.44.0-nightly.20260512.g022e8baef",
"description": "Gemini CLI",
"license": "Apache-2.0",
"repository": {
@@ -27,7 +27,7 @@
"dist"
],
"config": {
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.42.0-nightly.20260428.g59b2dea0e"
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.44.0-nightly.20260512.g022e8baef"
},
"dependencies": {
"@agentclientprotocol/sdk": "^0.16.1",
+5 -2
View File
@@ -60,8 +60,11 @@ export class AcpFileSystemService implements FileSystemService {
sessionId: this.sessionId,
});
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return response.content;
const content: unknown = response.content;
if (typeof content !== 'string') {
throw new Error('content must be a string'); // replace with other response type formats when modified in the future
}
return content;
} catch (err: unknown) {
this.normalizeFileSystemError(err);
}
@@ -19,6 +19,7 @@ import type * as acp from '@agentclientprotocol/sdk';
import {
AuthType,
type Config,
GEMINI_MODEL_ALIAS_AUTO,
type MessageBus,
type Storage,
} from '@google/gemini-cli-core';
@@ -208,7 +209,7 @@ describe('AcpSessionManager', () => {
expect(response.models?.availableModels).toEqual(
expect.arrayContaining([
expect.objectContaining({
modelId: 'auto-gemini-3',
modelId: GEMINI_MODEL_ALIAS_AUTO,
name: expect.stringContaining('Auto'),
}),
]),
+10 -17
View File
@@ -10,8 +10,7 @@ import {
type ToolCallConfirmationDetails,
Kind,
ApprovalMode,
DEFAULT_GEMINI_MODEL_AUTO,
PREVIEW_GEMINI_MODEL_AUTO,
GEMINI_MODEL_ALIAS_AUTO,
DEFAULT_GEMINI_MODEL,
DEFAULT_GEMINI_FLASH_MODEL,
DEFAULT_GEMINI_FLASH_LITE_MODEL,
@@ -23,6 +22,8 @@ import {
getDisplayString,
AuthType,
ToolConfirmationOutcome,
getChannelFromVersion,
getAutoModelDescription,
} from '@google/gemini-cli-core';
import type * as acp from '@agentclientprotocol/sdk';
import { z } from 'zod';
@@ -262,7 +263,7 @@ export function buildAvailableModels(
}>;
currentModelId: string;
} {
const preferredModel = config.getModel() || DEFAULT_GEMINI_MODEL_AUTO;
const preferredModel = config.getModel() || GEMINI_MODEL_ALIAS_AUTO;
const shouldShowPreviewModels = config.getHasAccessToPreviewModel();
const useGemini31 = config.getGemini31LaunchedSync?.() ?? false;
const useGemini31FlashLite =
@@ -271,6 +272,8 @@ export function buildAvailableModels(
const useCustomToolModel =
useGemini31 && selectedAuthType === AuthType.USE_GEMINI;
const releaseChannel = getChannelFromVersion(config.clientVersion);
// --- DYNAMIC PATH ---
if (
config.getExperimentalDynamicModelConfiguration?.() === true &&
@@ -281,6 +284,7 @@ export function buildAvailableModels(
useGemini3_1FlashLite: useGemini31FlashLite,
useCustomTools: useCustomToolModel,
hasAccessToPreview: shouldShowPreviewModels,
releaseChannel,
});
return {
@@ -292,23 +296,12 @@ export function buildAvailableModels(
// --- LEGACY PATH ---
const mainOptions = [
{
value: DEFAULT_GEMINI_MODEL_AUTO,
title: getDisplayString(DEFAULT_GEMINI_MODEL_AUTO),
description:
'Let Gemini CLI decide the best model for the task: gemini-2.5-pro, gemini-2.5-flash',
value: GEMINI_MODEL_ALIAS_AUTO,
title: getDisplayString(GEMINI_MODEL_ALIAS_AUTO),
description: getAutoModelDescription(releaseChannel, useGemini31),
},
];
if (shouldShowPreviewModels) {
mainOptions.unshift({
value: PREVIEW_GEMINI_MODEL_AUTO,
title: getDisplayString(PREVIEW_GEMINI_MODEL_AUTO),
description: useGemini31
? 'Let Gemini CLI decide the best model for the task: gemini-3.1-pro, gemini-3-flash'
: 'Let Gemini CLI decide the best model for the task: gemini-3-pro, gemini-3-flash',
});
}
const manualOptions = [
{
value: DEFAULT_GEMINI_MODEL,
@@ -27,7 +27,7 @@ export interface ConfigLogger {
export type RequestSettingCallback = (
setting: ExtensionSetting,
) => Promise<string>;
) => Promise<string | undefined>;
export type RequestConfirmationCallback = (message: string) => Promise<boolean>;
const defaultLogger: ConfigLogger = {
@@ -47,8 +47,7 @@ const defaultRequestConfirmation: RequestConfirmationCallback = async (
message,
initial: false,
});
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return response.confirm;
return typeof response.confirm === 'boolean' ? response.confirm : false;
};
export async function getExtensionManager() {
+2 -2
View File
@@ -1891,7 +1891,7 @@ describe('loadCliConfig model selection', () => {
argv,
);
expect(config.getModel()).toBe('auto-gemini-3');
expect(config.getModel()).toBe('auto');
});
it('always prefers model from argv', async () => {
@@ -1935,7 +1935,7 @@ describe('loadCliConfig model selection', () => {
argv,
);
expect(config.getModel()).toBe('auto-gemini-3');
expect(config.getModel()).toBe('auto');
});
});
+1 -2
View File
@@ -28,7 +28,6 @@ import {
debugLogger,
ASK_USER_TOOL_NAME,
getVersion,
PREVIEW_GEMINI_MODEL_AUTO,
coreEvents,
GEMINI_MODEL_ALIAS_AUTO,
getAdminErrorMessage,
@@ -825,7 +824,7 @@ export async function loadCliConfig(
interactive,
);
const defaultModel = PREVIEW_GEMINI_MODEL_AUTO;
const defaultModel = GEMINI_MODEL_ALIAS_AUTO;
const rawModel =
argv.model || process.env['GEMINI_MODEL'] || settings.model?.name;
+5 -3
View File
@@ -88,7 +88,9 @@ interface ExtensionManagerParams {
enabledExtensionOverrides?: string[];
settings: MergedSettings;
requestConsent: (consent: string) => Promise<boolean>;
requestSetting: ((setting: ExtensionSetting) => Promise<string>) | null;
requestSetting:
| ((setting: ExtensionSetting) => Promise<string | undefined>)
| null;
workspaceDir: string;
eventEmitter?: EventEmitter<ExtensionEvents>;
clientVersion?: string;
@@ -106,7 +108,7 @@ export class ExtensionManager extends ExtensionLoader {
private settings: MergedSettings;
private requestConsent: (consent: string) => Promise<boolean>;
private requestSetting:
| ((setting: ExtensionSetting) => Promise<string>)
| ((setting: ExtensionSetting) => Promise<string | undefined>)
| undefined;
private telemetryConfig: Config;
private workspaceDir: string;
@@ -161,7 +163,7 @@ export class ExtensionManager extends ExtensionLoader {
}
setRequestSetting(
requestSetting?: (setting: ExtensionSetting) => Promise<string>,
requestSetting?: (setting: ExtensionSetting) => Promise<string | undefined>,
): void {
this.requestSetting = requestSetting;
}
@@ -94,9 +94,8 @@ export class ExtensionRegistryClient {
fuzzy: true,
});
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const results = await fzf.find(query);
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return results.map((r: { item: RegistryExtension }) => r.item);
const results: Array<{ item: RegistryExtension }> = await fzf.find(query);
return results.map((r) => r.item);
}
async getExtension(id: string): Promise<RegistryExtension | undefined> {
@@ -8,6 +8,7 @@ import fs from 'node:fs';
import path from 'node:path';
import { coreEvents, type GeminiCLIExtension } from '@google/gemini-cli-core';
import { ExtensionStorage } from './storage.js';
import { z } from 'zod';
export interface ExtensionEnablementConfig {
overrides: string[];
@@ -179,8 +180,12 @@ export class ExtensionEnablementManager {
readConfig(): AllExtensionsEnablementConfig {
try {
const content = fs.readFileSync(this.configFilePath, 'utf-8');
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return JSON.parse(content);
const parsed: unknown = JSON.parse(content);
const schema = z.record(
z.string(),
z.object({ overrides: z.array(z.string()) }),
);
return schema.parse(parsed);
} catch (error) {
if (
error instanceof Error &&
@@ -62,7 +62,7 @@ export const getEnvFilePath = (
export async function maybePromptForSettings(
extensionConfig: ExtensionConfig,
extensionId: string,
requestSetting: (setting: ExtensionSetting) => Promise<string>,
requestSetting: (setting: ExtensionSetting) => Promise<string | undefined>,
previousExtensionConfig?: ExtensionConfig,
previousSettings?: Record<string, string>,
): Promise<void> {
@@ -106,7 +106,9 @@ export async function maybePromptForSettings(
settingsChanges.promptForEnv,
)) {
const answer = await requestSetting(setting);
allSettings[setting.envVar] = answer;
if (answer !== undefined) {
allSettings[setting.envVar] = answer;
}
}
const nonSensitiveSettings: Record<string, string> = {};
@@ -159,14 +161,13 @@ function formatEnvContent(settings: Record<string, string>): string {
export async function promptForSetting(
setting: ExtensionSetting,
): Promise<string> {
): Promise<string | undefined> {
const response = await prompts({
type: setting.sensitive ? 'password' : 'text',
name: 'value',
message: `${setting.name}\n${setting.description}`,
});
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return response.value;
return typeof response.value === 'string' ? response.value : undefined;
}
export async function getScopedEnvContents(
@@ -230,7 +231,7 @@ export async function updateSetting(
extensionConfig: ExtensionConfig,
extensionId: string,
settingKey: string,
requestSetting: (setting: ExtensionSetting) => Promise<string>,
requestSetting: (setting: ExtensionSetting) => Promise<string | undefined>,
scope: ExtensionSettingScope,
workspaceDir: string,
): Promise<void> {
@@ -250,6 +251,10 @@ export async function updateSetting(
}
const newValue = await requestSetting(settingToUpdate);
if (newValue === undefined) {
return;
}
const keychain = new KeychainTokenStorage(
getKeychainStorageName(extensionName, extensionId, scope, workspaceDir),
);
@@ -67,8 +67,7 @@ export function recursivelyHydrateStrings<T>(
}
if (Array.isArray(obj)) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
return obj.map((item) =>
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return (obj as unknown[]).map((item) =>
recursivelyHydrateStrings(item, values),
) as unknown as T;
}
+5 -1
View File
@@ -3451,7 +3451,11 @@ export const SETTINGS_SCHEMA_DEFINITIONS: Record<
family: { type: 'string' },
isPreview: { type: 'boolean' },
isVisible: { type: 'boolean' },
dialogDescription: { type: 'string' },
dialogDescription: {
type: 'string',
description:
"A description of the model to display in the model selection dialog. For the 'auto' alias, this value is dynamically generated and any value provided here will be ignored.",
},
features: {
type: 'object',
properties: {
@@ -112,5 +112,11 @@ export const createMockCommandContext = (
return output;
};
return merge(defaultMocks, overrides);
const merged: unknown = merge(defaultMocks, overrides);
const isCommandContext = (val: unknown): val is CommandContext =>
typeof val === 'object' && val !== null;
if (isCommandContext(merged)) {
return merged;
}
throw new Error('Unreachable');
};
@@ -1581,4 +1581,71 @@ describe('AskUserDialog', () => {
expect(frame).toContain('1. Option 1');
});
});
it('indents multi-line descriptions correctly', async () => {
const questions: Question[] = [
{
question: 'Single choice?',
header: 'Indent Test',
type: QuestionType.CHOICE,
options: [
{
label: 'Option 1',
description:
'This is a very long description that is expected to wrap onto multiple lines in a narrow terminal. We want to ensure that all lines are correctly indented.',
},
],
multiSelect: false,
},
];
const { lastFrame, waitUntilReady } = await renderWithProviders(
<AskUserDialog
questions={questions}
onSubmit={vi.fn()}
onCancel={vi.fn()}
width={40} // Narrow width to force wrapping
/>,
{ width: 40 },
);
await waitFor(async () => {
await waitUntilReady();
// Snapshot will capture the visual alignment
expect(lastFrame()).toMatchSnapshot();
});
});
it('indents multi-line descriptions correctly in multi-select mode', async () => {
const questions: Question[] = [
{
question: 'Multi-select?',
header: 'Indent Test',
type: QuestionType.CHOICE,
options: [
{
label: 'Option 1',
description:
'This is a very long description that is expected to wrap onto multiple lines in a narrow terminal. We want to ensure that all lines are correctly indented even with checkboxes.',
},
],
multiSelect: true,
},
];
const { lastFrame, waitUntilReady } = await renderWithProviders(
<AskUserDialog
questions={questions}
onSubmit={vi.fn()}
onCancel={vi.fn()}
width={40} // Narrow width to force wrapping
/>,
{ width: 40 },
);
await waitFor(async () => {
await waitUntilReady();
expect(lastFrame()).toMatchSnapshot();
});
});
});
@@ -1004,13 +1004,15 @@ const ChoiceQuestionView: React.FC<ChoiceQuestionViewProps> = ({
)}
</Box>
{optionItem.description && (
<Text color={theme.text.secondary} wrap="wrap">
{' '}
<RenderInline
text={optionItem.description}
defaultColor={theme.text.secondary}
/>
</Text>
// Padding aligns with option label: 4 for multi-select (checkbox + space), 1 for single-select
<Box paddingLeft={showCheck ? 4 : 1}>
<Text color={theme.text.secondary} wrap="wrap">
<RenderInline
text={optionItem.description}
defaultColor={theme.text.secondary}
/>
</Text>
</Box>
)}
</Box>
);
@@ -12,7 +12,7 @@ import { waitFor } from '../../test-utils/async.js';
import { createMockSettings } from '../../test-utils/settings.js';
import {
DEFAULT_GEMINI_MODEL,
DEFAULT_GEMINI_MODEL_AUTO,
GEMINI_MODEL_ALIAS_AUTO,
DEFAULT_GEMINI_FLASH_MODEL,
DEFAULT_GEMINI_FLASH_LITE_MODEL,
PREVIEW_GEMINI_MODEL,
@@ -93,7 +93,7 @@ describe('<ModelDialog />', () => {
beforeEach(() => {
vi.resetAllMocks();
mockGetModel.mockReturnValue(DEFAULT_GEMINI_MODEL_AUTO);
mockGetModel.mockReturnValue(GEMINI_MODEL_ALIAS_AUTO);
mockGetHasAccessToPreviewModel.mockReturnValue(false);
mockGetGemini31LaunchedSync.mockReturnValue(false);
mockGetGemini31FlashLiteLaunchedSync.mockReturnValue(false);
@@ -102,8 +102,7 @@ describe('<ModelDialog />', () => {
// Default implementation for getDisplayString
mockGetDisplayString.mockImplementation((val: string) => {
if (val === 'auto-gemini-2.5') return 'Auto (Gemini 2.5)';
if (val === 'auto-gemini-3') return 'Auto (Preview)';
if (val === 'auto') return 'Auto';
return val;
});
});
@@ -234,7 +233,7 @@ describe('<ModelDialog />', () => {
await waitFor(() => {
expect(mockSetModel).toHaveBeenCalledWith(
DEFAULT_GEMINI_MODEL_AUTO,
GEMINI_MODEL_ALIAS_AUTO,
true, // Session only by default
);
expect(mockOnClose).toHaveBeenCalled();
@@ -292,7 +291,7 @@ describe('<ModelDialog />', () => {
await waitFor(() => {
expect(mockSetModel).toHaveBeenCalledWith(
DEFAULT_GEMINI_MODEL_AUTO,
GEMINI_MODEL_ALIAS_AUTO,
false, // Persist enabled
);
expect(mockOnClose).toHaveBeenCalled();
@@ -355,7 +354,7 @@ describe('<ModelDialog />', () => {
mockGetModel.mockReturnValue(DEFAULT_GEMINI_MODEL);
mockGetDisplayString.mockImplementation((val: string) => {
if (val === DEFAULT_GEMINI_MODEL) return 'My Custom Model Display';
if (val === 'auto-gemini-2.5') return 'Auto (Gemini 2.5)';
if (val === 'auto') return 'Auto';
return val;
});
const { lastFrame, unmount } = await renderComponent();
@@ -369,9 +368,9 @@ describe('<ModelDialog />', () => {
mockGetHasAccessToPreviewModel.mockReturnValue(true);
});
it('shows Auto (Preview) in main view when access is granted', async () => {
it('shows Auto in main view when access is granted', async () => {
const { lastFrame, unmount } = await renderComponent();
expect(lastFrame()).toContain('Auto (Preview)');
expect(lastFrame()).toContain('Auto');
unmount();
});
+17 -18
View File
@@ -14,11 +14,10 @@ import {
PREVIEW_GEMINI_3_1_MODEL,
PREVIEW_GEMINI_FLASH_MODEL,
PREVIEW_GEMINI_3_1_FLASH_LITE_MODEL,
PREVIEW_GEMINI_MODEL_AUTO,
DEFAULT_GEMINI_MODEL,
DEFAULT_GEMINI_FLASH_MODEL,
DEFAULT_GEMINI_FLASH_LITE_MODEL,
DEFAULT_GEMINI_MODEL_AUTO,
GEMINI_MODEL_ALIAS_AUTO,
GEMMA_4_31B_IT_MODEL,
GEMMA_4_26B_A4B_IT_MODEL,
ModelSlashCommandEvent,
@@ -27,6 +26,8 @@ import {
AuthType,
PREVIEW_GEMINI_3_1_CUSTOM_TOOLS_MODEL,
isProModel,
getChannelFromVersion,
getAutoModelDescription,
} from '@google/gemini-cli-core';
import { useKeypress } from '../hooks/useKeypress.js';
import { theme } from '../semantic-colors.js';
@@ -63,7 +64,7 @@ export function ModelDialog({ onClose }: ModelDialogProps): React.JSX.Element {
}, [config]);
// Determine the Preferred Model (read once when the dialog opens).
const preferredModel = config?.getModel() || DEFAULT_GEMINI_MODEL_AUTO;
const preferredModel = config?.getModel() || GEMINI_MODEL_ALIAS_AUTO;
const shouldShowPreviewModels = config?.getHasAccessToPreviewModel();
const useGemini31 = config?.getGemini31LaunchedSync?.() ?? false;
@@ -122,6 +123,11 @@ export function ModelDialog({ onClose }: ModelDialogProps): React.JSX.Element {
{ isActive: true },
);
const releaseChannel = useMemo(
() => getChannelFromVersion(config?.clientVersion ?? ''),
[config?.clientVersion],
);
const mainOptions = useMemo(() => {
// --- DYNAMIC PATH ---
if (
@@ -136,6 +142,7 @@ export function ModelDialog({ onClose }: ModelDialogProps): React.JSX.Element {
useCustomTools: useCustomToolModel,
hasAccessToPreview: shouldShowPreviewModels,
hasAccessToProModel,
releaseChannel,
});
const list = allOptions
@@ -161,11 +168,10 @@ export function ModelDialog({ onClose }: ModelDialogProps): React.JSX.Element {
// --- LEGACY PATH ---
const list = [
{
value: DEFAULT_GEMINI_MODEL_AUTO,
title: getDisplayString(DEFAULT_GEMINI_MODEL_AUTO),
description:
'Let Gemini CLI decide the best model for the task: gemini-2.5-pro, gemini-2.5-flash',
key: DEFAULT_GEMINI_MODEL_AUTO,
value: GEMINI_MODEL_ALIAS_AUTO,
title: getDisplayString(GEMINI_MODEL_ALIAS_AUTO),
description: getAutoModelDescription(releaseChannel, useGemini31),
key: GEMINI_MODEL_ALIAS_AUTO,
},
{
value: 'Manual',
@@ -177,16 +183,6 @@ export function ModelDialog({ onClose }: ModelDialogProps): React.JSX.Element {
},
];
if (shouldShowPreviewModels) {
list.unshift({
value: PREVIEW_GEMINI_MODEL_AUTO,
title: getDisplayString(PREVIEW_GEMINI_MODEL_AUTO),
description: useGemini31
? 'Let Gemini CLI decide the best model for the task: gemini-3.1-pro, gemini-3-flash'
: 'Let Gemini CLI decide the best model for the task: gemini-3-pro, gemini-3-flash',
key: PREVIEW_GEMINI_MODEL_AUTO,
});
}
return list;
}, [
config,
@@ -196,6 +192,7 @@ export function ModelDialog({ onClose }: ModelDialogProps): React.JSX.Element {
useGemini31FlashLite,
useCustomToolModel,
hasAccessToProModel,
releaseChannel,
]);
const manualOptions = useMemo(() => {
@@ -212,6 +209,7 @@ export function ModelDialog({ onClose }: ModelDialogProps): React.JSX.Element {
useCustomTools: useCustomToolModel,
hasAccessToPreview: shouldShowPreviewModels,
hasAccessToProModel,
releaseChannel,
});
return allOptions
@@ -304,6 +302,7 @@ export function ModelDialog({ onClose }: ModelDialogProps): React.JSX.Element {
useGemini31FlashLite,
useCustomToolModel,
hasAccessToProModel,
releaseChannel,
config,
]);
@@ -111,6 +111,42 @@ Enter to select · ↑/↓ to navigate · Esc to cancel
"
`;
exports[`AskUserDialog > indents multi-line descriptions correctly 1`] = `
"Single choice?
● 1. Option 1
This is a very long description
that is expected to wrap onto
multiple lines in a narrow
terminal. We want to ensure that
all lines are correctly indented.
2. Enter a custom value
Enter to select · ↑/↓ to navigate · Esc
to cancel
"
`;
exports[`AskUserDialog > indents multi-line descriptions correctly in multi-select mode 1`] = `
"Multi-select?
(Select all that apply)
● 1. [ ] Option 1
This is a very long description
that is expected to wrap onto
multiple lines in a narrow
terminal. We want to ensure
that all lines are correctly
indented even with checkboxes.
2. [ ] Enter a custom value
Done
Finish selection
Enter to select · ↑/↓ to navigate · Esc
to cancel
"
`;
exports[`AskUserDialog > renders question and options 1`] = `
"Which authentication method should we use?
@@ -188,7 +224,7 @@ exports[`AskUserDialog > verifies "All of the above" visual state with snapshot
1. [x] TypeScript
2. [x] ESLint
● 3. [x] All of the above
Select all options
Select all options
4. [ ] Enter a custom value
Done
Finish selection
@@ -6,7 +6,11 @@
import { waitFor } from '../../../test-utils/async.js';
import { renderWithProviders } from '../../../test-utils/render.js';
import { SubagentGroupDisplay } from './SubagentGroupDisplay.js';
import { Kind, CoreToolCallStatus } from '@google/gemini-cli-core';
import {
Kind,
CoreToolCallStatus,
SubagentState,
} from '@google/gemini-cli-core';
import type { IndividualToolCallDisplay } from '../../types.js';
import { describe, it, expect, vi } from 'vitest';
import { Text } from 'ink';
@@ -27,12 +31,12 @@ describe('<SubagentGroupDisplay />', () => {
resultDisplay: {
isSubagentProgress: true,
agentName: 'api-monitor',
state: 'running',
state: SubagentState.RUNNING,
recentActivity: [
{
id: 'act-1',
type: 'tool_call',
status: 'running',
status: SubagentState.RUNNING,
content: '',
displayName: 'Action Required',
description: 'Verify server is running',
@@ -50,13 +54,13 @@ describe('<SubagentGroupDisplay />', () => {
resultDisplay: {
isSubagentProgress: true,
agentName: 'db-manager',
state: 'completed',
state: SubagentState.COMPLETED,
result: 'Database schema validated',
recentActivity: [
{
id: 'act-2',
type: 'thought',
status: 'completed',
status: SubagentState.COMPLETED,
content: 'Database schema validated',
},
],
@@ -13,6 +13,7 @@ import {
isSubagentProgress,
checkExhaustive,
type SubagentActivityItem,
SubagentState,
} from '@google/gemini-cli-core';
import {
SubagentProgressDisplay,
@@ -66,13 +67,13 @@ export const SubagentGroupDisplay: React.FC<SubagentGroupDisplayProps> = ({
const singleAgent = toolCalls[0].resultDisplay;
if (isSubagentProgress(singleAgent)) {
switch (singleAgent.state) {
case 'completed':
case SubagentState.COMPLETED:
headerText = 'Agent Completed';
break;
case 'cancelled':
case SubagentState.CANCELLED:
headerText = 'Agent Cancelled';
break;
case 'error':
case SubagentState.ERROR:
headerText = 'Agent Error';
break;
default:
@@ -88,8 +89,8 @@ export const SubagentGroupDisplay: React.FC<SubagentGroupDisplayProps> = ({
for (const tc of toolCalls) {
const progress = tc.resultDisplay;
if (isSubagentProgress(progress)) {
if (progress.state === 'completed') completedCount++;
else if (progress.state === 'running') runningCount++;
if (progress.state === SubagentState.COMPLETED) completedCount++;
else if (progress.state === SubagentState.RUNNING) runningCount++;
} else {
// It hasn't emitted progress yet, but it is "running"
runningCount++;
@@ -200,7 +201,7 @@ export const SubagentGroupDisplay: React.FC<SubagentGroupDisplayProps> = ({
let content = 'Starting...';
let formattedArgs: string | undefined;
if (progress.state === 'completed') {
if (progress.state === SubagentState.COMPLETED) {
if (
progress.terminateReason &&
progress.terminateReason !== 'GOAL'
@@ -223,18 +224,18 @@ export const SubagentGroupDisplay: React.FC<SubagentGroupDisplayProps> = ({
}
const displayArgs =
progress.state === 'completed' ? '' : formattedArgs;
progress.state === SubagentState.COMPLETED ? '' : formattedArgs;
const renderStatusIcon = () => {
const state = progress.state ?? 'running';
const state = progress.state ?? SubagentState.RUNNING;
switch (state) {
case 'running':
case SubagentState.RUNNING:
return <Text color={theme.text.primary}>!</Text>;
case 'completed':
case SubagentState.COMPLETED:
return <Text color={theme.status.success}></Text>;
case 'cancelled':
case SubagentState.CANCELLED:
return <Text color={theme.status.warning}></Text>;
case 'error':
case SubagentState.ERROR:
return <Text color={theme.status.error}></Text>;
default:
return checkExhaustive(state);
@@ -8,6 +8,7 @@ import { describe, it, expect } from 'vitest';
import { renderWithProviders } from '../../../test-utils/render.js';
import { SubagentHistoryMessage } from './SubagentHistoryMessage.js';
import type { HistoryItemSubagent } from '../../types.js';
import { SubagentState } from '@google/gemini-cli-core';
describe('SubagentHistoryMessage', () => {
const mockItem: HistoryItemSubagent = {
@@ -18,19 +19,19 @@ describe('SubagentHistoryMessage', () => {
id: '1',
type: 'thought',
content: 'Thinking about the problem',
status: 'completed',
status: SubagentState.COMPLETED,
},
{
id: '2',
type: 'tool_call',
content: 'Calling search_web',
status: 'running',
status: SubagentState.RUNNING,
},
{
id: '3',
type: 'tool_call',
content: 'Calling read_file fail',
status: 'error',
status: SubagentState.ERROR,
},
],
};
@@ -6,7 +6,7 @@
import { render, cleanup } from '../../../test-utils/render.js';
import { SubagentProgressDisplay } from './SubagentProgressDisplay.js';
import type { SubagentProgress } from '@google/gemini-cli-core';
import { type SubagentProgress, SubagentState } from '@google/gemini-cli-core';
import { describe, it, expect, vi, afterEach } from 'vitest';
describe('<SubagentProgressDisplay />', () => {
@@ -25,7 +25,7 @@ describe('<SubagentProgressDisplay />', () => {
type: 'tool_call',
content: 'run_shell_command',
args: '{"command": "echo hello", "description": "Say hello"}',
status: 'running',
status: SubagentState.RUNNING,
},
],
};
@@ -48,7 +48,7 @@ describe('<SubagentProgressDisplay />', () => {
displayName: 'RunShellCommand',
description: 'Executing echo hello',
args: '{"command": "echo hello"}',
status: 'running',
status: SubagentState.RUNNING,
},
],
};
@@ -69,7 +69,7 @@ describe('<SubagentProgressDisplay />', () => {
type: 'tool_call',
content: 'run_shell_command',
args: '{"command": "echo hello"}',
status: 'running',
status: SubagentState.RUNNING,
},
],
};
@@ -90,7 +90,7 @@ describe('<SubagentProgressDisplay />', () => {
type: 'tool_call',
content: 'write_file',
args: '{"file_path": "/tmp/test.txt", "content": "foo"}',
status: 'completed',
status: SubagentState.COMPLETED,
},
],
};
@@ -113,7 +113,7 @@ describe('<SubagentProgressDisplay />', () => {
type: 'tool_call',
content: 'run_shell_command',
args: JSON.stringify({ description: longDesc }),
status: 'running',
status: SubagentState.RUNNING,
},
],
};
@@ -133,7 +133,7 @@ describe('<SubagentProgressDisplay />', () => {
id: '5',
type: 'thought',
content: 'Thinking about life',
status: 'running',
status: SubagentState.RUNNING,
},
],
};
@@ -149,7 +149,7 @@ describe('<SubagentProgressDisplay />', () => {
isSubagentProgress: true,
agentName: 'TestAgent',
recentActivity: [],
state: 'cancelled',
state: SubagentState.CANCELLED,
};
const { lastFrame } = await render(
@@ -167,7 +167,7 @@ describe('<SubagentProgressDisplay />', () => {
id: '6',
type: 'thought',
content: 'Request cancelled.',
status: 'error',
status: SubagentState.ERROR,
},
],
};
@@ -188,7 +188,7 @@ describe('<SubagentProgressDisplay />', () => {
type: 'tool_call',
content: 'run_shell_command',
args: '{"command": "echo hello"}',
status: 'error',
status: SubagentState.ERROR,
},
],
};
@@ -9,9 +9,10 @@ import { Box, Text } from 'ink';
import { theme } from '../../semantic-colors.js';
import Spinner from 'ink-spinner';
import { MarkdownDisplay } from '../../utils/MarkdownDisplay.js';
import type {
SubagentProgress,
SubagentActivityItem,
import {
type SubagentProgress,
type SubagentActivityItem,
SubagentState,
} from '@google/gemini-cli-core';
import { TOOL_STATUS } from '../../constants.js';
import { STATUS_INDICATOR_WIDTH } from './ToolShared.js';
@@ -62,13 +63,13 @@ export const SubagentProgressDisplay: React.FC<
let headerText: string | undefined;
let headerColor = theme.text.secondary;
if (progress.state === 'cancelled') {
if (progress.state === SubagentState.CANCELLED) {
headerText = `Subagent ${progress.agentName} was cancelled.`;
headerColor = theme.status.warning;
} else if (progress.state === 'error') {
} else if (progress.state === SubagentState.ERROR) {
headerText = `Subagent ${progress.agentName} failed.`;
headerColor = theme.status.error;
} else if (progress.state === 'completed') {
} else if (progress.state === SubagentState.COMPLETED) {
headerText = `Subagent ${progress.agentName} completed.`;
headerColor = theme.status.success;
} else {
@@ -107,13 +108,13 @@ export const SubagentProgressDisplay: React.FC<
);
} else if (item.type === 'tool_call') {
const statusSymbol =
item.status === 'running' ? (
item.status === SubagentState.RUNNING ? (
<Spinner type="dots" />
) : item.status === 'completed' ? (
) : item.status === SubagentState.COMPLETED ? (
<Text color={theme.status.success}>
{TOOL_STATUS.SUCCESS}
</Text>
) : item.status === 'cancelled' ? (
) : item.status === SubagentState.CANCELLED ? (
<Text color={theme.status.warning} bold>
{TOOL_STATUS.CANCELED}
</Text>
@@ -135,7 +136,7 @@ export const SubagentProgressDisplay: React.FC<
<Text
bold
color={theme.text.primary}
strikethrough={item.status === 'cancelled'}
strikethrough={item.status === SubagentState.CANCELLED}
>
{item.displayName || item.content}
</Text>
@@ -144,7 +145,9 @@ export const SubagentProgressDisplay: React.FC<
<Text
color={theme.text.secondary}
wrap="truncate"
strikethrough={item.status === 'cancelled'}
strikethrough={
item.status === SubagentState.CANCELLED
}
>
{displayArgs}
</Text>
@@ -170,7 +173,7 @@ export const SubagentProgressDisplay: React.FC<
)}
<MarkdownDisplay
text={safeJsonToMarkdown(progress.result)}
isPending={progress.state !== 'completed'}
isPending={progress.state !== SubagentState.COMPLETED}
terminalWidth={terminalWidth}
/>
</Box>
@@ -13,6 +13,7 @@ import {
ApprovalMode,
WRITE_FILE_DISPLAY_NAME,
Kind,
SubagentState,
} from '@google/gemini-cli-core';
import os from 'node:os';
import { createMockSettings } from '../../../test-utils/settings.js';
@@ -76,7 +77,7 @@ describe('ToolGroupMessage Regression Tests', () => {
resultDisplay: {
isSubagentProgress: true,
agentName: 'TestAgent',
state: 'running',
state: SubagentState.RUNNING,
recentActivity: [],
},
}),
@@ -112,7 +113,7 @@ describe('ToolGroupMessage Regression Tests', () => {
resultDisplay: {
isSubagentProgress: true,
agentName: 'TestAgent',
state: 'completed',
state: SubagentState.COMPLETED,
recentActivity: [],
},
}),
+13 -11
View File
@@ -170,13 +170,13 @@ async function searchResourceCandidates(
selector: (candidate: ResourceSuggestionCandidate) => candidate.searchKey,
});
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const results = await fzf.find(normalizedPattern, {
limit: MAX_SUGGESTIONS_TO_SHOW * 3,
});
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return results.map(
(result: { item: ResourceSuggestionCandidate }) => result.item.suggestion,
const results: Array<{ item: ResourceSuggestionCandidate }> = await fzf.find(
normalizedPattern,
{
limit: MAX_SUGGESTIONS_TO_SHOW * 3,
},
);
return results.map((result) => result.item.suggestion);
}
async function searchAgentCandidates(
@@ -194,11 +194,13 @@ async function searchAgentCandidates(
selector: (s: Suggestion) => s.label,
});
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const results = await fzf.find(normalizedPattern, {
limit: MAX_SUGGESTIONS_TO_SHOW,
});
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return results.map((r: { item: Suggestion }) => r.item);
const results: Array<{ item: Suggestion }> = await fzf.find(
normalizedPattern,
{
limit: MAX_SUGGESTIONS_TO_SHOW,
},
);
return results.map((r) => r.item);
}
export function useAtCompletion(props: UseAtCompletionProps): void {
@@ -21,6 +21,7 @@ import {
ROOT_SCHEDULER_ID,
CoreToolCallStatus,
type WaitingToolCall,
SubagentState,
} from '@google/gemini-cli-core';
import { createMockMessageBus } from '@google/gemini-cli-core/src/test-utils/mock-message-bus.js';
@@ -630,7 +631,7 @@ describe('useToolScheduler', () => {
id: '1',
type: 'thought',
content: 'Thinking...',
status: 'running',
status: SubagentState.RUNNING,
},
});
});
@@ -648,7 +649,7 @@ describe('useToolScheduler', () => {
id: '2',
type: 'tool_call',
content: 'Calling tool',
status: 'completed',
status: SubagentState.COMPLETED,
},
});
});
@@ -697,7 +698,7 @@ describe('useToolScheduler', () => {
id: '1',
type: 'thought',
content: 'Thinking...',
status: 'running',
status: SubagentState.RUNNING,
},
});
});
@@ -716,7 +717,7 @@ describe('useToolScheduler', () => {
id: '1',
type: 'thought',
content: 'Thinking... Done!',
status: 'completed',
status: SubagentState.COMPLETED,
},
});
});
@@ -726,6 +727,8 @@ describe('useToolScheduler', () => {
expect(result.current[0][0].subagentHistory![0].content).toBe(
'Thinking... Done!',
);
expect(result.current[0][0].subagentHistory![0].status).toBe('completed');
expect(result.current[0][0].subagentHistory![0].status).toBe(
SubagentState.COMPLETED,
);
});
});
+5 -7
View File
@@ -174,7 +174,10 @@ export const TableRenderer: React.FC<TableRendererProps> = ({
}
// --- Pre-wrap and Optimize Widths ---
const actualColumnWidths = new Array(numColumns).fill(0);
const actualColumnWidths: number[] = [];
for (let i = 0; i < numColumns; i++) {
actualColumnWidths.push(0);
}
const wrapAndProcessRow = (row: StyledLine[]) => {
const rowResult: ProcessedLine[][] = [];
@@ -208,11 +211,7 @@ export const TableRenderer: React.FC<TableRendererProps> = ({
const wrappedRows = styledRows.map((row) => wrapAndProcessRow(row));
// Use the TIGHTEST widths that fit the wrapped content + padding
const adjustedWidths = actualColumnWidths.map(
(w) =>
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
w + COLUMN_PADDING,
);
const adjustedWidths = actualColumnWidths.map((w) => w + COLUMN_PADDING);
return { wrappedHeaders, wrappedRows, adjustedWidths };
}, [styledHeaders, styledRows, terminalWidth]);
@@ -263,7 +262,6 @@ export const TableRenderer: React.FC<TableRendererProps> = ({
isHeader = false,
): React.ReactNode => {
const renderedCells = cells.map((cell, index) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const width = adjustedWidths[index] || 0;
return renderCell(cell, width, isHeader);
});
+9 -7
View File
@@ -111,18 +111,20 @@ function resolveEnvVarsInObjectInternal<T>(
// Check for circular reference
if (visited.has(obj)) {
// Return a shallow copy to break the cycle
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
return [...obj] as unknown as T;
const copy: unknown = [...obj];
const isTArray = (val: unknown): val is T => Array.isArray(val);
if (isTArray(copy)) return copy;
throw new Error('Unreachable');
}
visited.add(obj);
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
const result = obj.map((item) =>
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
const mapped: unknown = obj.map((item: unknown) =>
resolveEnvVarsInObjectInternal(item, visited, customEnv),
) as unknown as T;
);
visited.delete(obj);
return result;
const isTArray = (val: unknown): val is T => Array.isArray(val);
if (isTArray(mapped)) return mapped;
throw new Error('Unreachable');
}
if (typeof obj === 'object') {
+1 -2
View File
@@ -83,8 +83,7 @@ export const getLatestGitHubRelease = async (
if (!releaseTag) {
throw new Error(`Response did not include tag_name field`);
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return releaseTag;
return typeof releaseTag === 'string' ? releaseTag : '';
} catch (error) {
debugLogger.debug(
`Failed to determine latest run-gemini-cli release:`,
+1 -3
View File
@@ -29,8 +29,7 @@ export function tryParseJSON(input: string): object | null {
if (!checkInput(input)) return null;
const trimmed = input.trim();
try {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const parsed = JSON.parse(trimmed);
const parsed: unknown = JSON.parse(trimmed);
if (parsed === null || typeof parsed !== 'object') {
return null;
}
@@ -40,7 +39,6 @@ export function tryParseJSON(input: string): object | null {
if (!Array.isArray(parsed) && Object.keys(parsed).length === 0) return null;
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return parsed;
} catch {
return null;