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, name: /^OpenCode · 默认模型.*sonnet-5$/u,
}), }),
).toBeInTheDocument(); ).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("直连模型")).not.toBeInTheDocument();
expect( expect(
within(runtimeMenu).queryByText("Continue Runtime"), within(runtimeMenu).queryByText("Continue Runtime"),
@@ -8539,7 +8539,7 @@ describe("App", () => {
expect(openCodeModel).toBeEnabled(); expect(openCodeModel).toBeEnabled();
expect(continueModel).toBeEnabled(); expect(continueModel).toBeEnabled();
expect(deepseekHarness).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).getAllByRole("separator")).toHaveLength(4);
expect(within(runtimeMenu).queryByRole("menu")).not.toBeInTheDocument(); expect(within(runtimeMenu).queryByRole("menu")).not.toBeInTheDocument();
expect( expect(
-22
View File
@@ -76,8 +76,6 @@ import type {
} from "../../shared/contracts"; } from "../../shared/contracts";
import { import {
defaultContextCompressionSettings, defaultContextCompressionSettings,
isAgentRuntimeModelProtocol,
isDeepSeekHarnessModelProfile,
maximumPastedImageBytes, maximumPastedImageBytes,
} from "../../shared/contracts"; } from "../../shared/contracts";
import { import {
@@ -9802,26 +9800,6 @@ function App(): React.JSX.Element {
> >
<span>{t("runtime.manage")}</span> <span>{t("runtime.manage")}</span>
</button> </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>
)} )}
</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 { useTranslation } from 'react-i18next'
import { import type { RuntimeSettings } from '../../shared/contracts'
isAgentRuntimeModelProtocol,
isDeepSeekHarnessModelProfile,
type RuntimeSettings
} from '../../shared/contracts'
import { import {
agentRuntimeSelectionKey, agentRuntimeSelectionKey,
getRuntimeSelectionForProvider, getRuntimeSelectionForProvider,
@@ -58,15 +54,6 @@ function runtimeSelectionDescription(
: selection.provider === 'continue' : selection.provider === 'continue'
? 'Continue' ? 'Continue'
: 'DeepSeek Harness' : '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', { return t('channels.project.runtimeDescription', {
runtime: runtimeLabel runtime: runtimeLabel
}) })
@@ -121,22 +108,9 @@ export function ProjectRuntimeSelector({
...(selectionMode === 'configured' ? [{ provider: 'auto' as const }] : []), ...(selectionMode === 'configured' ? [{ provider: 'auto' as const }] : []),
{ provider: 'model' } { 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' && const fixedRuntimeSelection = runtimeSelection.provider !== 'model' &&
'profileId' in runtimeSelection && runtimeSelection.profileId && 'profileId' in runtimeSelection && runtimeSelection.profileId ? runtimeSelection : undefined
!fixedRuntimeSelections.some((candidate) =>
agentRuntimeSelectionKey(candidate) === agentRuntimeSelectionKey(runtimeSelection)
) ? runtimeSelection : undefined
const selections = [...inheritedSelections, ...directSelections, ...runtimeSelections, const selections = [...inheritedSelections, ...directSelections, ...runtimeSelections,
...fixedRuntimeSelections,
...(fixedRuntimeSelection ? [fixedRuntimeSelection] : [])] ...(fixedRuntimeSelection ? [fixedRuntimeSelection] : [])]
const selectionByKey = new Map( const selectionByKey = new Map(
selections.map((candidate) => [ selections.map((candidate) => [
@@ -216,19 +190,6 @@ export function ProjectRuntimeSelector({
</option> </option>
))} ))}
</optgroup> </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> </select>
<small> <small>
{runtimeSelectionDescription( {runtimeSelectionDescription(
@@ -210,8 +210,6 @@ export const app = {
picker: 'Runtime and model', picker: 'Runtime and model',
directModels: 'Direct models', directModels: 'Direct models',
defaultDirect: 'Direct · Follow the global default model', defaultDirect: 'Direct · Follow the global default model',
fixedRuntimeModel: 'Fixed Runtime model',
fixedModel: 'Fixed',
deepseekHarnessGroup: deepseekHarnessGroup:
'DeepSeek Harness (Developer preview · OpenAI-compatible)', 'DeepSeek Harness (Developer preview · OpenAI-compatible)',
manage: 'Manage Runtime and model connections', manage: 'Manage Runtime and model connections',
@@ -46,8 +46,6 @@ export const integrations = {
directDescription: directDescription:
'Process messages directly with {{name}} ({{modelName}}).', 'Process messages directly with {{name}} ({{modelName}}).',
defaultDirect: 'Direct · Follow the global default model', defaultDirect: 'Direct · Follow the global default model',
fixedRuntimeModels: 'Agent Runtime · Fixed model',
fixedRuntimeDescription: 'Run through {{runtime}} with the fixed model {{name}}.',
automaticDescription: automaticDescription:
'Follow the global default Runtime setting.', 'Follow the global default Runtime setting.',
runtimeDescription: runtimeDescription:
@@ -197,8 +197,6 @@ export const app = {
picker: 'Runtime 和模型', picker: 'Runtime 和模型',
directModels: '直连模型', directModels: '直连模型',
defaultDirect: '直连 · 跟随全局默认模型', defaultDirect: '直连 · 跟随全局默认模型',
fixedRuntimeModel: '固定 Runtime 模型',
fixedModel: '固定',
deepseekHarnessGroup: deepseekHarnessGroup:
'DeepSeek Harness(开发者预览 · OpenAI 兼容)', 'DeepSeek Harness(开发者预览 · OpenAI 兼容)',
manage: '管理 Runtime 和模型连接', manage: '管理 Runtime 和模型连接',
@@ -40,8 +40,6 @@ export const integrations = {
'直接使用 {{name}}({{modelName}})处理消息。', '直接使用 {{name}}({{modelName}})处理消息。',
automaticDescription: '跟随全局 Runtime 默认设置', automaticDescription: '跟随全局 Runtime 默认设置',
defaultDirect: '直连 · 跟随全局默认模型', defaultDirect: '直连 · 跟随全局默认模型',
fixedRuntimeModels: 'Agent Runtime · 固定模型',
fixedRuntimeDescription: '通过 {{runtime}} 运行,固定使用 {{name}}。',
runtimeDescription: runtimeDescription:
'通过 {{runtime}} Agent Runtime 运行,并跟随“Agent Runtime”设置中的全局 {{runtime}} 配置。', '通过 {{runtime}} Agent Runtime 运行,并跟随“Agent Runtime”设置中的全局 {{runtime}} 配置。',
defaultMode: '默认模式', defaultMode: '默认模式',