Compare commits

...

5 Commits

Author SHA1 Message Date
mkorwel e9c82e316e style: run prettier to fix formatting in useHistoryManager.ts 2026-04-13 15:07:04 -07:00
mkorwel f7bf22c711 fix: address unsafe assignments in migrate.ts 2026-04-13 15:07:04 -07:00
mkorwel d458dbbfb9 fix: resolve build and linting issues 2026-04-13 15:07:03 -07:00
mkorwel 112a4195bd fix(cli): resolve build failures and consolidate type guards 2026-04-13 15:05:59 -07:00
mkorwel 5605670d6d refactor(cli): remove unsafe type assertions in commands (Phase 5)
Replaces 'as Type' assertions with runtime type guards and safe property access in CLI commands (extensions, skills, mcp, hooks).

Fixes #19733
2026-04-13 15:01:21 -07:00
30 changed files with 280 additions and 238 deletions
@@ -5,7 +5,7 @@
*/
import type { CommandModule } from 'yargs';
import type { ExtensionSettingScope } from '../../config/extensions/extensionSettings.js';
import { ExtensionSettingScope } from '../../config/extensions/extensionSettings.js';
import {
configureAllExtensions,
configureExtension,
@@ -64,6 +64,10 @@ export const configureCommand: CommandModule<object, ConfigureArgs> = {
}
const extensionManager = await getExtensionManager();
const effectiveScope =
scope === 'workspace'
? ExtensionSettingScope.WORKSPACE
: ExtensionSettingScope.USER;
// Case 1: Configure specific setting for an extension
if (name && setting) {
@@ -71,26 +75,16 @@ export const configureCommand: CommandModule<object, ConfigureArgs> = {
extensionManager,
name,
setting,
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
scope as ExtensionSettingScope,
effectiveScope,
);
}
// Case 2: Configure all settings for an extension
else if (name) {
await configureExtension(
extensionManager,
name,
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
scope as ExtensionSettingScope,
);
await configureExtension(extensionManager, name, effectiveScope);
}
// Case 3: Configure all extensions
else {
await configureAllExtensions(
extensionManager,
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
scope as ExtensionSettingScope,
);
await configureAllExtensions(extensionManager, effectiveScope);
}
await exitCli();
@@ -77,11 +77,11 @@ export const disableCommand: CommandModule = {
return true;
}),
handler: async (argv) => {
const name = argv['name'];
const scope = argv['scope'];
await handleDisable({
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
name: argv['name'] as string,
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
scope: argv['scope'] as string,
name: typeof name === 'string' ? name : '',
scope: typeof scope === 'string' ? scope : undefined,
});
await exitCli();
},
@@ -104,11 +104,11 @@ export const enableCommand: CommandModule = {
return true;
}),
handler: async (argv) => {
const name = argv['name'];
const scope = argv['scope'];
await handleEnable({
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
name: argv['name'] as string,
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
scope: argv['scope'] as string,
name: typeof name === 'string' ? name : '',
scope: typeof scope === 'string' ? scope : undefined,
});
await exitCli();
},
+16 -12
View File
@@ -209,19 +209,23 @@ export const installCommand: CommandModule = {
return true;
}),
handler: async (argv) => {
const source = argv['source'];
const ref = argv['ref'];
const autoUpdate = argv['auto-update'];
const allowPreRelease = argv['pre-release'];
const consent = argv['consent'];
await handleInstall({
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
source: argv['source'] as string,
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
ref: argv['ref'] as string | undefined,
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
autoUpdate: argv['auto-update'] as boolean | undefined,
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
allowPreRelease: argv['pre-release'] as boolean | undefined,
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
consent: argv['consent'] as boolean | undefined,
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
skipSettings: argv['skip-settings'] as boolean | undefined,
source: typeof source === 'string' ? source : '',
ref: typeof ref === 'string' ? ref : undefined,
autoUpdate: typeof autoUpdate === 'boolean' ? autoUpdate : undefined,
allowPreRelease:
typeof allowPreRelease === 'boolean' ? allowPreRelease : undefined,
consent: typeof consent === 'boolean' ? consent : undefined,
skipSettings:
typeof argv['skip-settings'] === 'boolean'
? argv['skip-settings']
: undefined,
});
await exitCli();
},
+4 -4
View File
@@ -78,11 +78,11 @@ export const linkCommand: CommandModule = {
})
.check((_) => true),
handler: async (argv) => {
const path = argv['path'];
const consent = argv['consent'];
await handleLink({
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
path: argv['path'] as string,
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
consent: argv['consent'] as boolean | undefined,
path: typeof path === 'string' ? path : '',
consent: typeof consent === 'boolean' ? consent : undefined,
});
await exitCli();
},
+5 -2
View File
@@ -60,9 +60,12 @@ export const listCommand: CommandModule = {
default: 'text',
}),
handler: async (argv) => {
const outputFormat = argv['output-format'];
await handleList({
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
outputFormat: argv['output-format'] as 'text' | 'json',
outputFormat:
outputFormat === 'text' || outputFormat === 'json'
? outputFormat
: undefined,
});
await exitCli();
},
+4 -4
View File
@@ -97,11 +97,11 @@ export const newCommand: CommandModule = {
});
},
handler: async (args) => {
const path = args['path'];
const template = args['template'];
await handleNew({
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
path: args['path'] as string,
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
template: args['template'] as string | undefined,
path: typeof path === 'string' ? path : '',
template: typeof template === 'string' ? template : undefined,
});
await exitCli();
},
@@ -91,11 +91,16 @@ export const uninstallCommand: CommandModule = {
return true;
}),
handler: async (argv) => {
const names = argv['names'];
let namesArray: string[] = [];
if (Array.isArray(names)) {
namesArray = names.filter((n): n is string => typeof n === 'string');
} else if (typeof names === 'string') {
namesArray = [names];
}
await handleUninstall({
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
names: argv['names'] as string[] | undefined,
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
all: argv['all'] as boolean,
names: namesArray,
all: typeof argv['all'] === 'boolean' ? argv['all'] : false,
});
await exitCli();
},
@@ -157,11 +157,11 @@ export const updateCommand: CommandModule = {
return true;
}),
handler: async (argv) => {
const name = argv['name'];
const all = argv['all'];
await handleUpdate({
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
name: argv['name'] as string | undefined,
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
all: argv['all'] as boolean | undefined,
name: typeof name === 'string' ? name : undefined,
all: typeof all === 'boolean' ? all : undefined,
});
await exitCli();
},
@@ -98,9 +98,9 @@ export const validateCommand: CommandModule = {
demandOption: true,
}),
handler: async (args) => {
const path = args['path'];
await handleValidate({
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
path: args['path'] as string,
path: typeof path === 'string' ? path : '',
});
await exitCli();
},
+25 -39
View File
@@ -11,10 +11,7 @@ import { debugLogger, getErrorMessage } from '@google/gemini-cli-core';
import { loadSettings, SettingScope } from '../../config/settings.js';
import { exitCli } from '../utils.js';
import stripJsonComments from 'strip-json-comments';
interface MigrateArgs {
fromClaude: boolean;
}
import { isRecord } from '../../utils/typeGuards.js';
/**
* Mapping from Claude Code event names to Gemini event names
@@ -66,12 +63,11 @@ function transformMatcher(matcher: string | undefined): string | undefined {
* Migrate a Claude Code hook configuration to Gemini format
*/
function migrateClaudeHook(claudeHook: unknown): unknown {
if (!claudeHook || typeof claudeHook !== 'object') {
if (!isRecord(claudeHook)) {
return claudeHook;
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
const hook = claudeHook as Record<string, unknown>;
const hook = claudeHook;
const migrated: Record<string, unknown> = {};
// Map command field
@@ -106,18 +102,15 @@ function migrateClaudeHook(claudeHook: unknown): unknown {
* Migrate Claude Code hooks configuration to Gemini format
*/
function migrateClaudeHooks(claudeConfig: unknown): Record<string, unknown> {
if (!claudeConfig || typeof claudeConfig !== 'object') {
if (!isRecord(claudeConfig)) {
return {};
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
const config = claudeConfig as Record<string, unknown>;
const geminiHooks: Record<string, unknown> = {};
// Check if there's a hooks section
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
const hooksSection = config['hooks'] as Record<string, unknown> | undefined;
if (!hooksSection || typeof hooksSection !== 'object') {
const hooksSection = claudeConfig['hooks'];
if (!isRecord(hooksSection)) {
return {};
}
@@ -131,31 +124,25 @@ function migrateClaudeHooks(claudeConfig: unknown): Record<string, unknown> {
// Migrate each hook definition
const migratedDefinitions = eventConfig.map((def: unknown) => {
if (!def || typeof def !== 'object') {
if (!isRecord(def)) {
return def;
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
const definition = def as Record<string, unknown>;
const migratedDef: Record<string, unknown> = {};
// Transform matcher
if (
'matcher' in definition &&
// eslint-disable-next-line no-restricted-syntax
typeof definition['matcher'] === 'string'
) {
migratedDef['matcher'] = transformMatcher(definition['matcher']);
if ('matcher' in def && typeof def['matcher'] === 'string') {
migratedDef['matcher'] = transformMatcher(def['matcher']);
}
// Copy sequential flag
if ('sequential' in definition) {
migratedDef['sequential'] = definition['sequential'];
if ('sequential' in def) {
migratedDef['sequential'] = def['sequential'];
}
// Migrate hooks array
if ('hooks' in definition && Array.isArray(definition['hooks'])) {
migratedDef['hooks'] = definition['hooks'].map(migrateClaudeHook);
if ('hooks' in def && Array.isArray(def['hooks'])) {
migratedDef['hooks'] = def['hooks'].map(migrateClaudeHook);
}
return migratedDef;
@@ -186,11 +173,11 @@ export async function handleMigrateFromClaude() {
sourceFile = claudeLocalSettingsPath;
try {
const content = fs.readFileSync(claudeLocalSettingsPath, 'utf-8');
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
claudeSettings = JSON.parse(stripJsonComments(content)) as Record<
string,
unknown
>;
const parsed: unknown = JSON.parse(stripJsonComments(content));
if (isRecord(parsed)) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-type-assertion
claudeSettings = parsed as any;
}
} catch (error) {
debugLogger.error(
`Error reading ${claudeLocalSettingsPath}: ${getErrorMessage(error)}`,
@@ -200,11 +187,11 @@ export async function handleMigrateFromClaude() {
sourceFile = claudeSettingsPath;
try {
const content = fs.readFileSync(claudeSettingsPath, 'utf-8');
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
claudeSettings = JSON.parse(stripJsonComments(content)) as Record<
string,
unknown
>;
const parsed: unknown = JSON.parse(stripJsonComments(content));
if (isRecord(parsed)) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-type-assertion
claudeSettings = parsed as any;
}
} catch (error) {
debugLogger.error(
`Error reading ${claudeSettingsPath}: ${getErrorMessage(error)}`,
@@ -268,9 +255,8 @@ export const migrateCommand: CommandModule = {
default: false,
}),
handler: async (argv) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
const args = argv as unknown as MigrateArgs;
if (args.fromClaude) {
const fromClaude = argv['from-claude'];
if (typeof fromClaude === 'boolean' && fromClaude) {
await handleMigrateFromClaude();
} else {
debugLogger.log(
+53 -29
View File
@@ -211,40 +211,64 @@ export const addCommand: CommandModule = {
})
.middleware((argv) => {
// Handle -- separator args as server args if present
if (argv['--']) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
const existingArgs = (argv['args'] as Array<string | number>) || [];
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
argv['args'] = [...existingArgs, ...(argv['--'] as string[])];
if (argv['--'] && Array.isArray(argv['--'])) {
const args = argv['args'];
let existingArgs: Array<string | number> = [];
if (Array.isArray(args)) {
existingArgs = args.filter(
(a): a is string | number =>
typeof a === 'string' || typeof a === 'number',
);
} else if (typeof args === 'string' || typeof args === 'number') {
existingArgs = [args];
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
argv['args'] = [...existingArgs, ...argv['--']];
}
}),
handler: async (argv) => {
const name = argv['name'];
const commandOrUrl = argv['commandOrUrl'];
const args = argv['args'];
const scope = argv['scope'];
const transport = argv['transport'];
const env = argv['env'];
const header = argv['header'];
const timeout = argv['timeout'];
const trust = argv['trust'];
const description = argv['description'];
const includeTools = argv['includeTools'];
const excludeTools = argv['excludeTools'];
const argsVal = Array.isArray(args)
? args.filter(
(a): a is string | number =>
typeof a === 'string' || typeof a === 'number',
)
: undefined;
await addMcpServer(
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
argv['name'] as string,
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
argv['commandOrUrl'] as string,
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
argv['args'] as Array<string | number>,
typeof name === 'string' ? name : '',
typeof commandOrUrl === 'string' ? commandOrUrl : '',
argsVal,
{
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
scope: argv['scope'] as string,
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
transport: argv['transport'] as string,
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
env: argv['env'] as string[],
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
header: argv['header'] as string[],
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
timeout: argv['timeout'] as number | undefined,
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
trust: argv['trust'] as boolean | undefined,
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
description: argv['description'] as string | undefined,
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
includeTools: argv['includeTools'] as string[] | undefined,
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
excludeTools: argv['excludeTools'] as string[] | undefined,
scope: typeof scope === 'string' ? scope : 'project',
transport: typeof transport === 'string' ? transport : 'stdio',
env: Array.isArray(env)
? env.filter((e): e is string => typeof e === 'string')
: undefined,
header: Array.isArray(header)
? header.filter((h): h is string => typeof h === 'string')
: undefined,
timeout: typeof timeout === 'number' ? timeout : undefined,
trust: typeof trust === 'boolean' ? trust : undefined,
description: typeof description === 'string' ? description : undefined,
includeTools: Array.isArray(includeTools)
? includeTools.filter((t): t is string => typeof t === 'string')
: undefined,
excludeTools: Array.isArray(excludeTools)
? excludeTools.filter((t): t is string => typeof t === 'string')
: undefined,
},
);
await exitCli();
+12 -2
View File
@@ -112,7 +112,12 @@ export const enableCommand: CommandModule<object, Args> = {
default: false,
}),
handler: async (argv) => {
await handleEnable(argv as Args);
const name = argv['name'];
const session = argv['session'];
await handleEnable({
name: typeof name === 'string' ? name : '',
session: typeof session === 'boolean' ? session : undefined,
});
await exitCli();
},
};
@@ -133,7 +138,12 @@ export const disableCommand: CommandModule<object, Args> = {
default: false,
}),
handler: async (argv) => {
await handleDisable(argv as Args);
const name = argv['name'];
const session = argv['session'];
await handleDisable({
name: typeof name === 'string' ? name : '',
session: typeof session === 'boolean' ? session : undefined,
});
await exitCli();
},
};
+4 -4
View File
@@ -55,10 +55,10 @@ export const removeCommand: CommandModule = {
choices: ['user', 'project'],
}),
handler: async (argv) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
await removeMcpServer(argv['name'] as string, {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
scope: argv['scope'] as string,
const name = argv['name'];
const scope = argv['scope'];
await removeMcpServer(typeof name === 'string' ? name : '', {
scope: typeof scope === 'string' ? scope : 'project',
});
await exitCli();
},
+2 -2
View File
@@ -48,13 +48,13 @@ export const disableCommand: CommandModule = {
choices: ['user', 'workspace'],
}),
handler: async (argv) => {
const name = argv['name'];
const scope =
argv['scope'] === 'workspace'
? SettingScope.Workspace
: SettingScope.User;
await handleDisable({
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
name: argv['name'] as string,
name: typeof name === 'string' ? name : '',
scope,
});
await exitCli();
+2 -2
View File
@@ -39,9 +39,9 @@ export const enableCommand: CommandModule = {
demandOption: true,
}),
handler: async (argv) => {
const name = argv['name'];
await handleEnable({
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
name: argv['name'] as string,
name: typeof name === 'string' ? name : '',
});
await exitCli();
},
+9 -8
View File
@@ -104,15 +104,16 @@ export const installCommand: CommandModule = {
return true;
}),
handler: async (argv) => {
const source = argv['source'];
const scope = argv['scope'];
const path = argv['path'];
const consent = argv['consent'];
await handleInstall({
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
source: argv['source'] as string,
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
scope: argv['scope'] as 'user' | 'workspace',
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
path: argv['path'] as string | undefined,
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
consent: argv['consent'] as boolean | undefined,
source: typeof source === 'string' ? source : '',
scope: scope === 'user' || scope === 'workspace' ? scope : undefined,
path: typeof path === 'string' ? path : undefined,
consent: typeof consent === 'boolean' ? consent : undefined,
});
await exitCli();
},
+7 -6
View File
@@ -82,13 +82,14 @@ export const linkCommand: CommandModule = {
return true;
}),
handler: async (argv) => {
const path = argv['path'];
const scope = argv['scope'];
const consent = argv['consent'];
await handleLink({
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
path: argv['path'] as string,
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
scope: argv['scope'] as 'user' | 'workspace',
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
consent: argv['consent'] as boolean | undefined,
path: typeof path === 'string' ? path : '',
scope: scope === 'user' || scope === 'workspace' ? scope : undefined,
consent: typeof consent === 'boolean' ? consent : undefined,
});
await exitCli();
},
+3 -3
View File
@@ -20,7 +20,7 @@ export async function handleList(args: { all?: boolean }) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
{
debug: false,
} as Partial<CliArgs> as CliArgs,
} as unknown as CliArgs,
{ cwd: workspaceDir },
);
@@ -72,8 +72,8 @@ export const listCommand: CommandModule = {
default: false,
}),
handler: async (argv) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
await handleList({ all: argv['all'] as boolean });
const all = argv['all'];
await handleList({ all: typeof all === 'boolean' ? all : false });
await exitCli();
},
};
@@ -62,11 +62,12 @@ export const uninstallCommand: CommandModule = {
return true;
}),
handler: async (argv) => {
const name = argv['name'];
const scope = argv['scope'];
await handleUninstall({
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
name: argv['name'] as string,
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
scope: argv['scope'] as 'user' | 'workspace',
name: typeof name === 'string' ? name : '',
scope: scope === 'user' || scope === 'workspace' ? scope : undefined,
});
await exitCli();
},
@@ -14,7 +14,7 @@ import {
type SlashCommand,
type CommandContext,
} from './types.js';
import { MessageType, type HistoryItem } from '../types.js';
import { MessageType, type HistoryItemWithoutId } from '../types.js';
import {
refreshServerHierarchicalMemory,
type Config,
@@ -29,10 +29,7 @@ import * as fs from 'node:fs';
async function finishAddingDirectories(
config: Config,
addItem: (
itemData: Omit<HistoryItem, 'id'>,
baseTimestamp?: number,
) => number,
addItem: (itemData: HistoryItemWithoutId, baseTimestamp?: number) => number,
added: string[],
errors: string[],
) {
@@ -209,8 +209,8 @@ async function restartAction(
const s = extensionsToRestart.length > 1 ? 's' : '';
const reloadingMessage = {
type: MessageType.INFO,
const reloadingMessage: HistoryItemInfo = {
type: 'info',
text: `Reloading ${extensionsToRestart.length} extension${s}...`,
color: theme.text.primary,
};
@@ -472,8 +472,7 @@ async function installAction(
args: string,
requestConsentOverride?: (consent: string) => Promise<boolean>,
) {
const extensionLoader =
context.services.agentContext?.config.getExtensionLoader();
const extensionLoader = context.services.config?.getExtensionLoader();
if (!(extensionLoader instanceof ExtensionManager)) {
debugLogger.error(
`Cannot ${context.invocation?.name} extensions in this environment`,
@@ -16,7 +16,7 @@ import { useKeypress } from '../hooks/useKeypress.js';
import { loadTrustedFolders, TrustLevel } from '../../config/trustedFolders.js';
import { expandHomeDir } from '../utils/directoryUtils.js';
import * as path from 'node:path';
import { MessageType, type HistoryItem } from '../types.js';
import { MessageType, type HistoryItemWithoutId } from '../types.js';
import { type Config } from '@google/gemini-cli-core';
export enum MultiFolderTrustChoice {
@@ -32,18 +32,12 @@ export interface MultiFolderTrustDialogProps {
errors: string[];
finishAddingDirectories: (
config: Config,
addItem: (
itemData: Omit<HistoryItem, 'id'>,
baseTimestamp?: number,
) => number,
addItem: (itemData: HistoryItemWithoutId, baseTimestamp?: number) => number,
added: string[],
errors: string[],
) => Promise<void>;
config: Config;
addItem: (
itemData: Omit<HistoryItem, 'id'>,
baseTimestamp?: number,
) => number;
addItem: (itemData: HistoryItemWithoutId, baseTimestamp?: number) => number;
}
export const MultiFolderTrustDialog: React.FC<MultiFolderTrustDialogProps> = ({
@@ -63,6 +63,7 @@ import {
LogoutChoice,
} from '../components/LogoutConfirmationDialog.js';
import { runExitCleanup } from '../../utils/cleanup.js';
import { isRecord } from '../../utils/typeGuards.js';
interface SlashCommandProcessorActions {
openAuthDialog: () => void;
@@ -501,35 +502,41 @@ export const useSlashCommandProcessor = (
actions.openModelDialog();
return { type: 'handled' };
case 'agentConfig': {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
const props = result.props as Record<string, unknown>;
const props = result.props;
const name = isRecord(props) ? props['name'] : undefined;
const displayName = isRecord(props)
? props['displayName']
: undefined;
const definition = isRecord(props)
? props['definition']
: undefined;
if (
!props ||
// eslint-disable-next-line no-restricted-syntax
typeof props['name'] !== 'string' ||
// eslint-disable-next-line no-restricted-syntax
typeof props['displayName'] !== 'string' ||
!props['definition']
typeof name !== 'string' ||
typeof displayName !== 'string' ||
!definition
) {
throw new Error(
'Received invalid properties for agentConfig dialog action.',
);
}
actions.openAgentConfigDialog(
props['name'],
props['displayName'],
name,
displayName,
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
props['definition'] as AgentDefinition,
definition as AgentDefinition,
);
return { type: 'handled' };
}
case 'permissions':
case 'permissions': {
const props = result.props;
actions.openPermissionsDialog(
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
result.props as { targetDirectory?: string },
isRecord(props)
? (props as { targetDirectory?: string })
: undefined,
);
return { type: 'handled' };
}
case 'help':
return { type: 'handled' };
default: {
+29 -20
View File
@@ -92,7 +92,6 @@ import {
type TrackedCompletedToolCall,
type TrackedCancelledToolCall,
type TrackedWaitingToolCall,
type TrackedExecutingToolCall,
} from './useToolScheduler.js';
import { theme } from '../semantic-colors.js';
import { getToolGroupBorderAppearance } from '../utils/borderStyles.js';
@@ -102,6 +101,10 @@ import { useSessionStats } from '../contexts/SessionContext.js';
import { useKeypress } from './useKeypress.js';
import type { LoadedSettings } from '../../config/settings.js';
function isRecord(obj: unknown): obj is Record<string, unknown> {
return typeof obj === 'object' && obj !== null && !Array.isArray(obj);
}
type ToolResponseWithParts = ToolCallResponseInfo & {
llmContent?: PartListUnion;
};
@@ -842,9 +845,11 @@ export const useGeminiStream = (
// If it is a shell command, we update the status to Canceled and clear the output
// to avoid artifacts, then add it to history immediately.
if (isShellCommand) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
const toolGroup = pendingHistoryItemRef.current as HistoryItemToolGroup;
if (
isShellCommand &&
pendingHistoryItemRef.current.type === 'tool_group'
) {
const toolGroup = pendingHistoryItemRef.current;
const updatedTools = toolGroup.tools.map((tool) => {
if (tool.name === SHELL_COMMAND_NAME) {
return {
@@ -855,7 +860,7 @@ export const useGeminiStream = (
}
return tool;
});
addItem({ ...toolGroup, tools: updatedTools } as HistoryItemWithoutId);
addItem({ ...toolGroup, tools: updatedTools });
} else {
addItem(pendingHistoryItemRef.current);
}
@@ -1069,11 +1074,15 @@ export const useGeminiStream = (
const splitPoint = findLastSafeSplitPoint(newGeminiMessageBuffer);
if (splitPoint === newGeminiMessageBuffer.length) {
// Update the existing message with accumulated content
setPendingHistoryItem((item) => ({
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
type: item?.type as 'gemini' | 'gemini_content',
text: newGeminiMessageBuffer,
}));
setPendingHistoryItem((item) => {
if (item?.type === 'gemini' || item?.type === 'gemini_content') {
return {
type: item.type,
text: newGeminiMessageBuffer,
};
}
return item;
});
} else {
// This indicates that we need to split up this Gemini Message.
// Splitting a message is primarily a performance consideration. There is a
@@ -1086,16 +1095,16 @@ export const useGeminiStream = (
const beforeText = newGeminiMessageBuffer.substring(0, splitPoint);
const afterText = newGeminiMessageBuffer.substring(splitPoint);
if (beforeText.length > 0) {
addItem(
{
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
type: pendingHistoryItemRef.current?.type as
| 'gemini'
| 'gemini_content',
text: beforeText,
},
userMessageTimestamp,
);
const type = pendingHistoryItemRef.current?.type;
if (type === 'gemini' || type === 'gemini_content') {
addItem(
{
type,
text: beforeText,
},
userMessageTimestamp,
);
}
}
setPendingHistoryItem({ type: 'gemini_content', text: afterText });
newGeminiMessageBuffer = afterText;
@@ -8,7 +8,7 @@ import { describe, it, expect } from 'vitest';
import { act } from 'react';
import { renderHook } from '../../test-utils/render.js';
import { useHistory } from './useHistoryManager.js';
import type { HistoryItem } from '../types.js';
import type { HistoryItem, HistoryItemWithoutId } from '../types.js';
describe('useHistoryManager', () => {
it('should initialize with an empty history', async () => {
@@ -19,7 +19,7 @@ describe('useHistoryManager', () => {
it('should add an item to history with a unique ID', async () => {
const { result } = await renderHook(() => useHistory());
const timestamp = Date.now();
const itemData: Omit<HistoryItem, 'id'> = {
const itemData: HistoryItemWithoutId = {
type: 'user', // Replaced HistoryItemType.User
text: 'Hello',
};
@@ -92,11 +92,11 @@ describe('useHistoryManager', () => {
it('should generate unique IDs for items added with the same base timestamp', async () => {
const { result } = await renderHook(() => useHistory());
const timestamp = Date.now();
const itemData1: Omit<HistoryItem, 'id'> = {
const itemData1: HistoryItemWithoutId = {
type: 'user', // Replaced HistoryItemType.User
text: 'First',
};
const itemData2: Omit<HistoryItem, 'id'> = {
const itemData2: HistoryItemWithoutId = {
type: 'gemini', // Replaced HistoryItemType.Gemini
text: 'Second',
};
@@ -120,7 +120,7 @@ describe('useHistoryManager', () => {
it('should update an existing history item', async () => {
const { result } = await renderHook(() => useHistory());
const timestamp = Date.now();
const initialItem: Omit<HistoryItem, 'id'> = {
const initialItem: HistoryItemWithoutId = {
type: 'gemini', // Replaced HistoryItemType.Gemini
text: 'Initial content',
};
@@ -146,7 +146,7 @@ describe('useHistoryManager', () => {
it('should not change history if updateHistoryItem is called with a nonexistent ID', async () => {
const { result } = await renderHook(() => useHistory());
const timestamp = Date.now();
const itemData: Omit<HistoryItem, 'id'> = {
const itemData: HistoryItemWithoutId = {
type: 'user', // Replaced HistoryItemType.User
text: 'Hello',
};
@@ -167,11 +167,11 @@ describe('useHistoryManager', () => {
it('should clear the history', async () => {
const { result } = await renderHook(() => useHistory());
const timestamp = Date.now();
const itemData1: Omit<HistoryItem, 'id'> = {
const itemData1: HistoryItemWithoutId = {
type: 'user', // Replaced HistoryItemType.User
text: 'First',
};
const itemData2: Omit<HistoryItem, 'id'> = {
const itemData2: HistoryItemWithoutId = {
type: 'gemini', // Replaced HistoryItemType.Gemini
text: 'Second',
};
@@ -193,19 +193,19 @@ describe('useHistoryManager', () => {
it('should not add consecutive duplicate user messages', async () => {
const { result } = await renderHook(() => useHistory());
const timestamp = Date.now();
const itemData1: Omit<HistoryItem, 'id'> = {
const itemData1: HistoryItemWithoutId = {
type: 'user', // Replaced HistoryItemType.User
text: 'Duplicate message',
};
const itemData2: Omit<HistoryItem, 'id'> = {
const itemData2: HistoryItemWithoutId = {
type: 'user', // Replaced HistoryItemType.User
text: 'Duplicate message',
};
const itemData3: Omit<HistoryItem, 'id'> = {
const itemData3: HistoryItemWithoutId = {
type: 'gemini', // Replaced HistoryItemType.Gemini
text: 'Gemini response',
};
const itemData4: Omit<HistoryItem, 'id'> = {
const itemData4: HistoryItemWithoutId = {
type: 'user', // Replaced HistoryItemType.User
text: 'Another user message',
};
@@ -226,15 +226,15 @@ describe('useHistoryManager', () => {
it('should add duplicate user messages if they are not consecutive', async () => {
const { result } = await renderHook(() => useHistory());
const timestamp = Date.now();
const itemData1: Omit<HistoryItem, 'id'> = {
const itemData1: HistoryItemWithoutId = {
type: 'user', // Replaced HistoryItemType.User
text: 'Message 1',
};
const itemData2: Omit<HistoryItem, 'id'> = {
const itemData2: HistoryItemWithoutId = {
type: 'gemini', // Replaced HistoryItemType.Gemini
text: 'Gemini response',
};
const itemData3: Omit<HistoryItem, 'id'> = {
const itemData3: HistoryItemWithoutId = {
type: 'user', // Replaced HistoryItemType.User
text: 'Message 1', // Duplicate text, but not consecutive
};
@@ -254,7 +254,7 @@ describe('useHistoryManager', () => {
it('should use Date.now() as default baseTimestamp if not provided', async () => {
const { result } = await renderHook(() => useHistory());
const before = Date.now();
const itemData: Omit<HistoryItem, 'id'> = {
const itemData: HistoryItemWithoutId = {
type: 'user',
text: 'Default timestamp test',
};
@@ -5,24 +5,24 @@
*/
import { useState, useRef, useCallback, useMemo } from 'react';
import type { HistoryItem } from '../types.js';
import type { HistoryItem, HistoryItemWithoutId } from '../types.js';
import type { ChatRecordingService } from '@google/gemini-cli-core/src/services/chatRecordingService.js';
// Type for the updater function passed to updateHistoryItem
type HistoryItemUpdater = (
prevItem: HistoryItem,
) => Partial<Omit<HistoryItem, 'id'>>;
) => Partial<HistoryItemWithoutId>;
export interface UseHistoryManagerReturn {
history: HistoryItem[];
addItem: (
itemData: Omit<HistoryItem, 'id'>,
itemData: HistoryItemWithoutId,
baseTimestamp?: number,
isResuming?: boolean,
) => number; // Returns the generated ID
updateItem: (
id: number,
updates: Partial<Omit<HistoryItem, 'id'>> | HistoryItemUpdater,
updates: Partial<HistoryItemWithoutId> | HistoryItemUpdater,
) => void;
clearItems: () => void;
loadHistory: (newHistory: HistoryItem[]) => void;
@@ -63,13 +63,12 @@ export function useHistory({
// Adds a new item to the history state with a unique ID.
const addItem = useCallback(
(
itemData: Omit<HistoryItem, 'id'>,
itemData: HistoryItemWithoutId,
baseTimestamp: number = Date.now(),
isResuming: boolean = false,
): number => {
const id = getNextMessageId(baseTimestamp);
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
const newItem: HistoryItem = { ...itemData, id } as HistoryItem;
const newItem: HistoryItem = { ...itemData, id };
setHistory((prevHistory) => {
if (prevHistory.length > 0) {
@@ -138,7 +137,7 @@ export function useHistory({
const updateItem = useCallback(
(
id: number,
updates: Partial<Omit<HistoryItem, 'id'>> | HistoryItemUpdater,
updates: Partial<HistoryItemWithoutId> | HistoryItemUpdater,
) => {
setHistory((prevHistory) =>
prevHistory.map((item) => {
@@ -14,14 +14,11 @@ import {
} from '@google/gemini-cli-core';
import { MultiFolderTrustDialog } from '../components/MultiFolderTrustDialog.js';
import type { UseHistoryManagerReturn } from './useHistoryManager.js';
import { MessageType, type HistoryItem } from '../types.js';
import { MessageType, type HistoryItemWithoutId } from '../types.js';
async function finishAddingDirectories(
config: Config,
addItem: (
itemData: Omit<HistoryItem, 'id'>,
baseTimestamp?: number,
) => number,
addItem: (itemData: HistoryItemWithoutId, baseTimestamp?: number) => number,
added: string[],
errors: string[],
) {
+2 -3
View File
@@ -8,7 +8,7 @@ import type { UpdateObject } from '../ui/utils/updateCheck.js';
import type { LoadedSettings } from '../config/settings.js';
import { getInstallationInfo, PackageManager } from './installationInfo.js';
import { updateEventEmitter } from './updateEventEmitter.js';
import { MessageType, type HistoryItem } from '../ui/types.js';
import { MessageType, type HistoryItemWithoutId } from '../ui/types.js';
import { spawnWrapper } from './spawnWrapper.js';
import type { spawn } from 'node:child_process';
import { debugLogger } from '@google/gemini-cli-core';
@@ -115,7 +115,6 @@ export function handleAutoUpdate(
}
updateEventEmitter.emit('update-received', {
...info,
message: combinedMessage,
isUpdating: true,
});
if (_updateInProgress) {
@@ -163,7 +162,7 @@ export function handleAutoUpdate(
}
export function setUpdateHandler(
addItem: (item: Omit<HistoryItem, 'id'>, timestamp: number) => void,
addItem: (item: HistoryItemWithoutId, timestamp: number) => void,
setUpdateInfo: (info: UpdateObject | null) => void,
) {
let successfullyInstalled = false;
+12
View File
@@ -0,0 +1,12 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
/**
* Type guard to check if a value is a non-null object and not an array.
*/
export function isRecord(obj: unknown): obj is Record<string, unknown> {
return typeof obj === 'object' && obj !== null && !Array.isArray(obj);
}