fix: simplify runtime model choices in conversations and projects
Remove redundant provider and model combinations from conversation and project Runtime selectors. Runtime model configuration stays in system settings, while saved fixed project selections remain visible for compatibility. Release note: Conversation and project Runtime menus are simpler; change Runtime models in system settings.
This commit is contained in:
@@ -6351,7 +6351,7 @@ describe("App", () => {
|
||||
name: /^OpenCode · 默认模型.*sonnet-5$/u,
|
||||
}),
|
||||
).toBeInTheDocument();
|
||||
expect(within(runtimeMenu).getAllByRole("menuitemradio")).toHaveLength(2);
|
||||
expect(within(runtimeMenu).getAllByRole("menuitemradio")).toHaveLength(1);
|
||||
expect(within(runtimeMenu).queryByText("直连模型")).not.toBeInTheDocument();
|
||||
expect(
|
||||
within(runtimeMenu).queryByText("Continue Runtime"),
|
||||
@@ -8539,7 +8539,7 @@ describe("App", () => {
|
||||
expect(openCodeModel).toBeEnabled();
|
||||
expect(continueModel).toBeEnabled();
|
||||
expect(deepseekHarness).toBeEnabled();
|
||||
expect(screen.getAllByRole("menuitemradio")).toHaveLength(10);
|
||||
expect(screen.getAllByRole("menuitemradio")).toHaveLength(6);
|
||||
expect(within(runtimeMenu).getAllByRole("separator")).toHaveLength(4);
|
||||
expect(within(runtimeMenu).queryByRole("menu")).not.toBeInTheDocument();
|
||||
expect(
|
||||
|
||||
@@ -76,8 +76,6 @@ import type {
|
||||
} from "../../shared/contracts";
|
||||
import {
|
||||
defaultContextCompressionSettings,
|
||||
isAgentRuntimeModelProtocol,
|
||||
isDeepSeekHarnessModelProfile,
|
||||
maximumPastedImageBytes,
|
||||
} from "../../shared/contracts";
|
||||
import {
|
||||
@@ -9802,26 +9800,6 @@ function App(): React.JSX.Element {
|
||||
>
|
||||
<span>{t("runtime.manage")}</span>
|
||||
</button>
|
||||
{runtimeSettings && (
|
||||
<>
|
||||
<strong role="presentation">{t("runtime.fixedRuntimeModel")}</strong>
|
||||
{(["opencode", "continue", "deepseek-harness"] as const).flatMap((provider) =>
|
||||
runtimeSettings.modelProfiles.filter((profile) =>
|
||||
(!activeProjectUsesManagedSsh || provider === "opencode") &&
|
||||
(provider === "deepseek-harness" ? isDeepSeekHarnessModelProfile(profile) : isAgentRuntimeModelProtocol(profile.protocol))
|
||||
).map((profile) => {
|
||||
const selection = { provider, profileId: profile.id };
|
||||
const selected = activeRuntimeSelectionKey === agentRuntimeSelectionKey(selection);
|
||||
return <button key={`${provider}:${profile.id}`} type="button" role="menuitemradio"
|
||||
aria-checked={selected} tabIndex={selected ? 0 : -1}
|
||||
onClick={() => void switchRuntime(selection)}>
|
||||
<span>{t("runtime.fixedModel")} · {provider === "opencode" ? "OpenCode" : provider === "continue" ? "Continue" : "DeepSeek Harness"} · {profile.name}</span>
|
||||
<small>{profile.modelName}</small>
|
||||
</button>;
|
||||
})
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
import { cleanup, fireEvent, render, screen } from '@testing-library/react'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { defaultRuntimeSettings, type RuntimeSettings } from '../../shared/contracts'
|
||||
import { ProjectRuntimeSelector } from './ProjectRuntimeSelector'
|
||||
import i18n from './i18n'
|
||||
|
||||
afterEach(cleanup)
|
||||
|
||||
describe('ProjectRuntimeSelector rules', () => {
|
||||
it('allows fixed Runtime models and switching back to a profile-free reference', async () => {
|
||||
await i18n.changeLanguage('zh-CN')
|
||||
const profileId = '00000000-0000-4000-8000-000000000011'
|
||||
const settings = {
|
||||
...defaultRuntimeSettings,
|
||||
defaultModelProfileId: profileId,
|
||||
modelProfiles: [{
|
||||
id: profileId, name: 'Model A', modelName: 'model-a',
|
||||
baseUrl: 'https://example.com/v1', protocol: 'openai-chat-completions',
|
||||
authentication: 'api-key', apiKeyConfigured: true
|
||||
}]
|
||||
} as RuntimeSettings
|
||||
const onChange = vi.fn()
|
||||
const props = {
|
||||
ariaLabel: 'Project Runtime', label: 'Runtime', onChange,
|
||||
runtimeSettings: settings
|
||||
}
|
||||
const view = render(<ProjectRuntimeSelector {...props} selection={{ provider: 'opencode' }} />)
|
||||
const select = screen.getByRole('combobox')
|
||||
for (const provider of ['opencode', 'continue', 'deepseek-harness'] as const) {
|
||||
expect(screen.getByRole('option', {
|
||||
name: `${provider === 'opencode' ? 'OpenCode' : provider === 'continue' ? 'Continue' : 'DeepSeek Harness'} · Model A`
|
||||
})).toHaveValue(`${provider}:${profileId}`)
|
||||
fireEvent.change(select, { target: { value: `${provider}:${profileId}` } })
|
||||
expect(onChange).toHaveBeenLastCalledWith({ provider, profileId })
|
||||
}
|
||||
view.rerender(<ProjectRuntimeSelector {...props} selection={{ provider: 'opencode', profileId }} />)
|
||||
expect(screen.getByText('通过 OpenCode 运行,固定使用 Model A。')).toBeInTheDocument()
|
||||
fireEvent.change(select, { target: { value: 'opencode:default' } })
|
||||
expect(onChange).toHaveBeenLastCalledWith({ provider: 'opencode' })
|
||||
fireEvent.change(select, { target: { value: 'model:default' } })
|
||||
expect(onChange).toHaveBeenLastCalledWith({ provider: 'model' })
|
||||
})
|
||||
})
|
||||
@@ -1,10 +1,6 @@
|
||||
import type { TFunction } from 'i18next'
|
||||
import type { TFunction } from 'i18next'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
isAgentRuntimeModelProtocol,
|
||||
isDeepSeekHarnessModelProfile,
|
||||
type RuntimeSettings
|
||||
} from '../../shared/contracts'
|
||||
import type { RuntimeSettings } from '../../shared/contracts'
|
||||
import {
|
||||
agentRuntimeSelectionKey,
|
||||
getRuntimeSelectionForProvider,
|
||||
@@ -58,15 +54,6 @@ function runtimeSelectionDescription(
|
||||
: selection.provider === 'continue'
|
||||
? 'Continue'
|
||||
: 'DeepSeek Harness'
|
||||
if (selection.profileId) {
|
||||
const profile = settings.modelProfiles.find(
|
||||
(candidate) => candidate.id === selection.profileId
|
||||
)
|
||||
return t('channels.project.fixedRuntimeDescription', {
|
||||
runtime: runtimeLabel,
|
||||
name: profile?.name ?? t('channels.project.missingProfile')
|
||||
})
|
||||
}
|
||||
return t('channels.project.runtimeDescription', {
|
||||
runtime: runtimeLabel
|
||||
})
|
||||
@@ -121,22 +108,9 @@ export function ProjectRuntimeSelector({
|
||||
...(selectionMode === 'configured' ? [{ provider: 'auto' as const }] : []),
|
||||
{ provider: 'model' }
|
||||
]
|
||||
const fixedRuntimeSelections = selectionMode === 'configured'
|
||||
? runtimeProviders.flatMap((provider) =>
|
||||
directProfiles.filter((profile) =>
|
||||
provider === 'deepseek-harness'
|
||||
? isDeepSeekHarnessModelProfile(profile)
|
||||
: isAgentRuntimeModelProtocol(profile.protocol)
|
||||
).map((profile) => ({ provider, profileId: profile.id }))
|
||||
)
|
||||
: []
|
||||
const fixedRuntimeSelection = runtimeSelection.provider !== 'model' &&
|
||||
'profileId' in runtimeSelection && runtimeSelection.profileId &&
|
||||
!fixedRuntimeSelections.some((candidate) =>
|
||||
agentRuntimeSelectionKey(candidate) === agentRuntimeSelectionKey(runtimeSelection)
|
||||
) ? runtimeSelection : undefined
|
||||
'profileId' in runtimeSelection && runtimeSelection.profileId ? runtimeSelection : undefined
|
||||
const selections = [...inheritedSelections, ...directSelections, ...runtimeSelections,
|
||||
...fixedRuntimeSelections,
|
||||
...(fixedRuntimeSelection ? [fixedRuntimeSelection] : [])]
|
||||
const selectionByKey = new Map(
|
||||
selections.map((candidate) => [
|
||||
@@ -216,19 +190,6 @@ export function ProjectRuntimeSelector({
|
||||
</option>
|
||||
))}
|
||||
</optgroup>
|
||||
{fixedRuntimeSelections.length > 0 && (
|
||||
<optgroup label={t('channels.project.fixedRuntimeModels')}>
|
||||
{fixedRuntimeSelections.map((candidate) => (
|
||||
<option
|
||||
key={agentRuntimeSelectionKey(candidate)}
|
||||
value={agentRuntimeSelectionKey(candidate)}
|
||||
>
|
||||
{candidate.provider === 'opencode' ? 'OpenCode' : candidate.provider === 'continue' ? 'Continue' : 'DeepSeek Harness'}
|
||||
{' · '}{runtimeSettings.modelProfiles.find((profile) => profile.id === candidate.profileId)?.name}
|
||||
</option>
|
||||
))}
|
||||
</optgroup>
|
||||
)}
|
||||
</select>
|
||||
<small>
|
||||
{runtimeSelectionDescription(
|
||||
|
||||
@@ -210,8 +210,6 @@ export const app = {
|
||||
picker: 'Runtime and model',
|
||||
directModels: 'Direct models',
|
||||
defaultDirect: 'Direct · Follow the global default model',
|
||||
fixedRuntimeModel: 'Fixed Runtime model',
|
||||
fixedModel: 'Fixed',
|
||||
deepseekHarnessGroup:
|
||||
'DeepSeek Harness (Developer preview · OpenAI-compatible)',
|
||||
manage: 'Manage Runtime and model connections',
|
||||
|
||||
@@ -46,8 +46,6 @@ export const integrations = {
|
||||
directDescription:
|
||||
'Process messages directly with {{name}} ({{modelName}}).',
|
||||
defaultDirect: 'Direct · Follow the global default model',
|
||||
fixedRuntimeModels: 'Agent Runtime · Fixed model',
|
||||
fixedRuntimeDescription: 'Run through {{runtime}} with the fixed model {{name}}.',
|
||||
automaticDescription:
|
||||
'Follow the global default Runtime setting.',
|
||||
runtimeDescription:
|
||||
|
||||
@@ -197,8 +197,6 @@ export const app = {
|
||||
picker: 'Runtime 和模型',
|
||||
directModels: '直连模型',
|
||||
defaultDirect: '直连 · 跟随全局默认模型',
|
||||
fixedRuntimeModel: '固定 Runtime 模型',
|
||||
fixedModel: '固定',
|
||||
deepseekHarnessGroup:
|
||||
'DeepSeek Harness(开发者预览 · OpenAI 兼容)',
|
||||
manage: '管理 Runtime 和模型连接',
|
||||
|
||||
@@ -40,8 +40,6 @@ export const integrations = {
|
||||
'直接使用 {{name}}({{modelName}})处理消息。',
|
||||
automaticDescription: '跟随全局 Runtime 默认设置',
|
||||
defaultDirect: '直连 · 跟随全局默认模型',
|
||||
fixedRuntimeModels: 'Agent Runtime · 固定模型',
|
||||
fixedRuntimeDescription: '通过 {{runtime}} 运行,固定使用 {{name}}。',
|
||||
runtimeDescription:
|
||||
'通过 {{runtime}} Agent Runtime 运行,并跟随“Agent Runtime”设置中的全局 {{runtime}} 配置。',
|
||||
defaultMode: '默认模式',
|
||||
|
||||
Reference in New Issue
Block a user