Add install as an option when extension is selected. (#20358)

This commit is contained in:
David Pierce
2026-03-02 20:41:16 +00:00
committed by GitHub
parent 66530e44c8
commit 3a7a6e1540
3 changed files with 66 additions and 2 deletions

View File

@@ -21,6 +21,10 @@ import {
ConfigExtensionDialog,
type ConfigExtensionDialogProps,
} from '../components/ConfigExtensionDialog.js';
import {
ExtensionRegistryView,
type ExtensionRegistryViewProps,
} from '../components/views/ExtensionRegistryView.js';
import { type CommandContext, type SlashCommand } from './types.js';
import {
@@ -39,6 +43,8 @@ import {
} from '../../config/extension-manager.js';
import { SettingScope } from '../../config/settings.js';
import { stat } from 'node:fs/promises';
import { type RegistryExtension } from '../../config/extensionRegistryClient.js';
import { waitFor } from '../../test-utils/async.js';
vi.mock('../../config/extension-manager.js', async (importOriginal) => {
const actual =
@@ -167,6 +173,7 @@ describe('extensionsCommand', () => {
},
ui: {
dispatchExtensionStateUpdate: mockDispatchExtensionState,
removeComponent: vi.fn(),
},
});
});
@@ -429,6 +436,61 @@ describe('extensionsCommand', () => {
throw new Error('Explore action not found');
}
it('should return ExtensionRegistryView custom dialog when experimental.extensionRegistry is true', async () => {
mockContext.services.settings.merged.experimental.extensionRegistry = true;
const result = await exploreAction(mockContext, '');
expect(result).toBeDefined();
if (result?.type !== 'custom_dialog') {
throw new Error('Expected custom_dialog');
}
const component =
result.component as ReactElement<ExtensionRegistryViewProps>;
expect(component.type).toBe(ExtensionRegistryView);
expect(component.props.extensionManager).toBe(mockExtensionLoader);
});
it('should handle onSelect and onClose in ExtensionRegistryView', async () => {
mockContext.services.settings.merged.experimental.extensionRegistry = true;
const result = await exploreAction(mockContext, '');
if (result?.type !== 'custom_dialog') {
throw new Error('Expected custom_dialog');
}
const component =
result.component as ReactElement<ExtensionRegistryViewProps>;
const extension = {
extensionName: 'test-ext',
url: 'https://github.com/test/ext.git',
} as RegistryExtension;
vi.mocked(inferInstallMetadata).mockResolvedValue({
source: extension.url,
type: 'git',
});
mockInstallExtension.mockResolvedValue({ name: extension.url });
// Call onSelect
component.props.onSelect?.(extension);
await waitFor(() => {
expect(inferInstallMetadata).toHaveBeenCalledWith(extension.url);
expect(mockInstallExtension).toHaveBeenCalledWith({
source: extension.url,
type: 'git',
});
});
expect(mockContext.ui.removeComponent).toHaveBeenCalledTimes(1);
// Call onClose
component.props.onClose?.();
expect(mockContext.ui.removeComponent).toHaveBeenCalledTimes(2);
});
it("should add an info message and call 'open' in a non-sandbox environment", async () => {
// Ensure no special environment variables that would affect behavior
vi.stubEnv('NODE_ENV', '');

View File

@@ -280,7 +280,9 @@ async function exploreAction(
type: 'custom_dialog' as const,
component: React.createElement(ExtensionRegistryView, {
onSelect: (extension) => {
debugLogger.debug(`Selected extension: ${extension.extensionName}`);
debugLogger.log(`Selected extension: ${extension.extensionName}`);
void installAction(context, extension.url);
context.ui.removeComponent();
},
onClose: () => context.ui.removeComponent(),
extensionManager,

View File

@@ -24,7 +24,7 @@ import { useRegistrySearch } from '../../hooks/useRegistrySearch.js';
import { useUIState } from '../../contexts/UIStateContext.js';
interface ExtensionRegistryViewProps {
export interface ExtensionRegistryViewProps {
onSelect?: (extension: RegistryExtension) => void;
onClose?: () => void;
extensionManager: ExtensionManager;