fix: preserve runtime configuration references
GoodBuddy Agents / Validate Agent source (push) Canceled after 0s
GoodBuddy Agents / Build darwin arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux x64 Agent (push) Canceled after 0s
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
GoodBuddy Agents / Validate Agent source (push) Canceled after 0s
GoodBuddy Agents / Build darwin arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux x64 Agent (push) Canceled after 0s
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
Projects, channels, conversations, and Agent Runtimes now retain dynamic default references while explicit model selections remain pinned. Compatible fallback resolution stays aligned across settings validation, runtime creation, labels, context metrics, DeepSeek Harness browser routing, and remote project saves. Prepare the bilingual Desktop 0.12.11 release metadata and keep Agent 0.11.22 as the published remote baseline. Release note: 修复默认模型与 Runtime 配置变化后项目、通道和对话选择失去跟随关系的问题;显式选择仍保持固定,新请求使用当前兼容配置。
This commit is contained in:
+2
-2
@@ -21,7 +21,7 @@ otherwise.
|
||||
packages target compatible x64 and arm64 environments, including UOS, Kylin,
|
||||
Hygon, Zhaoxin, Kunpeng, and Phytium systems. This is not vendor certification.
|
||||
LoongArch has a separate experimental loong64 preview outside standard
|
||||
releases and automatic updates; no preview is planned for 0.12.10. See the
|
||||
releases and automatic updates; no preview is planned for 0.12.11. See the
|
||||
[preview boundaries](./docs/development/loongarch-preview-build.md).
|
||||
- [x] **Configurable global shortcut**: Enable, disable, or record an Electron
|
||||
accelerator under Platform Features / General. The default remains
|
||||
@@ -187,7 +187,7 @@ otherwise.
|
||||
failure, Agent `SIGKILL`/restart, and recovery from a reopened Desktop SQLite
|
||||
database. Successful tool START/END events appear exactly once, with no
|
||||
Prompt, provider, or tool replay observed. The current Agent source lock is
|
||||
`0.11.22`, while the current Desktop release candidate is `0.12.10`; formal
|
||||
`0.11.22`, while the current Desktop release candidate is `0.12.11`; formal
|
||||
publication status follows the separate Agent and Desktop
|
||||
release channels. Current macOS source has passed native package installation,
|
||||
detached lifecycle, Attach, real Ask/Execute, and cancellation of tools in
|
||||
|
||||
+2
-2
@@ -14,7 +14,7 @@
|
||||
### 桌面基础、工作空间与上下文
|
||||
|
||||
- [x] **跨平台桌面应用**:支持 Windows、macOS、Linux,以及 `x64`、`arm64` 发布目标。
|
||||
- [x] **信创软硬件覆盖**:正式 Linux 安装包覆盖国产 `x64` 与 `arm64` 架构,可用于统信 UOS、银河麒麟及海光、兆芯、鲲鹏、飞腾等兼容环境。龙芯 LoongArch 另行提供 `loong64` 实验预览版,不属于正式发布矩阵,也未纳入自动更新;`0.12.10` 不构建该预览。这里的覆盖范围不等同于厂商或整机兼容认证,具体状态与限制见[龙芯预览版构建说明](./docs/development/loongarch-preview-build.md)。
|
||||
- [x] **信创软硬件覆盖**:正式 Linux 安装包覆盖国产 `x64` 与 `arm64` 架构,可用于统信 UOS、银河麒麟及海光、兆芯、鲲鹏、飞腾等兼容环境。龙芯 LoongArch 另行提供 `loong64` 实验预览版,不属于正式发布矩阵,也未纳入自动更新;`0.12.11` 不构建该预览。这里的覆盖范围不等同于厂商或整机兼容认证,具体状态与限制见[龙芯预览版构建说明](./docs/development/loongarch-preview-build.md)。
|
||||
- [x] **可配置全局快捷唤起**:在“平台功能 / 通用设置”中启停或录制 Electron accelerator;默认保留 `CommandOrControl+Shift+Space`,冲突或保存失败时继续使用上一组已注册快捷键,并显示可处理的状态。
|
||||
- [x] **Projects、独立对话与会话分支**:按项目隔离上下文,管理会话、附件和 Git 工作区变更;本地会话可在稳定状态下复制当前聊天内容到独立分支,分支持续显示来源徽标且不复制 Task、队列或成果归属;项目选择器区分本地、托管 SSH 与远程消息通道项目,托管 SSH 项目按 Host 分组并在 Host 标题显示真实 Agent 连接状态,项目行只保留远端路径。
|
||||
- [x] **文件、截图、窗口、剪贴板上下文**:用户明确选择后才加入模型上下文。
|
||||
@@ -107,7 +107,7 @@
|
||||
|
||||
### 开源、构建与发布
|
||||
|
||||
- 当前源码候选为 Desktop `0.12.10`、Agent `0.11.22`,OpenCode 固定为 `1.18.29`;正式发布状态以 Desktop 与 Agent 的独立发布渠道为准。
|
||||
- 当前源码候选为 Desktop `0.12.11`、Agent `0.11.22`,OpenCode 固定为 `1.18.29`;正式发布状态以 Desktop 与 Agent 的独立发布渠道为准。
|
||||
- [x] **0BSD 开源许可**:原创代码可自由使用、复制、修改、分发和商用;第三方组件和资源仍遵循各自许可证。
|
||||
- [x] **可复现依赖安装与源码构建**:使用锁定依赖、Node.js 24 和统一的测试、类型检查、Lint、生产构建命令。
|
||||
- [x] **六平台原生发布矩阵**:Windows、macOS、Linux 的 `x64`、`arm64` 目标由原生 Runner 构建,并提供发布清单和 SHA-256 哈希。Windows 提供 NSIS 与 portable ZIP,macOS 仅 DMG,Linux 提供 AppImage、DEB 和 RPM,共 12 个安装包、20 个发布资产。
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
## 状态
|
||||
|
||||
本文记录截至 2026-09-09 的当前代码实现,不定义额外的信任框架。“新增 Host 只探测、Host 卡片手动准备
|
||||
本文记录截至 2026-09-10 的当前代码实现,不定义额外的信任框架。“新增 Host 只探测、Host 卡片手动准备
|
||||
Agent/Runtime、Host 直接从 GitHub/北京镜像下载、项目始终使用 Host current 环境”已经完成源码接线,
|
||||
详细事务与验收边界见
|
||||
[SSH Host 远程环境准备与直连下载设计](./environment-provisioning-technical-design.md);
|
||||
@@ -12,7 +12,7 @@ Linux x64/arm64、取消和离线 GoodBuddy 传输的真实 Host 验收。
|
||||
Windows 到 Linux x64 的安装、Agent-owned Prompt、Agent 本地模型 gateway、断线恢复、
|
||||
同一 OpenCode Session 续接、取消和终态清理已经使用真实模型与工具验证。Agent
|
||||
`0.11.14` 已通过独立 workflow 发布 Linux x64/arm64 复合包和签名累计目录;当前源码
|
||||
候选为 Agent `0.11.22`、Desktop `0.12.10`,包含托管 OpenCode 自动快照策略修复。
|
||||
候选为 Agent `0.11.22`、Desktop `0.12.11`,包含托管 OpenCode 自动快照策略修复。
|
||||
正式发布状态以 Agent 与 Desktop 独立发布渠道为准。
|
||||
现有源码显示本地与远端 OpenCode 原生 Task,并取消 GoodBuddy 对生产 Prompt 的
|
||||
固定墙钟总时限。失败的 `agent-v0.11.3` 保持不可变且未发布。
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "goodbuddy",
|
||||
"version": "0.12.10",
|
||||
"version": "0.12.11",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "goodbuddy",
|
||||
"version": "0.12.10",
|
||||
"version": "0.12.11",
|
||||
"license": "0BSD",
|
||||
"dependencies": {
|
||||
"@agentclientprotocol/sdk": "0.25.1",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "goodbuddy",
|
||||
"version": "0.12.10",
|
||||
"version": "0.12.11",
|
||||
"private": true,
|
||||
"description": "Secure desktop AI workspace with controlled Agent Runtimes",
|
||||
"desktopName": "GoodBuddy",
|
||||
|
||||
@@ -1,6 +1,50 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"releases": [
|
||||
{
|
||||
"version": "0.12.11",
|
||||
"releasedAt": "2026-09-10",
|
||||
"notes": {
|
||||
"zh-CN": {
|
||||
"highlights": [],
|
||||
"features": [
|
||||
"**工作栏直接管理项目文件与 Git**:可在右侧工作区创建、重命名、移动和删除文件或目录,并查看属性。Git 工具新增分支搜索、新建与切换、手动 Fetch、提交历史和单文件差异,返回文件预览后仍保留目录展开、滚动位置和焦点。",
|
||||
"**多个独立浏览器页签**:可在工作栏中同时打开多个浏览器,每个页签分别保留页面、导航和操作状态,并支持直接交互和全屏显示。同一对话的页签共享登录状态,模型浏览器工具固定使用请求开始时绑定的页签,不会因用户切换页签而改投其他页面。",
|
||||
"**直连模型获得完整开发工具**:安装包随附 ripgrep,直连模型可搜索项目文件和内容、分页读取大型 UTF-8 文本,并通过多文件补丁修改代码。Ask 可搜索和读取,Execute 还可修改文件和运行命令。",
|
||||
"**长任务保留更多上下文**:移除历史消息、长回复和文档解析文本的固定截断限制,并允许模型分页续读较长的命令与 Subagent 输出。浏览器快照和知识库单次可选结果范围也已扩大,实际可用上下文仍受所选模型窗口限制。",
|
||||
"**任务中心可切换范围**:任务中心默认跟随当前项目,也可切换到全局任务或全部项目。所选范围会随工作栏布局保存,切换项目时不必重复调整。"
|
||||
],
|
||||
"fixes": [
|
||||
"**计划任务状态及时同步**:排队和运行中的计划任务会及时刷新状态。当前 occurrence 完成前“立即运行”保持禁用,避免同一计划重复入队。",
|
||||
"**Runtime 与模型选择保持原意**:未固定连接的项目、通道、对话和 Agent Runtime 会继续跟随当前兼容的全局推荐连接,显式选择的连接保持固定;远程项目保存后也会保留用户选择的 Runtime。切换全局默认连接后,新请求使用新配置,已经开始的请求不受影响。",
|
||||
"**远程 Runtime 元数据兼容**:有效的 Runtime 锁定 JSON 不再仅因空白或缩进格式不同而被误判为无效。签名、Schema、版本和完整性校验保持不变。"
|
||||
],
|
||||
"notices": [
|
||||
"删除文件或目录在确认后永久执行;移动范围限制在当前工作区内,Git 分支切换不会自动暂存或丢弃冲突改动。",
|
||||
"本次仅发布 Desktop `0.12.11`,Agent 保持 `0.11.22`;新版远端限额相关源码不作为当前 Agent 已交付能力。标准发布使用 Windows、macOS、Linux 的 x64/arm64 六平台矩阵,不构建 LoongArch 实验预览。"
|
||||
]
|
||||
},
|
||||
"en-US": {
|
||||
"highlights": [],
|
||||
"features": [
|
||||
"**Manage project files and Git from the workbar**: Create, rename, move, and delete files or directories in the right workspace panel, and inspect their properties. Git tools now support branch search, creation and switching, explicit Fetch, commit history, and per-file diffs, while returning from a preview preserves expansion, scroll position, and focus.",
|
||||
"**Multiple independent browser tabs**: Open several browsers in the workbar, each with its own page, navigation, and interaction state, plus direct interaction and fullscreen display. Tabs in one conversation share login state; model browser tools stay bound to the tab selected when the request starts, and later UI switches do not change that target.",
|
||||
"**Complete development tools for direct models**: Packaged ripgrep lets direct models search project files and content, read large UTF-8 files in pages, and modify code with multi-file patches. Ask can search and read; Execute can additionally edit files and run commands.",
|
||||
"**More context for long tasks**: Fixed truncation limits on message history, long replies, and parsed document text have been removed, and models can page through longer command and Subagent output. Browser snapshots and selectable knowledge-search result ranges are also larger, while the selected model's context window still applies.",
|
||||
"**Switchable Task Center scopes**: Task Center follows the current project by default and can switch to Global Tasks or All Projects. The selected scope is saved with the workbar layout, so project switches do not require repeated adjustment."
|
||||
],
|
||||
"fixes": [
|
||||
"**Prompt scheduled-task status**: Queued and running schedules now refresh their status promptly. Run Now remains disabled until the active occurrence finishes, preventing duplicate queue entries for the same schedule.",
|
||||
"**Runtime and model selections keep their intent**: Projects, channels, conversations, and Agent Runtimes without a pinned connection continue following the currently compatible global recommendation, while explicit selections remain fixed; remote projects also retain the Runtime chosen during save. New requests use a changed global default, while requests already in progress remain unchanged.",
|
||||
"**Compatible remote Runtime metadata**: Valid Runtime lock JSON is no longer rejected solely because whitespace or indentation differs. Signature, schema, version, and integrity validation remain unchanged."
|
||||
],
|
||||
"notices": [
|
||||
"File and directory deletion is permanent after confirmation; moves remain inside the current workspace, and Git branch switching does not automatically stash or discard conflicting changes.",
|
||||
"This release publishes Desktop `0.12.11` only; Agent remains at `0.11.22`, and source changes to newer remote limits are not claimed as delivered Agent behavior. The standard Windows, macOS, and Linux x64/arm64 matrix is used without a LoongArch experimental preview."
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "0.12.10",
|
||||
"releasedAt": "2026-09-09",
|
||||
|
||||
@@ -677,7 +677,7 @@ describe('DeepSeek Harness real ACP control-plane E2E', () => {
|
||||
assignments: ['deepseek-harness'],
|
||||
secretConfigured: false,
|
||||
transport: 'stdio',
|
||||
command: process.execPath,
|
||||
command: process.versions.electron ? 'node' : process.execPath,
|
||||
args: [
|
||||
resolve(
|
||||
'tests',
|
||||
|
||||
@@ -832,6 +832,7 @@ export class DeepSeekHarnessRuntime implements AgentRuntime {
|
||||
conversationId:
|
||||
run?.request.conversationId ??
|
||||
'deepseek-harness-tool-catalog',
|
||||
browserTabId: run?.request.browserTabId,
|
||||
workMode:
|
||||
run?.request.workMode === 'ask'
|
||||
? ('ask' as const)
|
||||
@@ -873,6 +874,7 @@ export class DeepSeekHarnessRuntime implements AgentRuntime {
|
||||
}
|
||||
const context = {
|
||||
conversationId: run.request.conversationId,
|
||||
browserTabId: run.request.browserTabId,
|
||||
workMode:
|
||||
run.request.workMode === 'execute'
|
||||
? ('execute' as const)
|
||||
|
||||
@@ -75,7 +75,7 @@ describe('runtime discovery', () => {
|
||||
await expect(validation).resolves.toEqual({ valid: false })
|
||||
})
|
||||
|
||||
it('canonicalizes and validates a configured ordinary file first', async () => {
|
||||
it('canonicalizes a configured ordinary file without launching it', async () => {
|
||||
process.env.PATH = ''
|
||||
process.env.Path = ''
|
||||
|
||||
@@ -90,7 +90,7 @@ describe('runtime discovery', () => {
|
||||
path: await realpath(process.execPath),
|
||||
source: 'configured'
|
||||
})
|
||||
expect(detection.version).toMatch(/^\d+\.\d+\.\d+/u)
|
||||
expect(detection.version).toBeUndefined()
|
||||
})
|
||||
|
||||
it('trusts a configured execution path without reading or launching it', async () => {
|
||||
|
||||
@@ -5,6 +5,7 @@ import { join, resolve } from 'node:path'
|
||||
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest'
|
||||
import { z } from 'zod'
|
||||
import {
|
||||
browserTabIdSchema,
|
||||
defaultContextCompressionSettings,
|
||||
modelProtocolSchema
|
||||
} from '../../shared/contracts'
|
||||
@@ -1612,6 +1613,7 @@ describe.runIf(enabled)('runtime end-to-end', () => {
|
||||
url,
|
||||
origin: new URL(url).origin
|
||||
}))
|
||||
const browserTabId = browserTabIdSchema.parse(crypto.randomUUID())
|
||||
const browserService = {
|
||||
getOrigin: vi.fn(() => undefined),
|
||||
navigate,
|
||||
@@ -1621,7 +1623,19 @@ describe.runIf(enabled)('runtime end-to-end', () => {
|
||||
select: vi.fn(),
|
||||
back: vi.fn(),
|
||||
screenshot: vi.fn(),
|
||||
releaseConversation: vi.fn(async () => undefined)
|
||||
releaseConversation: vi.fn(async () => undefined),
|
||||
listTabs: vi.fn(() => [
|
||||
{
|
||||
conversationId,
|
||||
tabId: browserTabId,
|
||||
primary: true,
|
||||
status: 'ready' as const,
|
||||
isLoading: false,
|
||||
canGoBack: false,
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now()
|
||||
}
|
||||
])
|
||||
}
|
||||
const gateway = new KnowledgeMcpGateway({} as never, {
|
||||
browserService: browserService as never
|
||||
@@ -1635,7 +1649,8 @@ describe.runIf(enabled)('runtime end-to-end', () => {
|
||||
controller.signal,
|
||||
'none',
|
||||
undefined,
|
||||
conversationId
|
||||
conversationId,
|
||||
browserTabId
|
||||
)!
|
||||
const common = {
|
||||
modelProfile: {
|
||||
|
||||
@@ -100,6 +100,30 @@ function settings(
|
||||
}
|
||||
|
||||
describe('runtime selection', () => {
|
||||
it('resolves direct defaults per request while fixed selections and started requests stay unchanged', () => {
|
||||
const before = settings()
|
||||
const started = applyRuntimeSelection(before, { provider: 'model' })
|
||||
const after = settings({ defaultModelProfileId: secondProfileId })
|
||||
expect(applyRuntimeSelection(after, { provider: 'model' }).settings.defaultModelProfileId).toBe(secondProfileId)
|
||||
expect(applyRuntimeSelection(after, { provider: 'model', profileId: defaultProfileId }).settings.defaultModelProfileId).toBe(defaultProfileId)
|
||||
expect(started.settings.defaultModelProfileId).toBe(defaultProfileId)
|
||||
})
|
||||
|
||||
it.each(['opencode', 'continue', 'deepseek-harness'] as const)(
|
||||
'resolves %s references from its current configuration and preserves explicit overrides', (provider) => {
|
||||
const base = settings()
|
||||
const first = base.modelProfiles[4]!
|
||||
const second = { ...first, id: responsesProfileId }
|
||||
const key = provider === 'opencode' ? 'opencodeModelProfile' : provider === 'continue' ? 'continueModelProfile' : 'deepseekHarnessModelProfile'
|
||||
const configured = settings({ [key]: first })
|
||||
const started = applyRuntimeSelection(configured, { provider })
|
||||
const changed = settings({ [key]: second })
|
||||
expect(applyRuntimeSelection(changed, { provider }).settings[key]?.id).toBe(second.id)
|
||||
expect(applyRuntimeSelection(changed, { provider, profileId: first.id }).settings[key]?.id).toBe(first.id)
|
||||
expect(started.settings[key]?.id).toBe(first.id)
|
||||
}
|
||||
)
|
||||
|
||||
it('selects an independent direct model profile without changing defaults', () => {
|
||||
const original = settings()
|
||||
const selected = applyRuntimeSelection(original, {
|
||||
|
||||
@@ -5240,22 +5240,19 @@ describe('AssistantDatabase', () => {
|
||||
expect(
|
||||
database.getProject(imageChannelProject.id).runtimeSelection
|
||||
).toEqual({
|
||||
provider: 'model',
|
||||
profileId: defaultProfileId
|
||||
provider: 'model'
|
||||
})
|
||||
expect(
|
||||
database.getProject(automaticChannelProject.id).runtimeSelection
|
||||
).toEqual({
|
||||
provider: 'model',
|
||||
profileId: defaultProfileId
|
||||
provider: 'model'
|
||||
})
|
||||
expect(
|
||||
database.getConversation(
|
||||
automaticRemoteConversation.id
|
||||
).runtimeSelection
|
||||
).toEqual({
|
||||
provider: 'model',
|
||||
profileId: defaultProfileId
|
||||
provider: 'model'
|
||||
})
|
||||
database.close()
|
||||
})
|
||||
|
||||
@@ -327,7 +327,7 @@ describe('RemoteProjectSaveService', () => {
|
||||
])
|
||||
})
|
||||
|
||||
it('persists the resolved Runtime selection', async () => {
|
||||
it('validates a resolved Runtime but persists the requested reference', async () => {
|
||||
const selection = {
|
||||
provider: 'opencode' as const,
|
||||
profileId: '00000000-0000-4000-8000-000000000099'
|
||||
@@ -341,7 +341,7 @@ describe('RemoteProjectSaveService', () => {
|
||||
draft
|
||||
})
|
||||
|
||||
expect(value.writes[0]?.project.runtimeSelection).toEqual(selection)
|
||||
expect(value.writes[0]?.project.runtimeSelection).toEqual(draft.runtimeSelection)
|
||||
})
|
||||
|
||||
it('uses the preflight project revision for updates', async () => {
|
||||
|
||||
@@ -320,7 +320,7 @@ export class RemoteProjectSaveService {
|
||||
signal.throwIfAborted()
|
||||
runtimeLease.assertCurrent()
|
||||
return {
|
||||
runtimeSelection,
|
||||
runtimeSelection: draft.runtimeSelection,
|
||||
target,
|
||||
installation,
|
||||
connection,
|
||||
|
||||
@@ -80,6 +80,89 @@ afterEach(async () => {
|
||||
})
|
||||
|
||||
describe('RuntimeSettingsStore', () => {
|
||||
it('persists default references separately from fixed profiles and resolves each settings generation', async () => {
|
||||
const { store, filePath } = await createStore()
|
||||
const firstId = '00000000-0000-4000-8000-000000000041'
|
||||
const secondId = '00000000-0000-4000-8000-000000000042'
|
||||
const modelProfiles = [firstId, secondId].map((id) => ({
|
||||
id, name: id, baseUrl: 'https://gateway.example/v1', modelName: id,
|
||||
protocol: 'openai-chat-completions' as const,
|
||||
authentication: 'api-key' as const, imageGenerationQuality: 'auto' as const,
|
||||
apiKey: { action: 'keep' as const }
|
||||
}))
|
||||
await store.update(settings({ modelProfiles, defaultModelProfileId: firstId,
|
||||
opencodeModelSource: { kind: 'default' },
|
||||
continueModelSource: { kind: 'profile', profileId: firstId },
|
||||
deepseekHarnessModelSource: { kind: 'default' }
|
||||
}))
|
||||
const started = await store.getResolvedSettings()
|
||||
await store.update(settings({ defaultModelProfileId: secondId, deepseekHarnessModelSource: { kind: 'default' } }))
|
||||
const reloaded = new RuntimeSettingsStore(filePath, cipher, {})
|
||||
await expect(reloaded.getPublicSettings()).resolves.toMatchObject({
|
||||
opencodeModelSource: { kind: 'default' },
|
||||
continueModelSource: { kind: 'profile', profileId: firstId },
|
||||
deepseekHarnessModelSource: { kind: 'default' }
|
||||
})
|
||||
await expect(reloaded.getResolvedSettings()).resolves.toMatchObject({
|
||||
opencodeModelProfile: { id: secondId },
|
||||
continueModelProfile: { id: firstId },
|
||||
deepseekHarnessModelProfile: { id: secondId }
|
||||
})
|
||||
expect(started.opencodeModelProfile?.id).toBe(firstId)
|
||||
})
|
||||
|
||||
it('falls back to compatible profiles when the global default is incompatible with a Runtime', async () => {
|
||||
const { store } = await createStore()
|
||||
const textId = '00000000-0000-4000-8000-000000000043'
|
||||
const imageId = '00000000-0000-4000-8000-000000000044'
|
||||
const anthropicId = '00000000-0000-4000-8000-000000000045'
|
||||
const modelProfiles = [
|
||||
{
|
||||
id: textId, name: 'OpenAI text', baseUrl: 'https://gateway.example/v1', modelName: 'text',
|
||||
protocol: 'openai-chat-completions' as const, authentication: 'api-key' as const,
|
||||
imageGenerationQuality: 'auto' as const, apiKey: { action: 'keep' as const }
|
||||
},
|
||||
{
|
||||
id: imageId, name: 'Image', baseUrl: 'https://gateway.example/v1', modelName: 'image',
|
||||
protocol: 'openai-images-generations' as const, authentication: 'api-key' as const,
|
||||
imageGenerationQuality: 'auto' as const, apiKey: { action: 'keep' as const }
|
||||
},
|
||||
{
|
||||
id: anthropicId, name: 'Anthropic text', baseUrl: 'https://gateway.example/v1', modelName: 'text',
|
||||
protocol: 'anthropic-messages' as const, authentication: 'api-key' as const,
|
||||
imageGenerationQuality: 'auto' as const, apiKey: { action: 'keep' as const }
|
||||
}
|
||||
]
|
||||
const runtimeDefaults = settings({
|
||||
modelProfiles,
|
||||
defaultModelProfileId: textId,
|
||||
opencodeModelSource: { kind: 'default' },
|
||||
continueModelSource: { kind: 'default' },
|
||||
deepseekHarnessModelSource: { kind: 'default' }
|
||||
})
|
||||
await store.update(runtimeDefaults)
|
||||
|
||||
await store.update({
|
||||
...runtimeDefaults,
|
||||
defaultModelProfileId: imageId
|
||||
})
|
||||
await expect(store.getResolvedSettings()).resolves.toMatchObject({
|
||||
opencodeModelProfile: { id: textId },
|
||||
continueModelProfile: { id: textId },
|
||||
deepseekHarnessModelProfile: { id: textId }
|
||||
})
|
||||
|
||||
await store.update({
|
||||
...runtimeDefaults,
|
||||
defaultModelProfileId: anthropicId
|
||||
})
|
||||
await expect(store.getResolvedSettings()).resolves.toMatchObject({
|
||||
opencodeModelProfile: { id: anthropicId },
|
||||
continueModelProfile: { id: anthropicId },
|
||||
deepseekHarnessModelProfile: { id: textId }
|
||||
})
|
||||
})
|
||||
|
||||
it('seeds a local provider-neutral model connection', async () => {
|
||||
const { store } = await createStore()
|
||||
|
||||
@@ -491,12 +574,10 @@ describe('RuntimeSettingsStore', () => {
|
||||
await expect(store.getPublicSettings()).resolves.toMatchObject({
|
||||
opencodeEmbedded: true,
|
||||
opencodeModelSource: {
|
||||
kind: 'profile',
|
||||
profileId: '00000000-0000-4000-8000-000000000001'
|
||||
kind: 'default'
|
||||
},
|
||||
continueModelSource: {
|
||||
kind: 'profile',
|
||||
profileId: '00000000-0000-4000-8000-000000000001'
|
||||
kind: 'default'
|
||||
}
|
||||
})
|
||||
await expect(store.getResolvedSettings()).resolves.toMatchObject({
|
||||
|
||||
@@ -1431,7 +1431,13 @@ export class RuntimeSettingsStore {
|
||||
): ResolvedModelProfile | undefined {
|
||||
const source = settings.deepseekHarnessModelSource
|
||||
if (source.kind === 'default') {
|
||||
return modelProfiles.find((profile) => profile.id === settings.defaultModelProfileId)
|
||||
return (
|
||||
modelProfiles.find(
|
||||
(profile) =>
|
||||
profile.id === settings.defaultModelProfileId &&
|
||||
isDeepSeekHarnessModelProfile(profile)
|
||||
) ?? modelProfiles.find(isDeepSeekHarnessModelProfile)
|
||||
)
|
||||
}
|
||||
if (source.kind === 'profile') {
|
||||
return modelProfiles.find(
|
||||
@@ -1939,13 +1945,19 @@ export class RuntimeSettingsStore {
|
||||
const opencodeModelProfile =
|
||||
!agent.opencodeBaseUrl &&
|
||||
settings.opencodeModelSource.kind !== 'platform'
|
||||
? profilesById.get(settings.opencodeModelSource.kind === 'default'
|
||||
? settings.defaultModelProfileId : settings.opencodeModelSource.profileId)
|
||||
? profilesById.get(
|
||||
settings.opencodeModelSource.kind === 'default'
|
||||
? compatibleTextProfileId(settings) ?? ''
|
||||
: settings.opencodeModelSource.profileId
|
||||
)
|
||||
: undefined
|
||||
const continueModelProfile =
|
||||
settings.continueModelSource.kind !== 'platform'
|
||||
? profilesById.get(settings.continueModelSource.kind === 'default'
|
||||
? settings.defaultModelProfileId : settings.continueModelSource.profileId)
|
||||
? profilesById.get(
|
||||
settings.continueModelSource.kind === 'default'
|
||||
? compatibleTextProfileId(settings) ?? ''
|
||||
: settings.continueModelSource.profileId
|
||||
)
|
||||
: undefined
|
||||
const deepseekHarnessModelProfile =
|
||||
this.resolveDeepSeekHarnessModelProfile(settings, modelProfiles)
|
||||
@@ -2366,12 +2378,22 @@ export class RuntimeSettingsStore {
|
||||
source: RuntimeSettings['opencodeModelSource'],
|
||||
runtimeLabel: 'OpenCode' | 'Continue'
|
||||
): void => {
|
||||
if (source.kind !== 'profile') {
|
||||
if (source.kind === 'platform') {
|
||||
return
|
||||
}
|
||||
const profile = modelProfiles.find(
|
||||
(candidate) => candidate.id === source.profileId
|
||||
)
|
||||
const profile =
|
||||
source.kind === 'default'
|
||||
? (modelProfiles.find(
|
||||
(candidate) =>
|
||||
candidate.id === defaultModelProfileId &&
|
||||
isAgentRuntimeModelProtocol(candidate.protocol)
|
||||
) ??
|
||||
modelProfiles.find((candidate) =>
|
||||
isAgentRuntimeModelProtocol(candidate.protocol)
|
||||
))
|
||||
: modelProfiles.find(
|
||||
(candidate) => candidate.id === source.profileId
|
||||
)
|
||||
if (!profile) {
|
||||
throw new Error(`${runtimeLabel} 引用的模型连接不存在`)
|
||||
}
|
||||
@@ -2417,11 +2439,18 @@ export class RuntimeSettingsStore {
|
||||
const requestedDeepSeekHarnessSource =
|
||||
input.deepseekHarnessModelSource ??
|
||||
current.deepseekHarnessModelSource
|
||||
if (requestedDeepSeekHarnessSource.kind === 'profile') {
|
||||
const profile = modelProfiles.find(
|
||||
(candidate) =>
|
||||
candidate.id === requestedDeepSeekHarnessSource.profileId
|
||||
)
|
||||
if (requestedDeepSeekHarnessSource.kind !== 'platform') {
|
||||
const profile =
|
||||
requestedDeepSeekHarnessSource.kind === 'default'
|
||||
? (modelProfiles.find(
|
||||
(candidate) =>
|
||||
candidate.id === defaultModelProfileId &&
|
||||
isDeepSeekHarnessModelProfile(candidate)
|
||||
) ?? modelProfiles.find(isDeepSeekHarnessModelProfile))
|
||||
: modelProfiles.find(
|
||||
(candidate) =>
|
||||
candidate.id === requestedDeepSeekHarnessSource.profileId
|
||||
)
|
||||
if (!profile) {
|
||||
throw new Error('DeepSeek Harness 引用的模型连接不存在')
|
||||
}
|
||||
|
||||
@@ -3507,7 +3507,6 @@ describe("App", () => {
|
||||
expect(request?.prompt).toBe("帮我分析项目");
|
||||
expect(request?.runtimeSelection).toEqual({
|
||||
provider: "model",
|
||||
profileId: modelProfileId,
|
||||
});
|
||||
expect(
|
||||
screen
|
||||
@@ -6267,7 +6266,7 @@ describe("App", () => {
|
||||
name: /^OpenCode · 默认模型.*sonnet-5$/u,
|
||||
}),
|
||||
).toBeInTheDocument();
|
||||
expect(within(runtimeMenu).getAllByRole("menuitemradio")).toHaveLength(1);
|
||||
expect(within(runtimeMenu).getAllByRole("menuitemradio")).toHaveLength(2);
|
||||
expect(within(runtimeMenu).queryByText("直连模型")).not.toBeInTheDocument();
|
||||
expect(
|
||||
within(runtimeMenu).queryByText("Continue Runtime"),
|
||||
@@ -8411,7 +8410,7 @@ describe("App", () => {
|
||||
expect(openCodeModel).toBeEnabled();
|
||||
expect(continueModel).toBeEnabled();
|
||||
expect(deepseekHarness).toBeEnabled();
|
||||
expect(screen.getAllByRole("menuitemradio")).toHaveLength(5);
|
||||
expect(screen.getAllByRole("menuitemradio")).toHaveLength(10);
|
||||
expect(within(runtimeMenu).getAllByRole("separator")).toHaveLength(4);
|
||||
expect(within(runtimeMenu).queryByRole("menu")).not.toBeInTheDocument();
|
||||
expect(
|
||||
@@ -8428,7 +8427,9 @@ describe("App", () => {
|
||||
screen.queryByRole("menuitem", { name: /Agent Runtime/u }),
|
||||
).not.toBeInTheDocument();
|
||||
|
||||
await waitFor(() => expect(directModel).toHaveFocus());
|
||||
const defaultDirect = screen.getByRole("menuitemradio", { name: /^直连 · 跟随全局默认模型/u });
|
||||
await waitFor(() => expect(defaultDirect).toHaveFocus());
|
||||
fireEvent.keyDown(defaultDirect, { key: "ArrowDown" });
|
||||
expect(directModel).toHaveAttribute("tabindex", "0");
|
||||
expect(secondDirectModel).toHaveAttribute("tabindex", "-1");
|
||||
fireEvent.keyDown(directModel, { key: "ArrowDown" });
|
||||
@@ -8453,7 +8454,6 @@ describe("App", () => {
|
||||
await waitFor(() =>
|
||||
expect(api.agent.getStatus).toHaveBeenLastCalledWith({
|
||||
provider: "opencode",
|
||||
profileId: modelProfileId,
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -8475,7 +8475,6 @@ describe("App", () => {
|
||||
await waitFor(() =>
|
||||
expect(api.agent.getStatus).toHaveBeenLastCalledWith({
|
||||
provider: "continue",
|
||||
profileId: modelProfileId,
|
||||
}),
|
||||
);
|
||||
});
|
||||
@@ -8499,7 +8498,7 @@ describe("App", () => {
|
||||
|
||||
fireEvent.click(runtimeButton);
|
||||
const selectedModel = screen.getByRole("menuitemradio", {
|
||||
name: /^默认模型.*sonnet-5$/u,
|
||||
name: /^直连 · 跟随全局默认模型/u,
|
||||
});
|
||||
await waitFor(() => expect(selectedModel).toHaveFocus());
|
||||
fireEvent.keyDown(selectedModel, { key: "Tab" });
|
||||
@@ -9313,7 +9312,6 @@ describe("App", () => {
|
||||
rootPath: project.rootPath,
|
||||
runtimeSelection: {
|
||||
provider: "continue",
|
||||
profileId: modelProfileId,
|
||||
},
|
||||
}),
|
||||
),
|
||||
|
||||
@@ -1491,7 +1491,8 @@ function getProjectDefaultRuntimeSelection(
|
||||
settings: RuntimeSettings,
|
||||
): AgentRuntimeSelection {
|
||||
if (isManagedSshProject(project)) {
|
||||
return getRuntimeSelectionForProvider("opencode", settings);
|
||||
return project.runtimeSelection?.provider === "opencode"
|
||||
? project.runtimeSelection : getRuntimeSelectionForProvider("opencode", settings);
|
||||
}
|
||||
const selection = project?.runtimeSelection;
|
||||
return !selection || selection.provider === "auto"
|
||||
@@ -3634,7 +3635,7 @@ function App(): React.JSX.Element {
|
||||
requestAnimationFrame(() => inputRef.current?.focus());
|
||||
return true;
|
||||
},
|
||||
[notify, projects, runtimeSettings, setActiveId, setView],
|
||||
[notify, projects, setActiveId, setView],
|
||||
);
|
||||
const activeProjectDisplayName = activeProject
|
||||
? getProjectDisplayText(activeProject, tWorkspace).name
|
||||
@@ -7209,7 +7210,9 @@ function App(): React.JSX.Element {
|
||||
...conversation,
|
||||
contextCompressionState: state,
|
||||
contextMetrics: {
|
||||
runtimeSelectionKey: activeRuntimeSelectionKey,
|
||||
runtimeSelectionKey: agentRuntimeSelectionKey(runtimeSettings
|
||||
? resolveContextMetricsRuntimeSelection(activeRuntimeSelection, runtimeSettings)
|
||||
: activeRuntimeSelection),
|
||||
contextTokens: estimatedAfterTokens,
|
||||
source: "estimated",
|
||||
basis: "conversation",
|
||||
|
||||
@@ -497,8 +497,7 @@ describe('ChannelSettingsSection', () => {
|
||||
)
|
||||
expect(backend).toHaveValue(
|
||||
agentRuntimeSelectionKey({
|
||||
provider: 'model',
|
||||
profileId: directProfileId
|
||||
provider: 'model'
|
||||
})
|
||||
)
|
||||
expect(
|
||||
|
||||
@@ -3,7 +3,6 @@ import { useTranslation } from 'react-i18next'
|
||||
import type { RuntimeSettings } from '../../shared/contracts'
|
||||
import {
|
||||
agentRuntimeSelectionKey,
|
||||
getDefaultRuntimeSelection,
|
||||
getRuntimeSelectionForProvider,
|
||||
isChannelModelProfileUsable,
|
||||
repairChannelRuntimeSelection,
|
||||
@@ -27,7 +26,7 @@ function runtimeSelectionDescription(
|
||||
): string {
|
||||
if (selection.provider === 'model') {
|
||||
const profile = settings.modelProfiles.find(
|
||||
(candidate) => candidate.id === selection.profileId
|
||||
(candidate) => candidate.id === (selection.profileId ?? settings.defaultModelProfileId)
|
||||
)
|
||||
if (!profile) {
|
||||
return t('channels.project.missingSelection')
|
||||
@@ -71,13 +70,11 @@ export function ProjectRuntimeSelector({
|
||||
}: ProjectRuntimeSelectorProps): React.JSX.Element {
|
||||
const { t } = useTranslation('integrations')
|
||||
const initialSelection =
|
||||
selection ?? getDefaultRuntimeSelection(runtimeSettings)
|
||||
selection ?? { provider: 'auto' as const }
|
||||
const runtimeSelection =
|
||||
selectionMode === 'channel'
|
||||
? repairChannelRuntimeSelection(initialSelection, runtimeSettings)
|
||||
: initialSelection.provider === 'auto'
|
||||
? getDefaultRuntimeSelection(runtimeSettings)
|
||||
: initialSelection
|
||||
: initialSelection
|
||||
const directProfiles = runtimeSettings.modelProfiles.filter(
|
||||
isChannelModelProfileUsable
|
||||
)
|
||||
@@ -99,9 +96,6 @@ export function ProjectRuntimeSelector({
|
||||
'deepseek-harness'
|
||||
] as const
|
||||
const runtimeSelections = runtimeProviders.map((provider) => {
|
||||
if (runtimeSelection.provider === provider) {
|
||||
return runtimeSelection
|
||||
}
|
||||
return selectionMode === 'channel'
|
||||
? ({ provider } as AgentRuntimeSelection)
|
||||
: getRuntimeSelectionForProvider(provider, runtimeSettings)
|
||||
@@ -110,7 +104,14 @@ export function ProjectRuntimeSelector({
|
||||
provider: 'model' as const,
|
||||
profileId: profile.id
|
||||
}))
|
||||
const selections = [...directSelections, ...runtimeSelections]
|
||||
const inheritedSelections: AgentRuntimeSelection[] = [
|
||||
...(selectionMode === 'configured' ? [{ provider: 'auto' as const }] : []),
|
||||
{ provider: 'model' }
|
||||
]
|
||||
const fixedRuntimeSelection = runtimeSelection.provider !== 'model' &&
|
||||
'profileId' in runtimeSelection && runtimeSelection.profileId ? runtimeSelection : undefined
|
||||
const selections = [...inheritedSelections, ...directSelections, ...runtimeSelections,
|
||||
...(fixedRuntimeSelection ? [fixedRuntimeSelection] : [])]
|
||||
const selectionByKey = new Map(
|
||||
selections.map((candidate) => [
|
||||
agentRuntimeSelectionKey(candidate),
|
||||
@@ -132,6 +133,16 @@ export function ProjectRuntimeSelector({
|
||||
}}
|
||||
value={agentRuntimeSelectionKey(runtimeSelection)}
|
||||
>
|
||||
{inheritedSelections.map((candidate) => (
|
||||
<option key={candidate.provider} value={agentRuntimeSelectionKey(candidate)}>
|
||||
{t(candidate.provider === 'auto' ? 'channels.project.automaticDescription' : 'channels.project.defaultDirect')}
|
||||
</option>
|
||||
))}
|
||||
{fixedRuntimeSelection && (
|
||||
<option value={agentRuntimeSelectionKey(fixedRuntimeSelection)}>
|
||||
{fixedRuntimeSelection.provider} · {runtimeSettings.modelProfiles.find((profile) => profile.id === fixedRuntimeSelection.profileId)?.name ?? t('channels.project.missingProfile')}
|
||||
</option>
|
||||
)}
|
||||
<optgroup label={t('channels.project.directModels')}>
|
||||
{selectedDirectUnavailable && (
|
||||
<option
|
||||
|
||||
@@ -3146,7 +3146,7 @@ describe('SettingsPanel runtime files', () => {
|
||||
const source = screen.getByLabelText(
|
||||
'DeepSeek Harness OpenAI 兼容模型连接'
|
||||
)
|
||||
expect(source).toHaveValue('')
|
||||
expect(source).toHaveValue('platform')
|
||||
fireEvent.change(source, {
|
||||
target: { value: runtimeSettings.modelProfiles[0]!.id }
|
||||
})
|
||||
@@ -3155,7 +3155,7 @@ describe('SettingsPanel runtime files', () => {
|
||||
await waitFor(() =>
|
||||
expect(updateRuntime).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
deepseekHarnessModelSource: { kind: 'platform' }
|
||||
deepseekHarnessModelSource: { kind: 'profile', profileId: runtimeSettings.defaultModelProfileId }
|
||||
})
|
||||
)
|
||||
)
|
||||
@@ -4215,11 +4215,11 @@ describe('SettingsPanel runtime files', () => {
|
||||
defaultModelProfileId: nextDefault?.id,
|
||||
opencodeModelSource: {
|
||||
kind: 'profile',
|
||||
profileId: nextDefault?.id
|
||||
profileId: runtimeSettings.defaultModelProfileId
|
||||
},
|
||||
continueModelSource: {
|
||||
kind: 'profile',
|
||||
profileId: nextDefault?.id
|
||||
profileId: runtimeSettings.defaultModelProfileId
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
@@ -1440,12 +1440,6 @@ export function SettingsPanel({
|
||||
profileInputs.find(
|
||||
(profile) => profile.id === defaultProfile.id
|
||||
) ?? profileInputs[0]!
|
||||
const normalizedDeepseekHarnessModelSource =
|
||||
deepseekHarnessModelSource.kind === 'profile' &&
|
||||
deepseekHarnessModelSource.profileId === defaultProfile.id &&
|
||||
defaultProfile.credentialSource === 'environment'
|
||||
? { kind: 'platform' as const }
|
||||
: deepseekHarnessModelSource
|
||||
const value = await window.goodbuddy.settings.updateRuntime({
|
||||
provider,
|
||||
modelBaseUrl: defaultProfileInput.baseUrl,
|
||||
@@ -1514,8 +1508,7 @@ export function SettingsPanel({
|
||||
defaultModelProfileId: defaultProfile.id,
|
||||
opencodeModelSource,
|
||||
continueModelSource,
|
||||
deepseekHarnessModelSource:
|
||||
normalizedDeepseekHarnessModelSource,
|
||||
deepseekHarnessModelSource,
|
||||
contextCompression: contextCompressionInput,
|
||||
toolApproval,
|
||||
subagentSmartRoutingEnabled
|
||||
@@ -1897,44 +1890,12 @@ export function SettingsPanel({
|
||||
const selectDefaultModelProfile = (
|
||||
profile: ModelProfileDraft
|
||||
): void => {
|
||||
const previousDefaultProfileId = defaultModelProfileId
|
||||
const compatibleProfile = findPreferredCompatibleModelProfile(
|
||||
modelProfiles,
|
||||
profile.id,
|
||||
(candidate) =>
|
||||
isAgentRuntimeModelProtocol(candidate.protocol)
|
||||
)
|
||||
const nextRuntimeSource: RuntimeModelSource = compatibleProfile
|
||||
? { kind: 'profile', profileId: compatibleProfile.id }
|
||||
: { kind: 'platform' }
|
||||
setDefaultModelProfileId(profile.id)
|
||||
if (
|
||||
opencodeModelSource.kind === 'profile' &&
|
||||
opencodeModelSource.profileId === previousDefaultProfileId
|
||||
) {
|
||||
setOpencodeModelSource(nextRuntimeSource)
|
||||
}
|
||||
if (
|
||||
continueModelSource.kind === 'profile' &&
|
||||
continueModelSource.profileId === previousDefaultProfileId
|
||||
) {
|
||||
setContinueModelSource(nextRuntimeSource)
|
||||
}
|
||||
if (
|
||||
deepseekHarnessModelSource.kind === 'profile' &&
|
||||
deepseekHarnessModelSource.profileId === previousDefaultProfileId &&
|
||||
profile.protocol === 'openai-chat-completions'
|
||||
) {
|
||||
setDeepseekHarnessModelSource({
|
||||
kind: 'profile',
|
||||
profileId: profile.id
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const parseModelSource = (value: string): RuntimeModelSource =>
|
||||
value === 'platform'
|
||||
? { kind: 'platform' }
|
||||
value === 'platform' || value === 'default'
|
||||
? { kind: value }
|
||||
: { kind: 'profile', profileId: value }
|
||||
|
||||
const isOpenCodeCompatible = (
|
||||
@@ -1981,7 +1942,11 @@ export function SettingsPanel({
|
||||
? isDeepseekHarnessCompatible(profile)
|
||||
: isAgentRuntimeModelProtocol(profile.protocol))
|
||||
)
|
||||
: undefined
|
||||
: activeRuntimeModelSource.kind === 'default'
|
||||
? agentRuntimeType === 'deepseek-harness'
|
||||
? defaultDeepseekHarnessModelProfile
|
||||
: defaultTextModelProfile
|
||||
: undefined
|
||||
const savedRoleModelProfiles = (settings?.modelProfiles ?? [])
|
||||
.filter((profile) =>
|
||||
isAgentRuntimeModelProtocol(profile.protocol)
|
||||
@@ -2386,14 +2351,13 @@ export function SettingsPanel({
|
||||
<legend>{t('runtime.sourceLegend')}</legend>
|
||||
<label>
|
||||
<input
|
||||
checked={opencodeModelSource.kind === 'profile'}
|
||||
checked={opencodeModelSource.kind !== 'platform'}
|
||||
disabled={!defaultTextModelProfile}
|
||||
name="opencode-model-source"
|
||||
onChange={() => {
|
||||
if (defaultTextModelProfile) {
|
||||
setOpencodeModelSource({
|
||||
kind: 'profile',
|
||||
profileId: defaultTextModelProfile.id
|
||||
kind: 'default'
|
||||
})
|
||||
setOpencodeBaseUrl('')
|
||||
}
|
||||
@@ -2401,7 +2365,7 @@ export function SettingsPanel({
|
||||
type="radio"
|
||||
/>
|
||||
<span>
|
||||
<strong>{t('runtime.followRecommended')}</strong>
|
||||
<strong>{t('runtime.goodBuddyModelSource')}</strong>
|
||||
<small>
|
||||
{t('runtime.opencode.followDescription')}
|
||||
</small>
|
||||
@@ -2428,14 +2392,14 @@ export function SettingsPanel({
|
||||
</span>
|
||||
</label>
|
||||
</fieldset>
|
||||
{opencodeModelSource.kind === 'profile' && (
|
||||
{opencodeModelSource.kind !== 'platform' && (
|
||||
<label className="field">
|
||||
<span>{t('runtime.goodBuddyConnection')}</span>
|
||||
<select
|
||||
aria-label={`OpenCode ${t(
|
||||
'runtime.goodBuddyConnection'
|
||||
)}`}
|
||||
value={opencodeModelSource.profileId}
|
||||
value={opencodeModelSource.kind === 'default' ? 'default' : opencodeModelSource.profileId}
|
||||
onChange={(event) => {
|
||||
setOpencodeModelSource(
|
||||
parseModelSource(event.target.value)
|
||||
@@ -2443,6 +2407,7 @@ export function SettingsPanel({
|
||||
setOpencodeBaseUrl('')
|
||||
}}
|
||||
>
|
||||
<option value="default">{t('runtime.followRecommended')}</option>
|
||||
{modelProfiles.map((profile) => (
|
||||
<option
|
||||
disabled={!isOpenCodeCompatible(profile)}
|
||||
@@ -2617,21 +2582,20 @@ export function SettingsPanel({
|
||||
<legend>{t('runtime.sourceLegend')}</legend>
|
||||
<label>
|
||||
<input
|
||||
checked={continueModelSource.kind === 'profile'}
|
||||
checked={continueModelSource.kind !== 'platform'}
|
||||
disabled={!defaultTextModelProfile}
|
||||
name="continue-model-source"
|
||||
onChange={() => {
|
||||
if (defaultTextModelProfile) {
|
||||
setContinueModelSource({
|
||||
kind: 'profile',
|
||||
profileId: defaultTextModelProfile.id
|
||||
kind: 'default'
|
||||
})
|
||||
}
|
||||
}}
|
||||
type="radio"
|
||||
/>
|
||||
<span>
|
||||
<strong>{t('runtime.followRecommended')}</strong>
|
||||
<strong>{t('runtime.goodBuddyModelSource')}</strong>
|
||||
<small>
|
||||
{t('runtime.continue.followDescription')}
|
||||
</small>
|
||||
@@ -2658,20 +2622,21 @@ export function SettingsPanel({
|
||||
</span>
|
||||
</label>
|
||||
</fieldset>
|
||||
{continueModelSource.kind === 'profile' && (
|
||||
{continueModelSource.kind !== 'platform' && (
|
||||
<label className="field">
|
||||
<span>{t('runtime.goodBuddyConnection')}</span>
|
||||
<select
|
||||
aria-label={`Continue ${t(
|
||||
'runtime.goodBuddyConnection'
|
||||
)}`}
|
||||
value={continueModelSource.profileId}
|
||||
value={continueModelSource.kind === 'default' ? 'default' : continueModelSource.profileId}
|
||||
onChange={(event) =>
|
||||
setContinueModelSource(
|
||||
parseModelSource(event.target.value)
|
||||
)
|
||||
}
|
||||
>
|
||||
<option value="default">{t('runtime.followRecommended')}</option>
|
||||
{modelProfiles.map((profile) => (
|
||||
<option
|
||||
disabled={!isContinueCompatible(profile)}
|
||||
@@ -2822,14 +2787,11 @@ export function SettingsPanel({
|
||||
value={
|
||||
deepseekHarnessModelSource.kind === 'profile'
|
||||
? deepseekHarnessModelSource.profileId
|
||||
: ''
|
||||
: deepseekHarnessModelSource.kind
|
||||
}
|
||||
>
|
||||
<option disabled value="">
|
||||
{t(
|
||||
'runtime.deepseekHarness.connectionPlaceholder'
|
||||
)}
|
||||
</option>
|
||||
<option value="default">{t('runtime.followRecommended')}</option>
|
||||
<option value="platform">{t('runtime.ownConfiguration', { runtime: 'DeepSeek Harness' })}</option>
|
||||
{modelProfiles.map((profile) => (
|
||||
<option
|
||||
disabled={!isDeepseekHarnessCompatible(profile)}
|
||||
|
||||
@@ -45,8 +45,9 @@ export const integrations = {
|
||||
'The selected direct model has no credential. Configure it under Model connections first.',
|
||||
directDescription:
|
||||
'Process messages directly with {{name}} ({{modelName}}).',
|
||||
defaultDirect: 'Direct · Follow the global default model',
|
||||
automaticDescription:
|
||||
'Process messages with the default direct model from Model settings.',
|
||||
'Follow the global default Runtime setting.',
|
||||
runtimeDescription:
|
||||
'Run through the {{runtime}} Agent Runtime using the global {{runtime}} configuration under Agent Runtime settings.',
|
||||
defaultMode: 'Default mode',
|
||||
|
||||
@@ -347,7 +347,8 @@ export const settings = {
|
||||
},
|
||||
advanced: 'Advanced settings',
|
||||
sourceLegend: 'Model configuration source',
|
||||
followRecommended: 'Follow the GoodBuddy model (recommended)',
|
||||
followRecommended: 'Follow the global default model',
|
||||
goodBuddyModelSource: 'Use a GoodBuddy model connection',
|
||||
goodBuddyConnection: 'GoodBuddy model connection',
|
||||
pinConnectionDescription:
|
||||
'You can pin this Runtime to another GoodBuddy text model connection.',
|
||||
|
||||
@@ -38,7 +38,8 @@ export const integrations = {
|
||||
'所选直连模型尚未配置密钥,请先到模型连接中完成配置。',
|
||||
directDescription:
|
||||
'直接使用 {{name}}({{modelName}})处理消息。',
|
||||
automaticDescription: '使用模型设置中的默认直连模型处理消息。',
|
||||
automaticDescription: '跟随全局 Runtime 默认设置',
|
||||
defaultDirect: '直连 · 跟随全局默认模型',
|
||||
runtimeDescription:
|
||||
'通过 {{runtime}} Agent Runtime 运行,并跟随“Agent Runtime”设置中的全局 {{runtime}} 配置。',
|
||||
defaultMode: '默认模式',
|
||||
|
||||
@@ -318,9 +318,10 @@ export const settings = {
|
||||
},
|
||||
advanced: '高级设置',
|
||||
sourceLegend: '模型配置来源',
|
||||
followRecommended: '跟随 GoodBuddy 模型(推荐)',
|
||||
followRecommended: '跟随全局默认模型',
|
||||
goodBuddyModelSource: '使用 GoodBuddy 模型连接',
|
||||
goodBuddyConnection: 'GoodBuddy 模型连接',
|
||||
pinConnectionDescription: '可固定到其他 GoodBuddy 文本模型连接。',
|
||||
pinConnectionDescription: '选择默认项会跟随全局默认模型;选择具体连接会固定使用该连接。',
|
||||
incompatibleSuffix: '(不兼容)',
|
||||
serverAddress: 'Server 地址',
|
||||
bundledProgramPlaceholder: '留空使用 GoodBuddy 内置程序',
|
||||
|
||||
@@ -2,8 +2,9 @@ import { describe, expect, it } from 'vitest'
|
||||
import type { RuntimeSettings } from '../../shared/contracts'
|
||||
import {
|
||||
getDefaultRuntimeSelection,
|
||||
getRuntimeSelectionProfileId,
|
||||
getRuntimeSelectionForProvider
|
||||
} from './runtime-selection'
|
||||
} from '../../shared/runtime-selection-contracts'
|
||||
|
||||
const harnessProfileId = '00000000-0000-4000-8000-000000000071'
|
||||
|
||||
@@ -38,4 +39,34 @@ describe('DeepSeek Harness runtime selection', () => {
|
||||
provider: 'deepseek-harness'
|
||||
})
|
||||
})
|
||||
|
||||
it('resolves default references to the first Runtime-compatible profile', () => {
|
||||
const imageId = '00000000-0000-4000-8000-000000000072'
|
||||
const anthropicId = '00000000-0000-4000-8000-000000000073'
|
||||
const openAiId = '00000000-0000-4000-8000-000000000074'
|
||||
const settings = {
|
||||
provider: 'model' as const,
|
||||
defaultModelProfileId: imageId,
|
||||
opencodeBaseUrl: '',
|
||||
opencodeEmbedded: true,
|
||||
opencodeModelSource: { kind: 'default' as const },
|
||||
continueModelSource: { kind: 'default' as const },
|
||||
deepseekHarnessModelSource: { kind: 'default' as const },
|
||||
modelProfiles: [
|
||||
{ id: imageId, protocol: 'openai-images-generations' },
|
||||
{ id: anthropicId, protocol: 'anthropic-messages' },
|
||||
{
|
||||
id: openAiId,
|
||||
baseUrl: 'https://gateway.example/v1',
|
||||
protocol: 'openai-chat-completions',
|
||||
authentication: 'api-key' as const,
|
||||
apiKeyConfigured: true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
expect(getRuntimeSelectionProfileId({ provider: 'opencode' }, settings)).toBe(anthropicId)
|
||||
expect(getRuntimeSelectionProfileId({ provider: 'continue' }, settings)).toBe(anthropicId)
|
||||
expect(getRuntimeSelectionProfileId({ provider: 'deepseek-harness' }, settings)).toBe(openAiId)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -69,6 +69,7 @@ type RuntimeModelSource =
|
||||
export type RuntimeSelectionDefaultSettings = {
|
||||
provider: AgentRuntimeSelection['provider']
|
||||
defaultModelProfileId: string
|
||||
modelProfiles?: RuntimeSelectionRepairSettings['modelProfiles']
|
||||
opencodeBaseUrl: string
|
||||
opencodeEmbedded: boolean
|
||||
opencodeModelSource?: RuntimeModelSource
|
||||
@@ -85,6 +86,7 @@ export function getRuntimeSelectionForProvider(
|
||||
provider: Exclude<AgentRuntimeSelection['provider'], 'auto'>,
|
||||
_settings: RuntimeSelectionDefaultSettings
|
||||
): AgentRuntimeSelection {
|
||||
void _settings
|
||||
return { provider }
|
||||
}
|
||||
|
||||
@@ -110,8 +112,25 @@ export function getRuntimeSelectionProfileId(
|
||||
if (selection.provider === 'model') return settings.defaultModelProfileId
|
||||
const source = selection.provider === 'opencode' ? settings.opencodeModelSource
|
||||
: selection.provider === 'continue' ? settings.continueModelSource : settings.deepseekHarnessModelSource
|
||||
return source?.kind === 'default' ? settings.defaultModelProfileId
|
||||
: source?.kind === 'profile' ? source.profileId : undefined
|
||||
if (source?.kind === 'profile') return source.profileId
|
||||
if (source?.kind !== 'default') return undefined
|
||||
const profiles = settings.modelProfiles ?? []
|
||||
const isCompatible = (profile: ChannelModelProfile): boolean =>
|
||||
selection.provider === 'deepseek-harness'
|
||||
? profile.protocol === 'openai-chat-completions' &&
|
||||
profile.authentication === 'api-key' &&
|
||||
Boolean(profile.baseUrl) &&
|
||||
isDeepSeekHarnessModelProfile({
|
||||
baseUrl: profile.baseUrl!,
|
||||
protocol: profile.protocol,
|
||||
authentication: profile.authentication
|
||||
})
|
||||
: Boolean(profile.protocol) &&
|
||||
profile.protocol !== 'openai-images-generations'
|
||||
return profiles.find(
|
||||
(profile) =>
|
||||
profile.id === settings.defaultModelProfileId && isCompatible(profile)
|
||||
)?.id ?? profiles.find(isCompatible)?.id
|
||||
}
|
||||
|
||||
export function isChannelModelProfileUsable(
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
import { createRequire } from 'node:module'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { dirname, join } from 'node:path'
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
||||
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from 'vitest'
|
||||
|
||||
interface PortableBuilderModule {
|
||||
assertPortableOutput(directory: string): void
|
||||
@@ -20,6 +20,16 @@ const portableBuilder = require(
|
||||
) as PortableBuilderModule
|
||||
|
||||
let directory: string
|
||||
const electronProcess = process as NodeJS.Process & { noAsar?: boolean }
|
||||
const originalNoAsar = electronProcess.noAsar
|
||||
|
||||
beforeAll(() => {
|
||||
electronProcess.noAsar = true
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
electronProcess.noAsar = originalNoAsar
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
directory = mkdtempSync(
|
||||
|
||||
@@ -11,7 +11,7 @@ import { createRequire } from 'node:module'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { Writable } from 'node:stream'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
|
||||
|
||||
interface ReleaseOptions {
|
||||
platform: 'windows' | 'macos' | 'linux'
|
||||
@@ -204,6 +204,16 @@ const windowsOptions: ReleaseOptions = {
|
||||
unsigned: false,
|
||||
help: false
|
||||
}
|
||||
const electronProcess = process as NodeJS.Process & { noAsar?: boolean }
|
||||
const originalNoAsar = electronProcess.noAsar
|
||||
|
||||
beforeAll(() => {
|
||||
electronProcess.noAsar = true
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
electronProcess.noAsar = originalNoAsar
|
||||
})
|
||||
|
||||
function pe(machine: number): Buffer {
|
||||
const buffer = Buffer.alloc(256)
|
||||
|
||||
Reference in New Issue
Block a user