feat(cli): Add /model command for interactive model selection (#8940)

Co-authored-by: Miguel Solorio <miguel.solorio07@gmail.com>
This commit is contained in:
Abhi
2025-09-23 12:50:09 -04:00
committed by GitHub
parent c96f8259c1
commit 5151bedf06
19 changed files with 743 additions and 1 deletions

View File

@@ -0,0 +1,86 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { renderWithProviders } from '../../../test-utils/render.js';
import {
DescriptiveRadioButtonSelect,
type DescriptiveRadioSelectItem,
type DescriptiveRadioButtonSelectProps,
} from './DescriptiveRadioButtonSelect.js';
vi.mock('./BaseSelectionList.js', async (importOriginal) => {
const actual =
await importOriginal<typeof import('./BaseSelectionList.js')>();
return {
...actual,
BaseSelectionList: vi.fn(({ children, ...props }) => (
<actual.BaseSelectionList {...props}>{children}</actual.BaseSelectionList>
)),
};
});
vi.mock('../../semantic-colors.js', () => ({
theme: {
text: {
primary: 'COLOR_PRIMARY',
secondary: 'COLOR_SECONDARY',
},
status: {
success: 'COLOR_SUCCESS',
},
},
}));
describe('DescriptiveRadioButtonSelect', () => {
const mockOnSelect = vi.fn();
const mockOnHighlight = vi.fn();
const ITEMS: Array<DescriptiveRadioSelectItem<string>> = [
{ title: 'Foo Title', description: 'This is Foo.', value: 'foo' },
{ title: 'Bar Title', description: 'This is Bar.', value: 'bar' },
{
title: 'Baz Title',
description: 'This is Baz.',
value: 'baz',
disabled: true,
},
];
const renderComponent = (
props: Partial<DescriptiveRadioButtonSelectProps<string>> = {},
) => {
const defaultProps: DescriptiveRadioButtonSelectProps<string> = {
items: ITEMS,
onSelect: mockOnSelect,
...props,
};
return renderWithProviders(
<DescriptiveRadioButtonSelect {...defaultProps} />,
);
};
beforeEach(() => {
vi.clearAllMocks();
});
it('should render correctly with default props', () => {
const { lastFrame } = renderComponent();
expect(lastFrame()).toMatchSnapshot();
});
it('should render correctly with custom props', () => {
const { lastFrame } = renderComponent({
initialIndex: 1,
isFocused: false,
showScrollArrows: true,
maxItemsToShow: 5,
showNumbers: true,
onHighlight: mockOnHighlight,
});
expect(lastFrame()).toMatchSnapshot();
});
});

View File

@@ -0,0 +1,71 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import type React from 'react';
import { Text, Box } from 'ink';
import { theme } from '../../semantic-colors.js';
import { BaseSelectionList } from './BaseSelectionList.js';
export interface DescriptiveRadioSelectItem<T> {
value: T;
title: string;
description: string;
disabled?: boolean;
}
export interface DescriptiveRadioButtonSelectProps<T> {
/** An array of items to display as descriptive radio options. */
items: Array<DescriptiveRadioSelectItem<T>>;
/** The initial index selected */
initialIndex?: number;
/** Function called when an item is selected. Receives the `value` of the selected item. */
onSelect: (value: T) => void;
/** Function called when an item is highlighted. Receives the `value` of the selected item. */
onHighlight?: (value: T) => void;
/** Whether this select input is currently focused and should respond to input. */
isFocused?: boolean;
/** Whether to show numbers next to items. */
showNumbers?: boolean;
/** Whether to show the scroll arrows. */
showScrollArrows?: boolean;
/** The maximum number of items to show at once. */
maxItemsToShow?: number;
}
/**
* A radio button select component that displays items with title and description.
*
* @template T The type of the value associated with each descriptive radio item.
*/
export function DescriptiveRadioButtonSelect<T>({
items,
initialIndex = 0,
onSelect,
onHighlight,
isFocused = true,
showNumbers = false,
showScrollArrows = false,
maxItemsToShow = 10,
}: DescriptiveRadioButtonSelectProps<T>): React.JSX.Element {
return (
<BaseSelectionList<T, DescriptiveRadioSelectItem<T>>
items={items}
initialIndex={initialIndex}
onSelect={onSelect}
onHighlight={onHighlight}
isFocused={isFocused}
showNumbers={showNumbers}
showScrollArrows={showScrollArrows}
maxItemsToShow={maxItemsToShow}
renderItem={(item, { titleColor }) => (
<Box flexDirection="column">
<Text color={titleColor}>{item.title}</Text>
<Text color={theme.text.secondary}>{item.description}</Text>
</Box>
)}
/>
);
}

View File

@@ -0,0 +1,21 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`DescriptiveRadioButtonSelect > should render correctly with custom props 1`] = `
"▲
1. Foo Title
This is Foo.
● 2. Bar Title
This is Bar.
3. Baz Title
This is Baz.
▼"
`;
exports[`DescriptiveRadioButtonSelect > should render correctly with default props 1`] = `
"● Foo Title
This is Foo.
Bar Title
This is Bar.
Baz Title
This is Baz."
`;