fix: describe saved fixed project models accurately

Projects with a saved fixed Runtime model could incorrectly claim to follow
the global Runtime configuration. Restore the fixed-model description while
keeping the simplified menu and the ability to return to inherited settings.
Model resolution, permissions, and remote Agent behavior are unchanged.

Validation: 3911 tests passed, 66 skipped; typecheck, lint, and release-note
validation passed. Added bilingual regressions for all three Agent Runtimes.

Release note: 项目已固定 Runtime 模型时,现在明确显示所用固定模型,不再误报为跟随全局设置。
This commit is contained in:
mesalogo
2026-09-11 01:21:24 +08:00
parent 073eb0d2f5
commit b2c85b8ece
6 changed files with 81 additions and 0 deletions
@@ -1,5 +1,17 @@
# 模型连接请求定制进度
## 2026-09-11:发布前固定模型说明修复
- 项目已保存固定 Runtime 模型时,恢复显示实际固定模型的帮助文字,保留简化后的菜单;
规则见 [会话与项目 Runtime 选择](./ui-design.md#会话与项目-runtime-选择)。
- `npm exec vitest run src/renderer/src/ProjectRuntimeSelector.test.tsx`:2 项通过,
覆盖中英文下 OpenCode、Continue、DeepSeek Harness 的固定选项及恢复跟随操作。
- `npm test`:343 个文件、3,911 项通过,66 项按平台或手工/真实服务条件跳过;
`npm run typecheck`、`npm run lint`、`npm run release:notes:verify` 通过。
- 本次新增修复仅影响 Renderer 帮助文字,未修改共享模型解析、Desktop-to-Agent 请求、
Agent 或 Runtime 启动实现;不需要单独远程修复。真实模型调用 0 次,未运行真实 Host
或桌面端到端模型会话。本地未构建或打包,发布候选生产构建由主分支 CI 验证。
## 2026-09-10:配置准确性修复
- 通道直连回退修复已按[技术设计第 8 节](./technical-design.md#8-通道直连模型选择修复)
@@ -37,6 +37,13 @@ OpenCode 与 DeepSeek Harness 不使用该值。
- 文本域遵循标准键盘编辑、选择、复制和粘贴行为;保存继续使用设置页唯一的主保存操作。
- 焦点边框、字体、间距、颜色和深浅主题全部复用全局语义令牌。
## 会话与项目 Runtime 选择
会话与项目选择器只提供直连模型和各 Runtime 入口,不再枚举每个 Runtime 与每个模型的
组合;Runtime 模型在系统设置中配置。已保存的固定 Runtime 模型选择继续保留,项目
选择器的选项与帮助文字均显示其固定模型,不能误报为跟随全局设置;用户可选择普通
Runtime 入口恢复跟随。该展示规则不改变模型解析、远程 Agent 协议或执行权限。
## Harness 模型来源展示
DeepSeek Harness 的 `platform` 选项命名为“管理员环境优先,回退 GoodBuddy 兼容连接”,
@@ -0,0 +1,51 @@
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 saved model references', () => {
it.each(['zh-CN', 'en-US'])('describes fixed selections accurately in %s without restoring duplicate menus', async (language) => {
await i18n.changeLanguage(language)
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()
for (const provider of ['opencode', 'continue', 'deepseek-harness'] as const) {
const runtime = provider === 'opencode' ? 'OpenCode'
: provider === 'continue' ? 'Continue' : 'DeepSeek Harness'
const props = {
ariaLabel: 'Project Runtime', label: 'Runtime', onChange,
runtimeSettings: settings
}
const view = render(<ProjectRuntimeSelector {...props} selection={{ provider, profileId }} />)
const select = screen.getByRole('combobox')
expect(select).toHaveValue(`${provider}:${profileId}`)
expect(screen.getByText(language === 'zh-CN'
? `通过 ${runtime} 运行,固定使用 Model A。`
: `Run through ${runtime} with the fixed model Model A.`)).toBeInTheDocument()
expect(screen.getAllByRole('option').filter((option) =>
(option as HTMLOptionElement).value.endsWith(`:${profileId}`))).toHaveLength(2)
fireEvent.change(select, { target: { value: `${provider}:default` } })
expect(onChange).toHaveBeenLastCalledWith({ provider })
view.rerender(<ProjectRuntimeSelector {...props} selection={{ provider }} />)
expect(select).toHaveValue(`${provider}:default`)
expect(screen.queryByText(language === 'zh-CN'
? `通过 ${runtime} 运行,固定使用 Model A。`
: `Run through ${runtime} with the fixed model Model A.`)).not.toBeInTheDocument()
expect(screen.getAllByRole('option').filter((option) =>
(option as HTMLOptionElement).value.endsWith(`:${profileId}`))).toHaveLength(1)
view.unmount()
}
})
})
@@ -54,6 +54,15 @@ 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
})
@@ -46,6 +46,7 @@ export const integrations = {
directDescription:
'Process messages directly with {{name}} ({{modelName}}).',
defaultDirect: 'Direct · Follow the global default model',
fixedRuntimeDescription: 'Run through {{runtime}} with the fixed model {{name}}.',
automaticDescription:
'Follow the global default Runtime setting.',
runtimeDescription:
@@ -40,6 +40,7 @@ export const integrations = {
'直接使用 {{name}}({{modelName}})处理消息。',
automaticDescription: '跟随全局 Runtime 默认设置',
defaultDirect: '直连 · 跟随全局默认模型',
fixedRuntimeDescription: '通过 {{runtime}} 运行,固定使用 {{name}}。',
runtimeDescription:
'通过 {{runtime}} Agent Runtime 运行,并跟随“Agent Runtime”设置中的全局 {{runtime}} 配置。',
defaultMode: '默认模式',