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:
mesalogo
2026-09-10 23:00:01 +08:00
parent c0ad27ab44
commit 1274c762e8
8 changed files with 5 additions and 117 deletions
+2 -2
View File
@@ -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(
-22
View File
@@ -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' })
})
})
+3 -42
View File
@@ -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: '默认模式',