diff --git a/docs/features/model-connections/progress.md b/docs/features/model-connections/progress.md index 8eb81c18..ab6f93b0 100644 --- a/docs/features/model-connections/progress.md +++ b/docs/features/model-connections/progress.md @@ -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-通道直连模型选择修复) diff --git a/docs/features/model-connections/ui-design.md b/docs/features/model-connections/ui-design.md index f79b333e..56fcb540 100644 --- a/docs/features/model-connections/ui-design.md +++ b/docs/features/model-connections/ui-design.md @@ -37,6 +37,13 @@ OpenCode 与 DeepSeek Harness 不使用该值。 - 文本域遵循标准键盘编辑、选择、复制和粘贴行为;保存继续使用设置页唯一的主保存操作。 - 焦点边框、字体、间距、颜色和深浅主题全部复用全局语义令牌。 +## 会话与项目 Runtime 选择 + +会话与项目选择器只提供直连模型和各 Runtime 入口,不再枚举每个 Runtime 与每个模型的 +组合;Runtime 模型在系统设置中配置。已保存的固定 Runtime 模型选择继续保留,项目 +选择器的选项与帮助文字均显示其固定模型,不能误报为跟随全局设置;用户可选择普通 +Runtime 入口恢复跟随。该展示规则不改变模型解析、远程 Agent 协议或执行权限。 + ## Harness 模型来源展示 DeepSeek Harness 的 `platform` 选项命名为“管理员环境优先,回退 GoodBuddy 兼容连接”, diff --git a/src/renderer/src/ProjectRuntimeSelector.test.tsx b/src/renderer/src/ProjectRuntimeSelector.test.tsx new file mode 100644 index 00000000..30ef109b --- /dev/null +++ b/src/renderer/src/ProjectRuntimeSelector.test.tsx @@ -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() + 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() + 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() + } + }) +}) diff --git a/src/renderer/src/ProjectRuntimeSelector.tsx b/src/renderer/src/ProjectRuntimeSelector.tsx index ddb92766..da67aac3 100644 --- a/src/renderer/src/ProjectRuntimeSelector.tsx +++ b/src/renderer/src/ProjectRuntimeSelector.tsx @@ -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 }) diff --git a/src/renderer/src/i18n/locales/en-US/integrations.ts b/src/renderer/src/i18n/locales/en-US/integrations.ts index cbad87e2..1a12423e 100644 --- a/src/renderer/src/i18n/locales/en-US/integrations.ts +++ b/src/renderer/src/i18n/locales/en-US/integrations.ts @@ -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: diff --git a/src/renderer/src/i18n/locales/zh-CN/integrations.ts b/src/renderer/src/i18n/locales/zh-CN/integrations.ts index b82d1e34..e2bb2149 100644 --- a/src/renderer/src/i18n/locales/zh-CN/integrations.ts +++ b/src/renderer/src/i18n/locales/zh-CN/integrations.ts @@ -40,6 +40,7 @@ export const integrations = { '直接使用 {{name}}({{modelName}})处理消息。', automaticDescription: '跟随全局 Runtime 默认设置', defaultDirect: '直连 · 跟随全局默认模型', + fixedRuntimeDescription: '通过 {{runtime}} 运行,固定使用 {{name}}。', runtimeDescription: '通过 {{runtime}} Agent Runtime 运行,并跟随“Agent Runtime”设置中的全局 {{runtime}} 配置。', defaultMode: '默认模式',