diff --git a/packages/cli/src/i18n/locales/de.js b/packages/cli/src/i18n/locales/de.js index b96ddda61..c19f89c79 100644 --- a/packages/cli/src/i18n/locales/de.js +++ b/packages/cli/src/i18n/locales/de.js @@ -1285,4 +1285,12 @@ export default { 'Haben Sie versucht, es aus- und wieder einzuschalten? (Den Ladebildschirm, nicht mich.)', 'Zusätzliche Pylonen werden gebaut...', ], + + // ============================================================================ + // Extension Settings Input + // ============================================================================ + 'Enter value...': 'Wert eingeben...', + 'Enter sensitive value...': 'Sensiblen Wert eingeben...', + 'Press Enter to submit, Escape to cancel': + 'Enter zum Absenden, Escape zum Abbrechen drücken', }; diff --git a/packages/cli/src/i18n/locales/en.js b/packages/cli/src/i18n/locales/en.js index 08c6ed5d0..b08d9fbdf 100644 --- a/packages/cli/src/i18n/locales/en.js +++ b/packages/cli/src/i18n/locales/en.js @@ -1249,4 +1249,12 @@ export default { 'Have you tried turning it off and on again? (The loading screen, not me.)', 'Constructing additional pylons...', ], + + // ============================================================================ + // Extension Settings Input + // ============================================================================ + 'Enter value...': 'Enter value...', + 'Enter sensitive value...': 'Enter sensitive value...', + 'Press Enter to submit, Escape to cancel': + 'Press Enter to submit, Escape to cancel', }; diff --git a/packages/cli/src/i18n/locales/ru.js b/packages/cli/src/i18n/locales/ru.js index 42e2fa2ca..b5235362c 100644 --- a/packages/cli/src/i18n/locales/ru.js +++ b/packages/cli/src/i18n/locales/ru.js @@ -1268,4 +1268,12 @@ export default { 'Пробовали выключить и включить снова? (Экран загрузки, не меня!)', 'Нужно построить больше пилонов...', ], + + // ============================================================================ + // Extension Settings Input + // ============================================================================ + 'Enter value...': 'Введите значение...', + 'Enter sensitive value...': 'Введите секретное значение...', + 'Press Enter to submit, Escape to cancel': + 'Нажмите Enter для отправки, Escape для отмены', }; diff --git a/packages/cli/src/i18n/locales/zh.js b/packages/cli/src/i18n/locales/zh.js index c33b47ed5..c6b3b3231 100644 --- a/packages/cli/src/i18n/locales/zh.js +++ b/packages/cli/src/i18n/locales/zh.js @@ -1092,4 +1092,11 @@ export default { '哪怕只有 0.1% 的进度,也是在向目标靠近...', '加载的是字节,承载的是对技术的热爱...', ], + + // ============================================================================ + // Extension Settings Input + // ============================================================================ + 'Enter value...': '请输入值...', + 'Enter sensitive value...': '请输入敏感值...', + 'Press Enter to submit, Escape to cancel': '按 Enter 提交,Escape 取消', }; diff --git a/packages/cli/src/ui/AppContainer.tsx b/packages/cli/src/ui/AppContainer.tsx index 0a2a4a6b8..d1a24d97f 100644 --- a/packages/cli/src/ui/AppContainer.tsx +++ b/packages/cli/src/ui/AppContainer.tsx @@ -92,6 +92,7 @@ import { useGitBranchName } from './hooks/useGitBranchName.js'; import { useExtensionUpdates, useConfirmUpdateRequests, + useSettingInputRequests, } from './hooks/useExtensionUpdates.js'; import { ShellFocusContext } from './contexts/ShellFocusContext.js'; import { t } from '../i18n/index.js'; @@ -169,14 +170,34 @@ export const AppContainer = (props: AppContainerProps) => { const extensionManager = config.getExtensionManager(); + const { addConfirmUpdateExtensionRequest, confirmUpdateExtensionRequests } = + useConfirmUpdateRequests(); + + const { addSettingInputRequest, settingInputRequests } = + useSettingInputRequests(); + extensionManager.setRequestConsent( requestConsentOrFail.bind(null, (description) => requestConsentInteractive(description, addConfirmUpdateExtensionRequest), ), ); - const { addConfirmUpdateExtensionRequest, confirmUpdateExtensionRequests } = - useConfirmUpdateRequests(); + extensionManager.setRequestSetting( + (setting) => + new Promise((resolve, reject) => { + addSettingInputRequest({ + settingName: setting.name, + settingDescription: setting.description, + sensitive: setting.sensitive ?? false, + onSubmit: (value) => { + resolve(value); + }, + onCancel: () => { + reject(new Error('Setting input cancelled')); + }, + }); + }), + ); const { extensionsUpdateState, @@ -1284,6 +1305,7 @@ export const AppContainer = (props: AppContainerProps) => { !!shellConfirmationRequest || !!confirmationRequest || confirmUpdateExtensionRequests.length > 0 || + settingInputRequests.length > 0 || !!loopDetectionConfirmationRequest || isThemeDialogOpen || isSettingsDialogOpen || @@ -1345,6 +1367,7 @@ export const AppContainer = (props: AppContainerProps) => { shellConfirmationRequest, confirmationRequest, confirmUpdateExtensionRequests, + settingInputRequests, loopDetectionConfirmationRequest, geminiMdFileCount, streamingState, @@ -1436,6 +1459,7 @@ export const AppContainer = (props: AppContainerProps) => { shellConfirmationRequest, confirmationRequest, confirmUpdateExtensionRequests, + settingInputRequests, loopDetectionConfirmationRequest, geminiMdFileCount, streamingState, diff --git a/packages/cli/src/ui/components/DialogManager.tsx b/packages/cli/src/ui/components/DialogManager.tsx index 48a622e70..675d17995 100644 --- a/packages/cli/src/ui/components/DialogManager.tsx +++ b/packages/cli/src/ui/components/DialogManager.tsx @@ -11,6 +11,7 @@ import { LoopDetectionConfirmation } from './LoopDetectionConfirmation.js'; import { FolderTrustDialog } from './FolderTrustDialog.js'; import { ShellConfirmationDialog } from './ShellConfirmationDialog.js'; import { ConsentPrompt } from './ConsentPrompt.js'; +import { SettingInputPrompt } from './SettingInputPrompt.js'; import { ThemeDialog } from './ThemeDialog.js'; import { SettingsDialog } from './SettingsDialog.js'; import { QwenOAuthProgress } from './QwenOAuthProgress.js'; @@ -131,6 +132,21 @@ export const DialogManager = ({ /> ); } + if (uiState.settingInputRequests.length > 0) { + const request = uiState.settingInputRequests[0]; + // Use settingName as key to force re-mount when switching between different settings + return ( + + ); + } if (uiState.isThemeDialogOpen) { return ( diff --git a/packages/cli/src/ui/components/SettingInputPrompt.test.tsx b/packages/cli/src/ui/components/SettingInputPrompt.test.tsx new file mode 100644 index 000000000..ff2755fb9 --- /dev/null +++ b/packages/cli/src/ui/components/SettingInputPrompt.test.tsx @@ -0,0 +1,133 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { render } from 'ink-testing-library'; +import { SettingInputPrompt } from './SettingInputPrompt.js'; +import { TextInput } from './shared/TextInput.js'; + +vi.mock('./shared/TextInput.js', () => ({ + TextInput: vi.fn(() => null), +})); + +vi.mock('../hooks/useKeypress.js', () => ({ + useKeypress: vi.fn(), +})); + +const MockedTextInput = vi.mocked(TextInput); + +describe('SettingInputPrompt', () => { + const onSubmit = vi.fn(); + const onCancel = vi.fn(); + const terminalWidth = 80; + + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('renders setting name and description', () => { + const { lastFrame } = render( + , + ); + + expect(lastFrame()).toContain('API_KEY'); + expect(lastFrame()).toContain('Enter your API key'); + }); + + it('renders TextInput for non-sensitive values', () => { + render( + , + ); + + expect(MockedTextInput).toHaveBeenCalled(); + }); + + it('does not render TextInput for sensitive values (uses PasswordInput)', () => { + MockedTextInput.mockClear(); + render( + , + ); + + // TextInput should not be called for sensitive input + expect(MockedTextInput).not.toHaveBeenCalled(); + }); + + it('shows masked input placeholder for sensitive mode', () => { + const { lastFrame } = render( + , + ); + + // Should show the sensitive placeholder hint + expect(lastFrame()).toContain('PASSWORD'); + expect(lastFrame()).toContain('Enter your password'); + }); + + it('displays help text for submit and cancel', () => { + const { lastFrame } = render( + , + ); + + expect(lastFrame()).toContain('Enter'); + expect(lastFrame()).toContain('Escape'); + }); + + it('passes correct props to TextInput for non-sensitive input', () => { + render( + , + ); + + expect(MockedTextInput).toHaveBeenCalledWith( + expect.objectContaining({ + value: '', + isActive: true, + inputWidth: expect.any(Number), + }), + undefined, + ); + }); +}); diff --git a/packages/cli/src/ui/components/SettingInputPrompt.tsx b/packages/cli/src/ui/components/SettingInputPrompt.tsx new file mode 100644 index 000000000..6f4c432ca --- /dev/null +++ b/packages/cli/src/ui/components/SettingInputPrompt.tsx @@ -0,0 +1,157 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Box, Text } from 'ink'; +import { useState } from 'react'; +import { theme } from '../semantic-colors.js'; +import { TextInput } from './shared/TextInput.js'; +import { t } from '../../i18n/index.js'; +import { useKeypress, type Key } from '../hooks/useKeypress.js'; +import chalk from 'chalk'; + +type SettingInputPromptProps = { + settingName: string; + settingDescription: string; + sensitive: boolean; + onSubmit: (value: string) => void; + onCancel: () => void; + terminalWidth: number; +}; + +/** + * A simple password input component that masks the input with asterisks. + */ +const PasswordInput = ({ + value, + onChange, + onSubmit, + placeholder, +}: { + value: string; + onChange: (value: string) => void; + onSubmit: () => void; + placeholder: string; +}) => { + useKeypress( + (key: Key) => { + // Handle submit + if (key.name === 'return') { + onSubmit(); + return; + } + + // Handle backspace + if (key.name === 'backspace' || key.name === 'delete') { + onChange(value.slice(0, -1)); + return; + } + + // Handle clear (Ctrl+U) + if (key.ctrl && key.name === 'u') { + onChange(''); + return; + } + + // Handle printable characters + if (key.sequence && !key.ctrl && !key.meta && key.sequence.length === 1) { + const charCode = key.sequence.charCodeAt(0); + // Only accept printable ASCII characters (32-126) + if (charCode >= 32 && charCode <= 126) { + onChange(value + key.sequence); + } + } + }, + { isActive: true }, + ); + + const maskedValue = '*'.repeat(value.length); + const displayValue = maskedValue || ''; + const cursorChar = chalk.inverse(' '); + + return ( + + {'> '} + {value.length === 0 ? ( + + {cursorChar} + {placeholder.slice(1)} + + ) : ( + + {displayValue} + {cursorChar} + + )} + + ); +}; + +export const SettingInputPrompt = (props: SettingInputPromptProps) => { + const { + settingName, + settingDescription, + sensitive, + onSubmit, + onCancel, + terminalWidth, + } = props; + + const [value, setValue] = useState(''); + + useKeypress( + (key) => { + if (key.name === 'escape') { + onCancel(); + } + }, + { isActive: true }, + ); + + const handleSubmit = () => { + if (value.trim()) { + onSubmit(value); + } + }; + + return ( + + + {settingName} + + + {settingDescription} + + + {sensitive ? ( + + ) : ( + + )} + + + {t('Press Enter to submit, Escape to cancel')} + + + ); +}; diff --git a/packages/cli/src/ui/contexts/UIStateContext.tsx b/packages/cli/src/ui/contexts/UIStateContext.tsx index 75e568b4e..e52dc7fd9 100644 --- a/packages/cli/src/ui/contexts/UIStateContext.tsx +++ b/packages/cli/src/ui/contexts/UIStateContext.tsx @@ -14,6 +14,7 @@ import type { LoopDetectionConfirmationRequest, HistoryItemWithoutId, StreamingState, + SettingInputRequest, } from '../types.js'; import type { QwenAuthState } from '../hooks/useQwenAuth.js'; import type { CommandContext, SlashCommand } from '../commands/types.js'; @@ -59,6 +60,7 @@ export interface UIState { shellConfirmationRequest: ShellConfirmationRequest | null; confirmationRequest: ConfirmationRequest | null; confirmUpdateExtensionRequests: ConfirmationRequest[]; + settingInputRequests: SettingInputRequest[]; loopDetectionConfirmationRequest: LoopDetectionConfirmationRequest | null; geminiMdFileCount: number; streamingState: StreamingState; diff --git a/packages/cli/src/ui/hooks/useExtensionUpdates.test.ts b/packages/cli/src/ui/hooks/useExtensionUpdates.test.ts index 0475b0bad..bb297b473 100644 --- a/packages/cli/src/ui/hooks/useExtensionUpdates.test.ts +++ b/packages/cli/src/ui/hooks/useExtensionUpdates.test.ts @@ -9,7 +9,11 @@ import * as fs from 'node:fs'; import * as os from 'node:os'; import * as path from 'node:path'; -import { useExtensionUpdates } from './useExtensionUpdates.js'; +import { + useExtensionUpdates, + useSettingInputRequests, + useConfirmUpdateRequests, +} from './useExtensionUpdates.js'; import { QWEN_DIR, type ExtensionManager, @@ -17,7 +21,7 @@ import { type ExtensionUpdateInfo, ExtensionUpdateState, } from '@qwen-code/qwen-code-core'; -import { renderHook, waitFor } from '@testing-library/react'; +import { renderHook, waitFor, act } from '@testing-library/react'; import { MessageType } from '../types.js'; vi.mock('os', async (importOriginal) => { @@ -71,6 +75,202 @@ function createMockExtensionManager( } as unknown as ExtensionManager; } +describe('useConfirmUpdateRequests', () => { + it('should add a confirmation request', () => { + const { result } = renderHook(() => useConfirmUpdateRequests()); + + const onConfirm = vi.fn(); + act(() => { + result.current.addConfirmUpdateExtensionRequest({ + prompt: 'Test prompt', + onConfirm, + }); + }); + + expect(result.current.confirmUpdateExtensionRequests).toHaveLength(1); + expect(result.current.confirmUpdateExtensionRequests[0].prompt).toBe( + 'Test prompt', + ); + }); + + it('should remove a confirmation request when confirmed', () => { + const { result } = renderHook(() => useConfirmUpdateRequests()); + + const onConfirm = vi.fn(); + act(() => { + result.current.addConfirmUpdateExtensionRequest({ + prompt: 'Test prompt', + onConfirm, + }); + }); + + expect(result.current.confirmUpdateExtensionRequests).toHaveLength(1); + + // Confirm the request + act(() => { + result.current.confirmUpdateExtensionRequests[0].onConfirm(true); + }); + + expect(result.current.confirmUpdateExtensionRequests).toHaveLength(0); + expect(onConfirm).toHaveBeenCalledWith(true); + }); + + it('should handle multiple confirmation requests', () => { + const { result } = renderHook(() => useConfirmUpdateRequests()); + + const onConfirm1 = vi.fn(); + const onConfirm2 = vi.fn(); + + act(() => { + result.current.addConfirmUpdateExtensionRequest({ + prompt: 'Prompt 1', + onConfirm: onConfirm1, + }); + result.current.addConfirmUpdateExtensionRequest({ + prompt: 'Prompt 2', + onConfirm: onConfirm2, + }); + }); + + expect(result.current.confirmUpdateExtensionRequests).toHaveLength(2); + + // Confirm first request + act(() => { + result.current.confirmUpdateExtensionRequests[0].onConfirm(false); + }); + + expect(result.current.confirmUpdateExtensionRequests).toHaveLength(1); + expect(result.current.confirmUpdateExtensionRequests[0].prompt).toBe( + 'Prompt 2', + ); + expect(onConfirm1).toHaveBeenCalledWith(false); + }); +}); + +describe('useSettingInputRequests', () => { + it('should add a setting input request', () => { + const { result } = renderHook(() => useSettingInputRequests()); + + const onSubmit = vi.fn(); + const onCancel = vi.fn(); + act(() => { + result.current.addSettingInputRequest({ + settingName: 'API_KEY', + settingDescription: 'Enter your API key', + sensitive: true, + onSubmit, + onCancel, + }); + }); + + expect(result.current.settingInputRequests).toHaveLength(1); + expect(result.current.settingInputRequests[0].settingName).toBe('API_KEY'); + expect(result.current.settingInputRequests[0].settingDescription).toBe( + 'Enter your API key', + ); + expect(result.current.settingInputRequests[0].sensitive).toBe(true); + }); + + it('should remove a setting input request when submitted', () => { + const { result } = renderHook(() => useSettingInputRequests()); + + const onSubmit = vi.fn(); + const onCancel = vi.fn(); + act(() => { + result.current.addSettingInputRequest({ + settingName: 'API_KEY', + settingDescription: 'Enter your API key', + sensitive: true, + onSubmit, + onCancel, + }); + }); + + expect(result.current.settingInputRequests).toHaveLength(1); + + // Submit the value + act(() => { + result.current.settingInputRequests[0].onSubmit('my-secret-key'); + }); + + expect(result.current.settingInputRequests).toHaveLength(0); + expect(onSubmit).toHaveBeenCalledWith('my-secret-key'); + expect(onCancel).not.toHaveBeenCalled(); + }); + + it('should remove a setting input request when cancelled', () => { + const { result } = renderHook(() => useSettingInputRequests()); + + const onSubmit = vi.fn(); + const onCancel = vi.fn(); + act(() => { + result.current.addSettingInputRequest({ + settingName: 'API_KEY', + settingDescription: 'Enter your API key', + sensitive: true, + onSubmit, + onCancel, + }); + }); + + expect(result.current.settingInputRequests).toHaveLength(1); + + // Cancel the request + act(() => { + result.current.settingInputRequests[0].onCancel(); + }); + + expect(result.current.settingInputRequests).toHaveLength(0); + expect(onCancel).toHaveBeenCalled(); + expect(onSubmit).not.toHaveBeenCalled(); + }); + + it('should handle multiple setting input requests in sequence', () => { + const { result } = renderHook(() => useSettingInputRequests()); + + const onSubmit1 = vi.fn(); + const onCancel1 = vi.fn(); + const onSubmit2 = vi.fn(); + const onCancel2 = vi.fn(); + + act(() => { + result.current.addSettingInputRequest({ + settingName: 'USERNAME', + settingDescription: 'Enter username', + sensitive: false, + onSubmit: onSubmit1, + onCancel: onCancel1, + }); + result.current.addSettingInputRequest({ + settingName: 'PASSWORD', + settingDescription: 'Enter password', + sensitive: true, + onSubmit: onSubmit2, + onCancel: onCancel2, + }); + }); + + expect(result.current.settingInputRequests).toHaveLength(2); + + // Submit first request + act(() => { + result.current.settingInputRequests[0].onSubmit('john_doe'); + }); + + expect(result.current.settingInputRequests).toHaveLength(1); + expect(result.current.settingInputRequests[0].settingName).toBe('PASSWORD'); + expect(onSubmit1).toHaveBeenCalledWith('john_doe'); + + // Submit second request + act(() => { + result.current.settingInputRequests[0].onSubmit('secret123'); + }); + + expect(result.current.settingInputRequests).toHaveLength(0); + expect(onSubmit2).toHaveBeenCalledWith('secret123'); + }); +}); + describe('useExtensionUpdates', () => { let tempHomeDir: string; let userExtensionsDir: string; diff --git a/packages/cli/src/ui/hooks/useExtensionUpdates.ts b/packages/cli/src/ui/hooks/useExtensionUpdates.ts index 4d409e100..b547698f9 100644 --- a/packages/cli/src/ui/hooks/useExtensionUpdates.ts +++ b/packages/cli/src/ui/hooks/useExtensionUpdates.ts @@ -13,7 +13,11 @@ import { } from '../state/extensions.js'; import { useCallback, useEffect, useMemo, useReducer } from 'react'; import type { UseHistoryManagerReturn } from './useHistoryManager.js'; -import { MessageType, type ConfirmationRequest } from '../types.js'; +import { + MessageType, + type ConfirmationRequest, + type SettingInputRequest, +} from '../types.js'; import { checkExhaustive } from '../../utils/checks.js'; type ConfirmationRequestWrapper = { @@ -72,6 +76,74 @@ export const useConfirmUpdateRequests = () => { }; }; +type SettingInputRequestWrapper = { + settingName: string; + settingDescription: string; + sensitive: boolean; + onSubmit: (value: string) => void; + onCancel: () => void; +}; + +type SettingInputRequestAction = + | { type: 'add'; request: SettingInputRequestWrapper } + | { type: 'remove'; request: SettingInputRequestWrapper }; + +function settingInputRequestsReducer( + state: SettingInputRequestWrapper[], + action: SettingInputRequestAction, +): SettingInputRequestWrapper[] { + switch (action.type) { + case 'add': + return [...state, action.request]; + case 'remove': + return state.filter((r) => r !== action.request); + default: + checkExhaustive(action); + return state; + } +} + +export const useSettingInputRequests = () => { + const [settingInputRequests, dispatchSettingInputRequests] = useReducer( + settingInputRequestsReducer, + [], + ); + const addSettingInputRequest = useCallback( + (original: SettingInputRequest) => { + const wrappedRequest: SettingInputRequestWrapper = { + settingName: original.settingName, + settingDescription: original.settingDescription, + sensitive: original.sensitive, + onSubmit: (value: string) => { + // Remove it from the outstanding list of requests by identity. + dispatchSettingInputRequests({ + type: 'remove', + request: wrappedRequest, + }); + original.onSubmit(value); + }, + onCancel: () => { + dispatchSettingInputRequests({ + type: 'remove', + request: wrappedRequest, + }); + original.onCancel(); + }, + }; + dispatchSettingInputRequests({ + type: 'add', + request: wrappedRequest, + }); + }, + [dispatchSettingInputRequests], + ); + return { + addSettingInputRequest, + settingInputRequests, + dispatchSettingInputRequests, + }; +}; + export const useExtensionUpdates = ( extensionManager: ExtensionManager, addItem: UseHistoryManagerReturn['addItem'], diff --git a/packages/cli/src/ui/types.ts b/packages/cli/src/ui/types.ts index ff7e68aaf..bc1bd3dcd 100644 --- a/packages/cli/src/ui/types.ts +++ b/packages/cli/src/ui/types.ts @@ -414,3 +414,11 @@ export interface ConfirmationRequest { export interface LoopDetectionConfirmationRequest { onComplete: (result: { userSelection: 'disable' | 'keep' }) => void; } + +export interface SettingInputRequest { + settingName: string; + settingDescription: string; + sensitive: boolean; + onSubmit: (value: string) => void; + onCancel: () => void; +} diff --git a/packages/core/src/extension/extensionManager.ts b/packages/core/src/extension/extensionManager.ts index 7c6ed12bf..d41207ae2 100644 --- a/packages/core/src/extension/extensionManager.ts +++ b/packages/core/src/extension/extensionManager.ts @@ -151,6 +151,7 @@ export interface ExtensionManagerOptions { telemetrySettings?: TelemetrySettings; config?: Config; requestConsent?: (options?: ExtensionRequestOptions) => Promise; + requestSetting?: (setting: ExtensionSetting) => Promise; } // ============================================================================ @@ -273,6 +274,7 @@ export class ExtensionManager { private telemetrySettings?: TelemetrySettings; private isWorkspaceTrusted: boolean; private requestConsent: (options?: ExtensionRequestOptions) => Promise; + private requestSetting?: (setting: ExtensionSetting) => Promise; constructor(options: ExtensionManagerOptions) { this.workspaceDir = options.workspaceDir ?? process.cwd(); @@ -284,6 +286,7 @@ export class ExtensionManager { this.configDir, 'extension-enablement.json', ); + this.requestSetting = options.requestSetting; this.requestConsent = options.requestConsent || (() => Promise.resolve()); this.config = options.config; this.telemetrySettings = options.telemetrySettings; @@ -300,6 +303,12 @@ export class ExtensionManager { this.requestConsent = requestConsent; } + setRequestSetting( + requestSetting?: (setting: ExtensionSetting) => Promise, + ): void { + this.requestSetting = requestSetting; + } + // ========================================================================== // Enablement functionality (directly implemented) // ========================================================================== @@ -690,6 +699,7 @@ export class ExtensionManager { async installExtension( installMetadata: ExtensionInstallMetadata, requestConsent?: (options?: ExtensionRequestOptions) => Promise, + requestSetting?: (setting: ExtensionSetting) => Promise, cwd?: string, previousExtensionConfig?: ExtensionConfig, ): Promise { @@ -847,7 +857,7 @@ export class ExtensionManager { previousSubagents, }); } else { - this.requestConsent({ + await this.requestConsent({ extensionConfig: newExtensionConfig, commands, skills, @@ -876,7 +886,7 @@ export class ExtensionManager { await maybePromptForSettings( newExtensionConfig, extensionId, - promptForSetting, + requestSetting || this.requestSetting || promptForSetting, previousExtensionConfig, previousSettings, ); @@ -884,7 +894,7 @@ export class ExtensionManager { await maybePromptForSettings( newExtensionConfig, extensionId, - promptForSetting, + requestSetting || this.requestSetting || promptForSetting, ); } @@ -1092,6 +1102,7 @@ export class ExtensionManager { async performWorkspaceExtensionMigration( extensions: Extension[], requestConsent: (options?: ExtensionRequestOptions) => Promise, + requestSetting?: (setting: ExtensionSetting) => Promise, ): Promise { const failedInstallNames: string[] = []; @@ -1101,7 +1112,11 @@ export class ExtensionManager { source: extension.path, type: 'local', }; - await this.installExtension(installMetadata, requestConsent); + await this.installExtension( + installMetadata, + requestConsent, + requestSetting, + ); } catch (_) { failedInstallNames.push(extension.config.name); } @@ -1164,6 +1179,7 @@ export class ExtensionManager { installMetadata, undefined, undefined, + undefined, previousExtensionConfig, ); } catch (e) {