mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-11 14:40:52 -07:00
Extension update confirm dialog (#10183)
This commit is contained in:
@@ -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]: '),
|
||||
|
||||
@@ -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}".`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -153,12 +153,16 @@ export const AppContainer = (props: AppContainerProps) => {
|
||||
);
|
||||
|
||||
const extensions = config.getExtensions();
|
||||
const { extensionsUpdateState, setExtensionsUpdateState } =
|
||||
useExtensionUpdates(
|
||||
extensions,
|
||||
historyManager.addItem,
|
||||
config.getWorkingDir(),
|
||||
);
|
||||
const {
|
||||
extensionsUpdateState,
|
||||
setExtensionsUpdateState,
|
||||
confirmUpdateExtensionRequests,
|
||||
addConfirmUpdateExtensionRequest,
|
||||
} = useExtensionUpdates(
|
||||
extensions,
|
||||
historyManager.addItem,
|
||||
config.getWorkingDir(),
|
||||
);
|
||||
|
||||
const [isPermissionsDialogOpen, setPermissionsDialogOpen] = useState(false);
|
||||
const openPermissionsDialog = useCallback(
|
||||
@@ -456,6 +460,7 @@ Logging in with Google... Please restart Gemini CLI to continue.
|
||||
setDebugMessage,
|
||||
toggleCorgiMode: () => setCorgiMode((prev) => !prev),
|
||||
setExtensionsUpdateState,
|
||||
addConfirmUpdateExtensionRequest,
|
||||
}),
|
||||
[
|
||||
setAuthState,
|
||||
@@ -469,6 +474,7 @@ Logging in with Google... Please restart Gemini CLI to continue.
|
||||
setCorgiMode,
|
||||
setExtensionsUpdateState,
|
||||
openPermissionsDialog,
|
||||
addConfirmUpdateExtensionRequest,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -1037,6 +1043,7 @@ Logging in with Google... Please restart Gemini CLI to continue.
|
||||
isFolderTrustDialogOpen ||
|
||||
!!shellConfirmationRequest ||
|
||||
!!confirmationRequest ||
|
||||
confirmUpdateExtensionRequests.length > 0 ||
|
||||
!!loopDetectionConfirmationRequest ||
|
||||
isThemeDialogOpen ||
|
||||
isSettingsDialogOpen ||
|
||||
@@ -1078,6 +1085,7 @@ Logging in with Google... Please restart Gemini CLI to continue.
|
||||
commandContext,
|
||||
shellConfirmationRequest,
|
||||
confirmationRequest,
|
||||
confirmUpdateExtensionRequests,
|
||||
loopDetectionConfirmationRequest,
|
||||
geminiMdFileCount,
|
||||
streamingState,
|
||||
@@ -1156,6 +1164,7 @@ Logging in with Google... Please restart Gemini CLI to continue.
|
||||
commandContext,
|
||||
shellConfirmationRequest,
|
||||
confirmationRequest,
|
||||
confirmUpdateExtensionRequests,
|
||||
loopDetectionConfirmationRequest,
|
||||
geminiMdFileCount,
|
||||
streamingState,
|
||||
|
||||
@@ -54,7 +54,10 @@ async function updateAction(context: CommandContext, args: string) {
|
||||
context.services.config!.getWorkingDir(),
|
||||
// We don't have the ability to prompt for consent yet in this flow.
|
||||
(description) =>
|
||||
requestConsentInteractive(description, context.ui.addItem),
|
||||
requestConsentInteractive(
|
||||
description,
|
||||
context.ui.addConfirmUpdateExtensionRequest,
|
||||
),
|
||||
context.services.config!.getExtensions(),
|
||||
context.ui.extensionsUpdateState,
|
||||
context.ui.setExtensionsUpdateState,
|
||||
@@ -80,7 +83,10 @@ async function updateAction(context: CommandContext, args: string) {
|
||||
extension,
|
||||
workingDir,
|
||||
(description) =>
|
||||
requestConsentInteractive(description, context.ui.addItem),
|
||||
requestConsentInteractive(
|
||||
description,
|
||||
context.ui.addConfirmUpdateExtensionRequest,
|
||||
),
|
||||
context.ui.extensionsUpdateState.get(extension.name) ??
|
||||
ExtensionUpdateState.UNKNOWN,
|
||||
(updateState) => {
|
||||
|
||||
@@ -6,7 +6,11 @@
|
||||
|
||||
import type { Dispatch, ReactNode, SetStateAction } from 'react';
|
||||
import type { Content, PartListUnion } from '@google/genai';
|
||||
import type { HistoryItemWithoutId, HistoryItem } from '../types.js';
|
||||
import type {
|
||||
HistoryItemWithoutId,
|
||||
HistoryItem,
|
||||
ConfirmationRequest,
|
||||
} from '../types.js';
|
||||
import type { Config, GitService, Logger } from '@google/gemini-cli-core';
|
||||
import type { LoadedSettings } from '../../config/settings.js';
|
||||
import type { UseHistoryManagerReturn } from '../hooks/useHistoryManager.js';
|
||||
@@ -66,6 +70,7 @@ export interface CommandContext {
|
||||
setExtensionsUpdateState: Dispatch<
|
||||
SetStateAction<Map<string, ExtensionUpdateState>>
|
||||
>;
|
||||
addConfirmUpdateExtensionRequest: (value: ConfirmationRequest) => void;
|
||||
};
|
||||
// Session-specific data
|
||||
session: {
|
||||
|
||||
119
packages/cli/src/ui/components/ConsentPrompt.test.tsx
Normal file
119
packages/cli/src/ui/components/ConsentPrompt.test.tsx
Normal file
@@ -0,0 +1,119 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { Text } from 'ink';
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { render } from 'ink-testing-library';
|
||||
import { ConsentPrompt } from './ConsentPrompt.js';
|
||||
import { RadioButtonSelect } from './shared/RadioButtonSelect.js';
|
||||
import { MarkdownDisplay } from '../utils/MarkdownDisplay.js';
|
||||
|
||||
vi.mock('./shared/RadioButtonSelect.js', () => ({
|
||||
RadioButtonSelect: vi.fn(() => null),
|
||||
}));
|
||||
|
||||
vi.mock('../utils/MarkdownDisplay.js', () => ({
|
||||
MarkdownDisplay: vi.fn(() => null),
|
||||
}));
|
||||
|
||||
const MockedRadioButtonSelect = vi.mocked(RadioButtonSelect);
|
||||
const MockedMarkdownDisplay = vi.mocked(MarkdownDisplay);
|
||||
|
||||
describe('ConsentPrompt', () => {
|
||||
const onConfirm = vi.fn();
|
||||
const terminalWidth = 80;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('renders a string prompt with MarkdownDisplay', () => {
|
||||
const prompt = 'Are you sure?';
|
||||
render(
|
||||
<ConsentPrompt
|
||||
prompt={prompt}
|
||||
onConfirm={onConfirm}
|
||||
terminalWidth={terminalWidth}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(MockedMarkdownDisplay).toHaveBeenCalledWith(
|
||||
{
|
||||
isPending: true,
|
||||
text: prompt,
|
||||
terminalWidth,
|
||||
},
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
it('renders a ReactNode prompt directly', () => {
|
||||
const prompt = <Text>Are you sure?</Text>;
|
||||
const { lastFrame } = render(
|
||||
<ConsentPrompt
|
||||
prompt={prompt}
|
||||
onConfirm={onConfirm}
|
||||
terminalWidth={terminalWidth}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(MockedMarkdownDisplay).not.toHaveBeenCalled();
|
||||
expect(lastFrame()).toContain('Are you sure?');
|
||||
});
|
||||
|
||||
it('calls onConfirm with true when "Yes" is selected', () => {
|
||||
const prompt = 'Are you sure?';
|
||||
render(
|
||||
<ConsentPrompt
|
||||
prompt={prompt}
|
||||
onConfirm={onConfirm}
|
||||
terminalWidth={terminalWidth}
|
||||
/>,
|
||||
);
|
||||
|
||||
const onSelect = MockedRadioButtonSelect.mock.calls[0][0].onSelect;
|
||||
onSelect(true);
|
||||
|
||||
expect(onConfirm).toHaveBeenCalledWith(true);
|
||||
});
|
||||
|
||||
it('calls onConfirm with false when "No" is selected', () => {
|
||||
const prompt = 'Are you sure?';
|
||||
render(
|
||||
<ConsentPrompt
|
||||
prompt={prompt}
|
||||
onConfirm={onConfirm}
|
||||
terminalWidth={terminalWidth}
|
||||
/>,
|
||||
);
|
||||
|
||||
const onSelect = MockedRadioButtonSelect.mock.calls[0][0].onSelect;
|
||||
onSelect(false);
|
||||
|
||||
expect(onConfirm).toHaveBeenCalledWith(false);
|
||||
});
|
||||
|
||||
it('passes correct items to RadioButtonSelect', () => {
|
||||
const prompt = 'Are you sure?';
|
||||
render(
|
||||
<ConsentPrompt
|
||||
prompt={prompt}
|
||||
onConfirm={onConfirm}
|
||||
terminalWidth={terminalWidth}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(MockedRadioButtonSelect).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
items: [
|
||||
{ label: 'Yes', value: true, key: 'Yes' },
|
||||
{ label: 'No', value: false, key: 'No' },
|
||||
],
|
||||
}),
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
});
|
||||
51
packages/cli/src/ui/components/ConsentPrompt.tsx
Normal file
51
packages/cli/src/ui/components/ConsentPrompt.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { Box } from 'ink';
|
||||
import { type ReactNode } from 'react';
|
||||
import { theme } from '../semantic-colors.js';
|
||||
import { MarkdownDisplay } from '../utils/MarkdownDisplay.js';
|
||||
import { RadioButtonSelect } from './shared/RadioButtonSelect.js';
|
||||
|
||||
type ConsentPromptProps = {
|
||||
// If a simple string is given, it will render using markdown by default.
|
||||
prompt: ReactNode;
|
||||
onConfirm: (value: boolean) => void;
|
||||
terminalWidth: number;
|
||||
};
|
||||
|
||||
export const ConsentPrompt = (props: ConsentPromptProps) => {
|
||||
const { prompt, onConfirm, terminalWidth } = props;
|
||||
|
||||
return (
|
||||
<Box
|
||||
borderStyle="round"
|
||||
borderColor={theme.border.default}
|
||||
flexDirection="column"
|
||||
paddingY={1}
|
||||
paddingX={2}
|
||||
>
|
||||
{typeof prompt === 'string' ? (
|
||||
<MarkdownDisplay
|
||||
isPending={true}
|
||||
text={prompt}
|
||||
terminalWidth={terminalWidth}
|
||||
/>
|
||||
) : (
|
||||
prompt
|
||||
)}
|
||||
<Box marginTop={1}>
|
||||
<RadioButtonSelect
|
||||
items={[
|
||||
{ label: 'Yes', value: true, key: 'Yes' },
|
||||
{ label: 'No', value: false, key: 'No' },
|
||||
]}
|
||||
onSelect={onConfirm}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -9,7 +9,7 @@ import { IdeIntegrationNudge } from '../IdeIntegrationNudge.js';
|
||||
import { LoopDetectionConfirmation } from './LoopDetectionConfirmation.js';
|
||||
import { FolderTrustDialog } from './FolderTrustDialog.js';
|
||||
import { ShellConfirmationDialog } from './ShellConfirmationDialog.js';
|
||||
import { RadioButtonSelect } from './shared/RadioButtonSelect.js';
|
||||
import { ConsentPrompt } from './ConsentPrompt.js';
|
||||
import { ThemeDialog } from './ThemeDialog.js';
|
||||
import { SettingsDialog } from './SettingsDialog.js';
|
||||
import { AuthInProgress } from '../auth/AuthInProgress.js';
|
||||
@@ -31,10 +31,14 @@ import { IdeTrustChangeDialog } from './IdeTrustChangeDialog.js';
|
||||
|
||||
interface DialogManagerProps {
|
||||
addItem: UseHistoryManagerReturn['addItem'];
|
||||
terminalWidth: number;
|
||||
}
|
||||
|
||||
// Props for DialogManager
|
||||
export const DialogManager = ({ addItem }: DialogManagerProps) => {
|
||||
export const DialogManager = ({
|
||||
addItem,
|
||||
terminalWidth,
|
||||
}: DialogManagerProps) => {
|
||||
const config = useConfig();
|
||||
const settings = useSettings();
|
||||
|
||||
@@ -94,20 +98,21 @@ export const DialogManager = ({ addItem }: DialogManagerProps) => {
|
||||
}
|
||||
if (uiState.confirmationRequest) {
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
{uiState.confirmationRequest.prompt}
|
||||
<Box paddingY={1}>
|
||||
<RadioButtonSelect
|
||||
items={[
|
||||
{ label: 'Yes', value: true, key: 'Yes' },
|
||||
{ label: 'No', value: false, key: 'No' },
|
||||
]}
|
||||
onSelect={(value: boolean) => {
|
||||
uiState.confirmationRequest!.onConfirm(value);
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
<ConsentPrompt
|
||||
prompt={uiState.confirmationRequest.prompt}
|
||||
onConfirm={uiState.confirmationRequest.onConfirm}
|
||||
terminalWidth={terminalWidth}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (uiState.confirmUpdateExtensionRequests.length > 0) {
|
||||
const request = uiState.confirmUpdateExtensionRequests[0];
|
||||
return (
|
||||
<ConsentPrompt
|
||||
prompt={request.prompt}
|
||||
onConfirm={request.onConfirm}
|
||||
terminalWidth={terminalWidth}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (uiState.isThemeDialogOpen) {
|
||||
|
||||
@@ -61,6 +61,7 @@ export interface UIState {
|
||||
commandContext: CommandContext;
|
||||
shellConfirmationRequest: ShellConfirmationRequest | null;
|
||||
confirmationRequest: ConfirmationRequest | null;
|
||||
confirmUpdateExtensionRequests: ConfirmationRequest[];
|
||||
loopDetectionConfirmationRequest: LoopDetectionConfirmationRequest | null;
|
||||
geminiMdFileCount: number;
|
||||
streamingState: StreamingState;
|
||||
|
||||
@@ -32,6 +32,7 @@ import type {
|
||||
HistoryItemWithoutId,
|
||||
SlashCommandProcessorResult,
|
||||
HistoryItem,
|
||||
ConfirmationRequest,
|
||||
} from '../types.js';
|
||||
import { MessageType } from '../types.js';
|
||||
import type { LoadedSettings } from '../../config/settings.js';
|
||||
@@ -57,6 +58,7 @@ interface SlashCommandProcessorActions {
|
||||
setExtensionsUpdateState: Dispatch<
|
||||
SetStateAction<Map<string, ExtensionUpdateState>>
|
||||
>;
|
||||
addConfirmUpdateExtensionRequest: (request: ConfirmationRequest) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -206,6 +208,8 @@ export const useSlashCommandProcessor = (
|
||||
reloadCommands,
|
||||
extensionsUpdateState,
|
||||
setExtensionsUpdateState: actions.setExtensionsUpdateState,
|
||||
addConfirmUpdateExtensionRequest:
|
||||
actions.addConfirmUpdateExtensionRequest,
|
||||
},
|
||||
session: {
|
||||
stats: session.stats,
|
||||
|
||||
@@ -7,9 +7,9 @@
|
||||
import type { GeminiCLIExtension } from '@google/gemini-cli-core';
|
||||
import { getErrorMessage } from '../../utils/errors.js';
|
||||
import { ExtensionUpdateState } from '../state/extensions.js';
|
||||
import { useState } from 'react';
|
||||
import { useCallback, useState } from 'react';
|
||||
import type { UseHistoryManagerReturn } from './useHistoryManager.js';
|
||||
import { MessageType } from '../types.js';
|
||||
import { MessageType, type ConfirmationRequest } from '../types.js';
|
||||
import {
|
||||
checkForAllExtensionUpdates,
|
||||
updateExtension,
|
||||
@@ -25,6 +25,29 @@ export const useExtensionUpdates = (
|
||||
new Map<string, ExtensionUpdateState>(),
|
||||
);
|
||||
const [isChecking, setIsChecking] = useState(false);
|
||||
const [confirmUpdateExtensionRequests, setConfirmUpdateExtensionRequests] =
|
||||
useState<
|
||||
Array<{
|
||||
prompt: React.ReactNode;
|
||||
onConfirm: (confirmed: boolean) => void;
|
||||
}>
|
||||
>([]);
|
||||
const addConfirmUpdateExtensionRequest = useCallback(
|
||||
(original: ConfirmationRequest) => {
|
||||
const wrappedRequest = {
|
||||
prompt: original.prompt,
|
||||
onConfirm: (confirmed: boolean) => {
|
||||
// Remove it from the outstanding list of requests by identity.
|
||||
setConfirmUpdateExtensionRequests((prev) =>
|
||||
prev.filter((r) => r !== wrappedRequest),
|
||||
);
|
||||
original.onConfirm(confirmed);
|
||||
},
|
||||
};
|
||||
setConfirmUpdateExtensionRequests((prev) => [...prev, wrappedRequest]);
|
||||
},
|
||||
[setConfirmUpdateExtensionRequests],
|
||||
);
|
||||
|
||||
(async () => {
|
||||
if (isChecking) return;
|
||||
@@ -49,7 +72,11 @@ export const useExtensionUpdates = (
|
||||
updateExtension(
|
||||
extension,
|
||||
cwd,
|
||||
(description) => requestConsentInteractive(description, addItem),
|
||||
(description) =>
|
||||
requestConsentInteractive(
|
||||
description,
|
||||
addConfirmUpdateExtensionRequest,
|
||||
),
|
||||
currentState,
|
||||
(newState) => {
|
||||
setExtensionsUpdateState((prev) => {
|
||||
@@ -70,8 +97,12 @@ export const useExtensionUpdates = (
|
||||
);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(
|
||||
`Error updating extension "${extension.name}": ${getErrorMessage(error)}.`,
|
||||
addItem(
|
||||
{
|
||||
type: MessageType.ERROR,
|
||||
text: getErrorMessage(error),
|
||||
},
|
||||
Date.now(),
|
||||
);
|
||||
});
|
||||
} else {
|
||||
@@ -96,5 +127,7 @@ export const useExtensionUpdates = (
|
||||
return {
|
||||
extensionsUpdateState,
|
||||
setExtensionsUpdateState,
|
||||
confirmUpdateExtensionRequests,
|
||||
addConfirmUpdateExtensionRequest,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -24,7 +24,10 @@ export const DefaultAppLayout: React.FC = () => {
|
||||
<Notifications />
|
||||
|
||||
{uiState.dialogsVisible ? (
|
||||
<DialogManager addItem={uiState.historyManager.addItem} />
|
||||
<DialogManager
|
||||
terminalWidth={uiState.terminalWidth}
|
||||
addItem={uiState.historyManager.addItem}
|
||||
/>
|
||||
) : (
|
||||
<Composer />
|
||||
)}
|
||||
|
||||
@@ -25,7 +25,10 @@ export const ScreenReaderAppLayout: React.FC = () => {
|
||||
<MainContent />
|
||||
</Box>
|
||||
{uiState.dialogsVisible ? (
|
||||
<DialogManager addItem={uiState.historyManager.addItem} />
|
||||
<DialogManager
|
||||
terminalWidth={uiState.terminalWidth}
|
||||
addItem={uiState.historyManager.addItem}
|
||||
/>
|
||||
) : (
|
||||
<Composer />
|
||||
)}
|
||||
|
||||
@@ -25,5 +25,6 @@ export function createNonInteractiveUI(): CommandContext['ui'] {
|
||||
reloadCommands: () => {},
|
||||
extensionsUpdateState: new Map(),
|
||||
setExtensionsUpdateState: (_updateState) => {},
|
||||
addConfirmUpdateExtensionRequest: (_request) => {},
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user