mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-04-30 04:30:48 +00:00
Merge pull request #1852 from QwenLM/feat/qwen3.5-plus
Merge coder-model and qwen3.5-plus, remove vision auto-switching
This commit is contained in:
commit
9dc758cfc0
38 changed files with 295 additions and 2090 deletions
|
|
@ -32,7 +32,6 @@ import process from 'node:process';
|
|||
import { type UseHistoryManagerReturn } from '../hooks/useHistoryManager.js';
|
||||
import { IdeTrustChangeDialog } from './IdeTrustChangeDialog.js';
|
||||
import { WelcomeBackDialog } from './WelcomeBackDialog.js';
|
||||
import { ModelSwitchDialog } from './ModelSwitchDialog.js';
|
||||
import { AgentCreationWizard } from './subagents/create/AgentCreationWizard.js';
|
||||
import { AgentsManagerDialog } from './subagents/manage/AgentsManagerDialog.js';
|
||||
import { SessionPicker } from './SessionPicker.js';
|
||||
|
|
@ -236,10 +235,6 @@ export const DialogManager = ({
|
|||
if (uiState.isModelDialogOpen) {
|
||||
return <ModelDialog onClose={uiActions.closeModelDialog} />;
|
||||
}
|
||||
if (uiState.isVisionSwitchDialogOpen) {
|
||||
return <ModelSwitchDialog onSelect={uiActions.handleVisionSwitchSelect} />;
|
||||
}
|
||||
|
||||
if (uiState.isAuthDialogOpen || uiState.authError) {
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
|
|
|
|||
|
|
@ -12,14 +12,10 @@ import { DescriptiveRadioButtonSelect } from './shared/DescriptiveRadioButtonSel
|
|||
import { ConfigContext } from '../contexts/ConfigContext.js';
|
||||
import { SettingsContext } from '../contexts/SettingsContext.js';
|
||||
import type { Config } from '@qwen-code/qwen-code-core';
|
||||
import { AuthType } from '@qwen-code/qwen-code-core';
|
||||
import { AuthType, DEFAULT_QWEN_MODEL } from '@qwen-code/qwen-code-core';
|
||||
import type { LoadedSettings } from '../../config/settings.js';
|
||||
import { SettingScope } from '../../config/settings.js';
|
||||
import {
|
||||
AVAILABLE_MODELS_QWEN,
|
||||
MAINLINE_CODER,
|
||||
MAINLINE_VLM,
|
||||
} from '../models/availableModels.js';
|
||||
import { getFilteredQwenModels } from '../models/availableModels.js';
|
||||
|
||||
vi.mock('../hooks/useKeypress.js', () => ({
|
||||
useKeypress: vi.fn(),
|
||||
|
|
@ -29,6 +25,19 @@ const mockedUseKeypress = vi.mocked(useKeypress);
|
|||
vi.mock('./shared/DescriptiveRadioButtonSelect.js', () => ({
|
||||
DescriptiveRadioButtonSelect: vi.fn(() => null),
|
||||
}));
|
||||
|
||||
// Helper to create getAvailableModelsForAuthType mock
|
||||
const createMockGetAvailableModelsForAuthType = () =>
|
||||
vi.fn((t: AuthType) => {
|
||||
if (t === AuthType.QWEN_OAUTH) {
|
||||
return getFilteredQwenModels().map((m) => ({
|
||||
id: m.id,
|
||||
label: m.label,
|
||||
authType: AuthType.QWEN_OAUTH,
|
||||
}));
|
||||
}
|
||||
return [];
|
||||
});
|
||||
const mockedSelect = vi.mocked(DescriptiveRadioButtonSelect);
|
||||
|
||||
const renderComponent = (
|
||||
|
|
@ -49,12 +58,12 @@ const renderComponent = (
|
|||
|
||||
const mockConfig = {
|
||||
// --- Functions used by ModelDialog ---
|
||||
getModel: vi.fn(() => MAINLINE_CODER),
|
||||
getModel: vi.fn(() => DEFAULT_QWEN_MODEL),
|
||||
setModel: vi.fn().mockResolvedValue(undefined),
|
||||
switchModel: vi.fn().mockResolvedValue(undefined),
|
||||
getAuthType: vi.fn(() => 'qwen-oauth'),
|
||||
getAllConfiguredModels: vi.fn(() =>
|
||||
AVAILABLE_MODELS_QWEN.map((m) => ({
|
||||
getFilteredQwenModels().map((m) => ({
|
||||
id: m.id,
|
||||
label: m.label,
|
||||
description: m.description || '',
|
||||
|
|
@ -68,7 +77,7 @@ const renderComponent = (
|
|||
getDebugMode: vi.fn(() => false),
|
||||
getContentGeneratorConfig: vi.fn(() => ({
|
||||
authType: AuthType.QWEN_OAUTH,
|
||||
model: MAINLINE_CODER,
|
||||
model: DEFAULT_QWEN_MODEL,
|
||||
})),
|
||||
getUseModelRouter: vi.fn(() => false),
|
||||
getProxy: vi.fn(() => undefined),
|
||||
|
|
@ -116,24 +125,34 @@ describe('<ModelDialog />', () => {
|
|||
expect(mockedSelect).toHaveBeenCalledTimes(1);
|
||||
|
||||
const props = mockedSelect.mock.calls[0][0];
|
||||
expect(props.items).toHaveLength(AVAILABLE_MODELS_QWEN.length);
|
||||
expect(props.items).toHaveLength(getFilteredQwenModels().length);
|
||||
// coder-model is the only model and it has vision capability
|
||||
expect(props.items[0].value).toBe(
|
||||
`${AuthType.QWEN_OAUTH}::${MAINLINE_CODER}`,
|
||||
);
|
||||
expect(props.items[1].value).toBe(
|
||||
`${AuthType.QWEN_OAUTH}::${MAINLINE_VLM}`,
|
||||
`${AuthType.QWEN_OAUTH}::${DEFAULT_QWEN_MODEL}`,
|
||||
);
|
||||
expect(props.showNumbers).toBe(true);
|
||||
});
|
||||
|
||||
it('initializes with the model from ConfigContext', () => {
|
||||
const mockGetModel = vi.fn(() => MAINLINE_VLM);
|
||||
renderComponent({}, { getModel: mockGetModel });
|
||||
const mockGetModel = vi.fn(() => DEFAULT_QWEN_MODEL);
|
||||
renderComponent(
|
||||
{},
|
||||
{
|
||||
getModel: mockGetModel,
|
||||
getAvailableModelsForAuthType:
|
||||
createMockGetAvailableModelsForAuthType(),
|
||||
},
|
||||
);
|
||||
|
||||
expect(mockGetModel).toHaveBeenCalled();
|
||||
// Calculate expected index dynamically based on model list
|
||||
const qwenModels = getFilteredQwenModels();
|
||||
const expectedIndex = qwenModels.findIndex(
|
||||
(m) => m.id === DEFAULT_QWEN_MODEL,
|
||||
);
|
||||
expect(mockedSelect).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
initialIndex: 1,
|
||||
initialIndex: expectedIndex,
|
||||
}),
|
||||
undefined,
|
||||
);
|
||||
|
|
@ -151,14 +170,19 @@ describe('<ModelDialog />', () => {
|
|||
});
|
||||
|
||||
it('initializes with default coder model if getModel returns undefined', () => {
|
||||
const mockGetModel = vi.fn(() => undefined);
|
||||
// @ts-expect-error This test validates component robustness when getModel
|
||||
// returns an unexpected undefined value.
|
||||
renderComponent({}, { getModel: mockGetModel });
|
||||
const mockGetModel = vi.fn(() => undefined as unknown as string);
|
||||
renderComponent(
|
||||
{},
|
||||
{
|
||||
getModel: mockGetModel,
|
||||
getAvailableModelsForAuthType:
|
||||
createMockGetAvailableModelsForAuthType(),
|
||||
},
|
||||
);
|
||||
|
||||
expect(mockGetModel).toHaveBeenCalled();
|
||||
|
||||
// When getModel returns undefined, preferredModel falls back to MAINLINE_CODER
|
||||
// When getModel returns undefined, preferredModel falls back to DEFAULT_QWEN_MODEL
|
||||
// which has index 0, so initialIndex should be 0
|
||||
expect(mockedSelect).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
|
|
@ -170,22 +194,36 @@ describe('<ModelDialog />', () => {
|
|||
});
|
||||
|
||||
it('calls config.switchModel and onClose when DescriptiveRadioButtonSelect.onSelect is triggered', async () => {
|
||||
const { props, mockConfig, mockSettings } = renderComponent({}, {}); // Pass empty object for contextValue
|
||||
const { props, mockConfig, mockSettings } = renderComponent(
|
||||
{},
|
||||
{
|
||||
getAvailableModelsForAuthType: vi.fn((t: AuthType) => {
|
||||
if (t === AuthType.QWEN_OAUTH) {
|
||||
return getFilteredQwenModels().map((m) => ({
|
||||
id: m.id,
|
||||
label: m.label,
|
||||
authType: AuthType.QWEN_OAUTH,
|
||||
}));
|
||||
}
|
||||
return [];
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
const childOnSelect = mockedSelect.mock.calls[0][0].onSelect;
|
||||
expect(childOnSelect).toBeDefined();
|
||||
|
||||
await childOnSelect(`${AuthType.QWEN_OAUTH}::${MAINLINE_CODER}`);
|
||||
await childOnSelect(`${AuthType.QWEN_OAUTH}::${DEFAULT_QWEN_MODEL}`);
|
||||
|
||||
expect(mockConfig?.switchModel).toHaveBeenCalledWith(
|
||||
AuthType.QWEN_OAUTH,
|
||||
MAINLINE_CODER,
|
||||
DEFAULT_QWEN_MODEL,
|
||||
undefined,
|
||||
);
|
||||
expect(mockSettings.setValue).toHaveBeenCalledWith(
|
||||
SettingScope.User,
|
||||
'model.name',
|
||||
MAINLINE_CODER,
|
||||
DEFAULT_QWEN_MODEL,
|
||||
);
|
||||
expect(mockSettings.setValue).toHaveBeenCalledWith(
|
||||
SettingScope.User,
|
||||
|
|
@ -203,7 +241,7 @@ describe('<ModelDialog />', () => {
|
|||
return [{ id: 'gpt-4', label: 'GPT-4', authType: t }];
|
||||
}
|
||||
if (t === AuthType.QWEN_OAUTH) {
|
||||
return AVAILABLE_MODELS_QWEN.map((m) => ({
|
||||
return getFilteredQwenModels().map((m) => ({
|
||||
id: m.id,
|
||||
label: m.label,
|
||||
authType: AuthType.QWEN_OAUTH,
|
||||
|
|
@ -217,7 +255,7 @@ describe('<ModelDialog />', () => {
|
|||
getModel: vi.fn(() => 'gpt-4'),
|
||||
getContentGeneratorConfig: vi.fn(() => ({
|
||||
authType: AuthType.QWEN_OAUTH,
|
||||
model: MAINLINE_CODER,
|
||||
model: DEFAULT_QWEN_MODEL,
|
||||
})),
|
||||
// Add switchModel to the mock object (not the type)
|
||||
switchModel,
|
||||
|
|
@ -231,17 +269,17 @@ describe('<ModelDialog />', () => {
|
|||
);
|
||||
|
||||
const childOnSelect = mockedSelect.mock.calls[0][0].onSelect;
|
||||
await childOnSelect(`${AuthType.QWEN_OAUTH}::${MAINLINE_CODER}`);
|
||||
await childOnSelect(`${AuthType.QWEN_OAUTH}::${DEFAULT_QWEN_MODEL}`);
|
||||
|
||||
expect(switchModel).toHaveBeenCalledWith(
|
||||
AuthType.QWEN_OAUTH,
|
||||
MAINLINE_CODER,
|
||||
DEFAULT_QWEN_MODEL,
|
||||
{ requireCachedCredentials: true },
|
||||
);
|
||||
expect(mockSettings.setValue).toHaveBeenCalledWith(
|
||||
SettingScope.User,
|
||||
'model.name',
|
||||
MAINLINE_CODER,
|
||||
DEFAULT_QWEN_MODEL,
|
||||
);
|
||||
expect(mockSettings.setValue).toHaveBeenCalledWith(
|
||||
SettingScope.User,
|
||||
|
|
@ -290,7 +328,7 @@ describe('<ModelDialog />', () => {
|
|||
});
|
||||
|
||||
it('updates initialIndex when config context changes', () => {
|
||||
const mockGetModel = vi.fn(() => MAINLINE_CODER);
|
||||
const mockGetModel = vi.fn(() => DEFAULT_QWEN_MODEL);
|
||||
const mockGetAuthType = vi.fn(() => 'qwen-oauth');
|
||||
const mockSettings = {
|
||||
isTrusted: true,
|
||||
|
|
@ -305,8 +343,10 @@ describe('<ModelDialog />', () => {
|
|||
{
|
||||
getModel: mockGetModel,
|
||||
getAuthType: mockGetAuthType,
|
||||
getAvailableModelsForAuthType:
|
||||
createMockGetAvailableModelsForAuthType(),
|
||||
getAllConfiguredModels: vi.fn(() =>
|
||||
AVAILABLE_MODELS_QWEN.map((m) => ({
|
||||
getFilteredQwenModels().map((m) => ({
|
||||
id: m.id,
|
||||
label: m.label,
|
||||
description: m.description || '',
|
||||
|
|
@ -321,14 +361,16 @@ describe('<ModelDialog />', () => {
|
|||
</SettingsContext.Provider>,
|
||||
);
|
||||
|
||||
// DEFAULT_QWEN_MODEL (coder-model) is at index 0
|
||||
expect(mockedSelect.mock.calls[0][0].initialIndex).toBe(0);
|
||||
|
||||
mockGetModel.mockReturnValue(MAINLINE_VLM);
|
||||
mockGetModel.mockReturnValue(DEFAULT_QWEN_MODEL);
|
||||
const newMockConfig = {
|
||||
getModel: mockGetModel,
|
||||
getAuthType: mockGetAuthType,
|
||||
getAvailableModelsForAuthType: createMockGetAvailableModelsForAuthType(),
|
||||
getAllConfiguredModels: vi.fn(() =>
|
||||
AVAILABLE_MODELS_QWEN.map((m) => ({
|
||||
getFilteredQwenModels().map((m) => ({
|
||||
id: m.id,
|
||||
label: m.label,
|
||||
description: m.description || '',
|
||||
|
|
@ -347,6 +389,11 @@ describe('<ModelDialog />', () => {
|
|||
|
||||
// Should be called at least twice: initial render + re-render after context change
|
||||
expect(mockedSelect).toHaveBeenCalledTimes(2);
|
||||
expect(mockedSelect.mock.calls[1][0].initialIndex).toBe(1);
|
||||
// Calculate expected index for DEFAULT_QWEN_MODEL dynamically
|
||||
const qwenModels = getFilteredQwenModels();
|
||||
const expectedCoderIndex = qwenModels.findIndex(
|
||||
(m) => m.id === DEFAULT_QWEN_MODEL,
|
||||
);
|
||||
expect(mockedSelect.mock.calls[1][0].initialIndex).toBe(expectedCoderIndex);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import {
|
|||
AuthType,
|
||||
ModelSlashCommandEvent,
|
||||
logModelSlashCommand,
|
||||
MAINLINE_CODER_MODEL,
|
||||
type AvailableModel as CoreAvailableModel,
|
||||
type ContentGeneratorConfig,
|
||||
type ContentGeneratorConfigSource,
|
||||
|
|
@ -22,7 +23,6 @@ import { DescriptiveRadioButtonSelect } from './shared/DescriptiveRadioButtonSel
|
|||
import { ConfigContext } from '../contexts/ConfigContext.js';
|
||||
import { UIStateContext, type UIState } from '../contexts/UIStateContext.js';
|
||||
import { useSettings } from '../contexts/SettingsContext.js';
|
||||
import { MAINLINE_CODER } from '../models/availableModels.js';
|
||||
import { getPersistScopeForModelSelection } from '../../config/modelProvidersScope.js';
|
||||
import { t } from '../../i18n/index.js';
|
||||
|
||||
|
|
@ -293,7 +293,7 @@ export function ModelDialog({ onClose }: ModelDialogProps): React.JSX.Element {
|
|||
[availableModelEntries],
|
||||
);
|
||||
|
||||
const preferredModelId = config?.getModel() || MAINLINE_CODER;
|
||||
const preferredModelId = config?.getModel() || MAINLINE_CODER_MODEL;
|
||||
// Check if current model is a runtime model
|
||||
// Runtime snapshot ID is already in $runtime|${authType}|${modelId} format
|
||||
const activeRuntimeSnapshot = config?.getActiveRuntimeModelSnapshot?.();
|
||||
|
|
|
|||
|
|
@ -1,184 +0,0 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { render } from 'ink-testing-library';
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { ModelSwitchDialog, VisionSwitchOutcome } from './ModelSwitchDialog.js';
|
||||
|
||||
// Mock the useKeypress hook
|
||||
const mockUseKeypress = vi.hoisted(() => vi.fn());
|
||||
vi.mock('../hooks/useKeypress.js', () => ({
|
||||
useKeypress: mockUseKeypress,
|
||||
}));
|
||||
|
||||
// Mock the RadioButtonSelect component
|
||||
const mockRadioButtonSelect = vi.hoisted(() => vi.fn());
|
||||
vi.mock('./shared/RadioButtonSelect.js', () => ({
|
||||
RadioButtonSelect: mockRadioButtonSelect,
|
||||
}));
|
||||
|
||||
describe('ModelSwitchDialog', () => {
|
||||
const mockOnSelect = vi.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
// Mock RadioButtonSelect to return a simple div
|
||||
mockRadioButtonSelect.mockReturnValue(
|
||||
React.createElement('div', { 'data-testid': 'radio-select' }),
|
||||
);
|
||||
});
|
||||
|
||||
it('should setup RadioButtonSelect with correct options', () => {
|
||||
render(<ModelSwitchDialog onSelect={mockOnSelect} />);
|
||||
|
||||
const expectedItems = [
|
||||
{
|
||||
key: 'switch-once',
|
||||
label: 'Switch for this request only',
|
||||
value: VisionSwitchOutcome.SwitchOnce,
|
||||
},
|
||||
{
|
||||
key: 'switch-session',
|
||||
label: 'Switch session to vision model',
|
||||
value: VisionSwitchOutcome.SwitchSessionToVL,
|
||||
},
|
||||
{
|
||||
key: 'continue',
|
||||
label: 'Continue with current model',
|
||||
value: VisionSwitchOutcome.ContinueWithCurrentModel,
|
||||
},
|
||||
];
|
||||
|
||||
const callArgs = mockRadioButtonSelect.mock.calls[0][0];
|
||||
expect(callArgs.items).toEqual(expectedItems);
|
||||
expect(callArgs.initialIndex).toBe(0);
|
||||
expect(callArgs.isFocused).toBe(true);
|
||||
});
|
||||
|
||||
it('should call onSelect when an option is selected', () => {
|
||||
render(<ModelSwitchDialog onSelect={mockOnSelect} />);
|
||||
|
||||
const callArgs = mockRadioButtonSelect.mock.calls[0][0];
|
||||
expect(typeof callArgs.onSelect).toBe('function');
|
||||
|
||||
// Simulate selection of "Switch for this request only"
|
||||
const onSelectCallback = mockRadioButtonSelect.mock.calls[0][0].onSelect;
|
||||
onSelectCallback(VisionSwitchOutcome.SwitchOnce);
|
||||
|
||||
expect(mockOnSelect).toHaveBeenCalledWith(VisionSwitchOutcome.SwitchOnce);
|
||||
});
|
||||
|
||||
it('should call onSelect with SwitchSessionToVL when second option is selected', () => {
|
||||
render(<ModelSwitchDialog onSelect={mockOnSelect} />);
|
||||
|
||||
const onSelectCallback = mockRadioButtonSelect.mock.calls[0][0].onSelect;
|
||||
onSelectCallback(VisionSwitchOutcome.SwitchSessionToVL);
|
||||
|
||||
expect(mockOnSelect).toHaveBeenCalledWith(
|
||||
VisionSwitchOutcome.SwitchSessionToVL,
|
||||
);
|
||||
});
|
||||
|
||||
it('should call onSelect with ContinueWithCurrentModel when third option is selected', () => {
|
||||
render(<ModelSwitchDialog onSelect={mockOnSelect} />);
|
||||
|
||||
const onSelectCallback = mockRadioButtonSelect.mock.calls[0][0].onSelect;
|
||||
onSelectCallback(VisionSwitchOutcome.ContinueWithCurrentModel);
|
||||
|
||||
expect(mockOnSelect).toHaveBeenCalledWith(
|
||||
VisionSwitchOutcome.ContinueWithCurrentModel,
|
||||
);
|
||||
});
|
||||
|
||||
it('should setup escape key handler to call onSelect with ContinueWithCurrentModel', () => {
|
||||
render(<ModelSwitchDialog onSelect={mockOnSelect} />);
|
||||
|
||||
expect(mockUseKeypress).toHaveBeenCalledWith(expect.any(Function), {
|
||||
isActive: true,
|
||||
});
|
||||
|
||||
// Simulate escape key press
|
||||
const keypressHandler = mockUseKeypress.mock.calls[0][0];
|
||||
keypressHandler({ name: 'escape' });
|
||||
|
||||
expect(mockOnSelect).toHaveBeenCalledWith(
|
||||
VisionSwitchOutcome.ContinueWithCurrentModel,
|
||||
);
|
||||
});
|
||||
|
||||
it('should not call onSelect for non-escape keys', () => {
|
||||
render(<ModelSwitchDialog onSelect={mockOnSelect} />);
|
||||
|
||||
const keypressHandler = mockUseKeypress.mock.calls[0][0];
|
||||
keypressHandler({ name: 'enter' });
|
||||
|
||||
expect(mockOnSelect).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should set initial index to 0 (first option)', () => {
|
||||
render(<ModelSwitchDialog onSelect={mockOnSelect} />);
|
||||
|
||||
const callArgs = mockRadioButtonSelect.mock.calls[0][0];
|
||||
expect(callArgs.initialIndex).toBe(0);
|
||||
});
|
||||
|
||||
describe('VisionSwitchOutcome enum', () => {
|
||||
it('should have correct enum values', () => {
|
||||
expect(VisionSwitchOutcome.SwitchOnce).toBe('once');
|
||||
expect(VisionSwitchOutcome.SwitchSessionToVL).toBe('session');
|
||||
expect(VisionSwitchOutcome.ContinueWithCurrentModel).toBe('persist');
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle multiple onSelect calls correctly', () => {
|
||||
render(<ModelSwitchDialog onSelect={mockOnSelect} />);
|
||||
|
||||
const onSelectCallback = mockRadioButtonSelect.mock.calls[0][0].onSelect;
|
||||
|
||||
// Call multiple times
|
||||
onSelectCallback(VisionSwitchOutcome.SwitchOnce);
|
||||
onSelectCallback(VisionSwitchOutcome.SwitchSessionToVL);
|
||||
onSelectCallback(VisionSwitchOutcome.ContinueWithCurrentModel);
|
||||
|
||||
expect(mockOnSelect).toHaveBeenCalledTimes(3);
|
||||
expect(mockOnSelect).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
VisionSwitchOutcome.SwitchOnce,
|
||||
);
|
||||
expect(mockOnSelect).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
VisionSwitchOutcome.SwitchSessionToVL,
|
||||
);
|
||||
expect(mockOnSelect).toHaveBeenNthCalledWith(
|
||||
3,
|
||||
VisionSwitchOutcome.ContinueWithCurrentModel,
|
||||
);
|
||||
});
|
||||
|
||||
it('should pass isFocused prop to RadioButtonSelect', () => {
|
||||
render(<ModelSwitchDialog onSelect={mockOnSelect} />);
|
||||
|
||||
const callArgs = mockRadioButtonSelect.mock.calls[0][0];
|
||||
expect(callArgs.isFocused).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle escape key multiple times', () => {
|
||||
render(<ModelSwitchDialog onSelect={mockOnSelect} />);
|
||||
|
||||
const keypressHandler = mockUseKeypress.mock.calls[0][0];
|
||||
|
||||
// Call escape multiple times
|
||||
keypressHandler({ name: 'escape' });
|
||||
keypressHandler({ name: 'escape' });
|
||||
|
||||
expect(mockOnSelect).toHaveBeenCalledTimes(2);
|
||||
expect(mockOnSelect).toHaveBeenCalledWith(
|
||||
VisionSwitchOutcome.ContinueWithCurrentModel,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,92 +0,0 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
import { Box, Text } from 'ink';
|
||||
import { Colors } from '../colors.js';
|
||||
import {
|
||||
RadioButtonSelect,
|
||||
type RadioSelectItem,
|
||||
} from './shared/RadioButtonSelect.js';
|
||||
import { useKeypress } from '../hooks/useKeypress.js';
|
||||
|
||||
export enum VisionSwitchOutcome {
|
||||
SwitchOnce = 'once',
|
||||
SwitchSessionToVL = 'session',
|
||||
ContinueWithCurrentModel = 'persist',
|
||||
}
|
||||
|
||||
export interface ModelSwitchDialogProps {
|
||||
onSelect: (outcome: VisionSwitchOutcome) => void;
|
||||
}
|
||||
|
||||
export const ModelSwitchDialog: React.FC<ModelSwitchDialogProps> = ({
|
||||
onSelect,
|
||||
}) => {
|
||||
useKeypress(
|
||||
(key) => {
|
||||
if (key.name === 'escape') {
|
||||
onSelect(VisionSwitchOutcome.ContinueWithCurrentModel);
|
||||
}
|
||||
},
|
||||
{ isActive: true },
|
||||
);
|
||||
|
||||
const options: Array<RadioSelectItem<VisionSwitchOutcome>> = [
|
||||
{
|
||||
key: 'switch-once',
|
||||
label: 'Switch for this request only',
|
||||
value: VisionSwitchOutcome.SwitchOnce,
|
||||
},
|
||||
{
|
||||
key: 'switch-session',
|
||||
label: 'Switch session to vision model',
|
||||
value: VisionSwitchOutcome.SwitchSessionToVL,
|
||||
},
|
||||
{
|
||||
key: 'continue',
|
||||
label: 'Continue with current model',
|
||||
value: VisionSwitchOutcome.ContinueWithCurrentModel,
|
||||
},
|
||||
];
|
||||
|
||||
const handleSelect = (outcome: VisionSwitchOutcome) => {
|
||||
onSelect(outcome);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
flexDirection="column"
|
||||
borderStyle="round"
|
||||
borderColor={Colors.AccentYellow}
|
||||
padding={1}
|
||||
width="100%"
|
||||
marginLeft={1}
|
||||
>
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
<Text bold>Vision Model Switch Required</Text>
|
||||
<Text>
|
||||
Your message contains an image, but the current model doesn't
|
||||
support vision.
|
||||
</Text>
|
||||
<Text>How would you like to proceed?</Text>
|
||||
</Box>
|
||||
|
||||
<Box marginBottom={1}>
|
||||
<RadioButtonSelect
|
||||
items={options}
|
||||
initialIndex={0}
|
||||
onSelect={handleSelect}
|
||||
isFocused
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Text color={Colors.Gray}>Press Enter to select, Esc to cancel</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue