Extension update confirm dialog (#10183)

This commit is contained in:
Jacob MacDonald
2025-09-29 14:19:19 -07:00
committed by GitHub
parent d6933c77ba
commit cea1a867b6
14 changed files with 310 additions and 50 deletions

View File

@@ -898,8 +898,8 @@ describe('extension tests', () => {
).resolves.toBe('my-local-extension');
expect(consoleInfoSpy).toHaveBeenCalledWith(
`Extensions may introduce unexpected behavior.
Ensure you have investigated the extension source and trust the author.
`Installing extension "my-local-extension".
**Extensions may introduce unexpected behavior. Ensure you have investigated the extension source and trust the author.**
This extension will run the following MCP servers:
* test-server (local): node server.js
* test-server-2 (remote): https://google.com`,
@@ -954,7 +954,7 @@ This extension will run the following MCP servers:
{ source: sourceExtDir, type: 'local' },
requestConsentNonInteractive,
),
).rejects.toThrow('Installation cancelled.');
).rejects.toThrow('Installation cancelled for "my-local-extension".');
expect(mockQuestion).toHaveBeenCalledWith(
expect.stringContaining('Do you want to continue? [Y/n]: '),

View File

@@ -37,8 +37,8 @@ import {
} from './extensions/github.js';
import type { LoadExtensionContext } from './extensions/variableSchema.js';
import { ExtensionEnablementManager } from './extensions/extensionEnablement.js';
import type { UseHistoryManagerReturn } from '../ui/hooks/useHistoryManager.js';
import chalk from 'chalk';
import type { ConfirmationRequest } from '../ui/types.js';
export const EXTENSIONS_DIRECTORY_NAME = path.join(GEMINI_DIR, 'extensions');
@@ -347,7 +347,7 @@ export async function requestConsentNonInteractive(
consentDescription: string,
): Promise<boolean> {
console.info(consentDescription);
const result = await promptForContinuationNonInteractive(
const result = await promptForConsentNonInteractive(
'Do you want to continue? [Y/n]: ',
);
return result;
@@ -359,20 +359,17 @@ export async function requestConsentNonInteractive(
* This should not be called from non-interactive mode as it will not work.
*
* @param consentDescription The description of the thing they will be consenting to.
* @param setExtensionUpdateConfirmationRequest A function to actually add a prompt to the UI.
* @returns boolean, whether they consented or not.
*/
export async function requestConsentInteractive(
_consentDescription: string,
addHistoryItem: UseHistoryManagerReturn['addItem'],
consentDescription: string,
addExtensionUpdateConfirmationRequest: (value: ConfirmationRequest) => void,
): Promise<boolean> {
addHistoryItem(
{
type: 'info',
text: 'Tried to update an extension but it has some changes that require consent, please use `gemini extensions update`.',
},
Date.now(),
return await promptForConsentInteractive(
consentDescription + '\n\nDo you want to continue?',
addExtensionUpdateConfirmationRequest,
);
return false;
}
/**
@@ -383,7 +380,7 @@ export async function requestConsentInteractive(
* @param prompt A yes/no prompt to ask the user
* @returns Whether or not the user answers 'y' (yes). Defaults to 'yes' on enter.
*/
async function promptForContinuationNonInteractive(
async function promptForConsentNonInteractive(
prompt: string,
): Promise<boolean> {
const readline = await import('node:readline');
@@ -400,6 +397,29 @@ async function promptForContinuationNonInteractive(
});
}
/**
* Asks users an interactive yes/no prompt.
*
* This should not be called from non-interactive mode as it will break the CLI.
*
* @param prompt A markdown prompt to ask the user
* @param setExtensionUpdateConfirmationRequest Function to update the UI state with the confirmation request.
* @returns Whether or not the user answers yes.
*/
async function promptForConsentInteractive(
prompt: string,
addExtensionUpdateConfirmationRequest: (value: ConfirmationRequest) => void,
): Promise<boolean> {
return await new Promise<boolean>((resolve) => {
addExtensionUpdateConfirmationRequest({
prompt,
onConfirm: (resolvedConfirmed) => {
resolve(resolvedConfirmed);
},
});
});
}
export async function installExtension(
installMetadata: ExtensionInstallMetadata,
requestConsent: (consent: string) => Promise<boolean>,
@@ -548,9 +568,9 @@ export async function installExtension(
function extensionConsentString(extensionConfig: ExtensionConfig): string {
const output: string[] = [];
const mcpServerEntries = Object.entries(extensionConfig.mcpServers || {});
output.push('Extensions may introduce unexpected behavior.');
output.push(`Installing extension "${extensionConfig.name}".`);
output.push(
'Ensure you have investigated the extension source and trust the author.',
'**Extensions may introduce unexpected behavior. Ensure you have investigated the extension source and trust the author.**',
);
if (mcpServerEntries.length) {
@@ -600,7 +620,7 @@ async function maybeRequestConsentOrFail(
}
}
if (!(await requestConsent(extensionConsent))) {
throw new Error('Installation cancelled.');
throw new Error(`Installation cancelled for "${extensionConfig.name}".`);
}
}