feat: add DeepSeek Harness runtime

This commit is contained in:
lofyer
2026-08-14 10:04:34 +08:00
parent fca9888f83
commit 8286e120a1
69 changed files with 13660 additions and 351 deletions
+16 -2
View File
@@ -243,6 +243,11 @@ const api: DesktopApi = {
continue: {
available: false,
detail: '未检测到 Continue'
},
deepseekHarness: {
available: true,
path: 'bundled://deepseek-harness',
detail: 'Bundled Harness Adapter ready'
}
})),
selectRuntimeFile: vi.fn(async () => undefined),
@@ -3076,6 +3081,11 @@ describe('App', () => {
name: /^Continue · .*sonnet-5$/u
})
).toBeInTheDocument()
expect(
screen.getByRole('menuitemradio', {
name: /^DeepSeek Harness · /u
})
).toBeInTheDocument()
fireEvent.click(
screen.getByRole('menuitemradio', {
name: /^.*sonnet-5$/u
@@ -3190,12 +3200,16 @@ describe('App', () => {
const continueModel = screen.getByRole('menuitemradio', {
name: /^Continue · .*sonnet-5$/u
})
const deepseekHarness = screen.getByRole('menuitemradio', {
name: /^DeepSeek Harness · /u
})
expect(directModel).toBeEnabled()
expect(secondDirectModel).toBeEnabled()
expect(openCodeModel).toBeEnabled()
expect(continueModel).toBeEnabled()
expect(screen.getAllByRole('menuitemradio')).toHaveLength(4)
expect(within(runtimeMenu).getAllByRole('separator')).toHaveLength(3)
expect(deepseekHarness).toBeEnabled()
expect(screen.getAllByRole('menuitemradio')).toHaveLength(5)
expect(within(runtimeMenu).getAllByRole('separator')).toHaveLength(4)
expect(within(runtimeMenu).queryByRole('menu')).not.toBeInTheDocument()
expect(
screen.queryByRole('menuitemradio', {
+68 -2
View File
@@ -295,6 +295,7 @@ function isAgentRuntime(
runtime: AgentRuntimeStatus | undefined
): boolean {
return runtime?.id === 'opencode' || runtime?.id === 'continue'
|| runtime?.id === 'deepseek-harness'
}
function supportsSubagentSmartRouting(
@@ -899,6 +900,13 @@ function getRuntimeSelectionLabel(
? `Continue · ${labels.modelUnavailable}`
: 'Continue'
}
if (selection.provider === 'deepseek-harness') {
return profile
? `DeepSeek Harness · ${profile.name}`
: requestedProfileMissing
? `DeepSeek Harness · ${labels.modelUnavailable}`
: 'DeepSeek Harness'
}
return status
? `${labels.automatic} · ${status.label}`
: labels.automaticSelection
@@ -906,7 +914,7 @@ function getRuntimeSelectionLabel(
function getConfiguredAgentRuntimeSource(
settings: RuntimeSettings,
provider: 'opencode' | 'continue',
provider: 'opencode' | 'continue' | 'deepseek-harness',
labels: {
modelUnavailable: string
selectModel: string
@@ -921,7 +929,12 @@ function getConfiguredAgentRuntimeSource(
(candidate) => candidate.id === selection.profileId
)
: undefined
const runtimeLabel = provider === 'opencode' ? 'OpenCode' : 'Continue'
const runtimeLabel =
provider === 'opencode'
? 'OpenCode'
: provider === 'continue'
? 'Continue'
: 'DeepSeek Harness'
if ('profileId' in selection) {
return {
label: `${runtimeLabel} · ${profile?.name ?? labels.modelUnavailable}`,
@@ -1801,6 +1814,12 @@ function App(): React.JSX.Element {
const continueMenuSelection = runtimeSettings
? getRuntimeSelectionForProvider('continue', runtimeSettings)
: undefined
const deepseekHarnessMenuSelection = runtimeSettings
? getRuntimeSelectionForProvider(
'deepseek-harness',
runtimeSettings
)
: undefined
const openCodeMenuSource = runtimeSettings
? getConfiguredAgentRuntimeSource(
runtimeSettings,
@@ -1815,6 +1834,13 @@ function App(): React.JSX.Element {
configuredRuntimeLabels
)
: undefined
const deepseekHarnessMenuSource = runtimeSettings
? getConfiguredAgentRuntimeSource(
runtimeSettings,
'deepseek-harness',
configuredRuntimeLabels
)
: undefined
useEffect(() => {
if (!runtimeMenuOpen) {
return
@@ -6397,6 +6423,46 @@ function App(): React.JSX.Element {
className="runtime-picker__divider"
role="separator"
/>
<strong role="presentation">
{t('runtime.deepseekHarnessGroup')}
</strong>
{deepseekHarnessMenuSelection &&
deepseekHarnessMenuSource && (
<button
aria-checked={
activeRuntimeSelectionKey ===
agentRuntimeSelectionKey(
deepseekHarnessMenuSelection
)
}
onClick={() =>
void switchRuntime(
deepseekHarnessMenuSelection
)
}
role="menuitemradio"
tabIndex={
activeRuntimeSelectionKey ===
agentRuntimeSelectionKey(
deepseekHarnessMenuSelection
)
? 0
: -1
}
type="button"
>
<span>
{deepseekHarnessMenuSource.label}
</span>
<small>
{deepseekHarnessMenuSource.detail}
</small>
</button>
)}
<div
className="runtime-picker__divider"
role="separator"
/>
<button
onClick={() => {
setRuntimeMenuOpen(false)
@@ -501,6 +501,11 @@ describe('ChannelSettingsSection', () => {
expect(
within(backend).getByRole('option', { name: 'Continue' })
).toBeInTheDocument()
expect(
within(backend).getByRole('option', {
name: 'DeepSeek Harness(预览 · 仅 DeepSeek'
})
).toBeInTheDocument()
fireEvent.change(backend, {
target: {
+16 -3
View File
@@ -196,7 +196,7 @@ function usableChannelModelProfiles(
}
function configuredRuntimeSelection(
provider: 'opencode' | 'continue'
provider: 'opencode' | 'continue' | 'deepseek-harness'
): AgentRuntimeSelection {
return { provider }
}
@@ -231,7 +231,11 @@ function runtimeSelectionDescription(
return t('channels.project.automaticDescription')
}
const runtimeLabel =
selection.provider === 'opencode' ? 'OpenCode' : 'Continue'
selection.provider === 'opencode'
? 'OpenCode'
: selection.provider === 'continue'
? 'Continue'
: 'DeepSeek Harness'
return t('channels.project.runtimeDescription', {
runtime: runtimeLabel
})
@@ -255,6 +259,9 @@ function ChannelProjectControls({
const continueSelection = configuredRuntimeSelection(
'continue'
)
const deepseekHarnessSelection = configuredRuntimeSelection(
'deepseek-harness'
)
const directProfiles = usableChannelModelProfiles(runtimeSettings)
const selectedDirectProfileId =
draft.runtimeSelection.provider === 'model'
@@ -274,7 +281,8 @@ function ChannelProjectControls({
profileId: profile.id
})),
openCodeSelection,
continueSelection
continueSelection,
deepseekHarnessSelection
]
const selectionByKey = new Map(
selections.map((selection) => [
@@ -377,6 +385,11 @@ function ChannelProjectControls({
<option value={agentRuntimeSelectionKey(continueSelection)}>
Continue
</option>
<option
value={agentRuntimeSelectionKey(deepseekHarnessSelection)}
>
{t('channels.project.deepseekHarnessOption')}
</option>
</optgroup>
</select>
<small>
@@ -213,6 +213,21 @@ describe('DocumentParsingSettingsSection', () => {
expect(screen.getByText(label)).toBeInTheDocument()
})
it('localizes built-in OCR model metadata in English', async () => {
await changeUiLocale('en-US')
render(<DocumentParsingSettingsSection />)
expect(await screen.findByText('PP-OCRv6 Tiny')).toBeInTheDocument()
expect(
screen.getByText(
'The official lightweight PaddleOCR Chinese model for local CPU recognition of scanned PDFs and images.'
)
).toBeInTheDocument()
expect(screen.getByText('Chinese / English')).toBeInTheDocument()
expect(screen.queryByText('轻量中文 OCR 模型')).not.toBeInTheDocument()
})
it('localizes recovered document parsing settings warnings', async () => {
await changeUiLocale('en-US')
getSnapshot.mockResolvedValueOnce({
@@ -423,6 +423,16 @@ export function DocumentParsingSettingsSection({
const installedModel = snapshot.ocrModels.installed.find(
(entry) => entry.id === draft.localOcrModelId
)
const modelDisplayName = model
? t(`documentParsing.ocr.catalog.${model.id}.displayName`, {
defaultValue: model.displayName
})
: ''
const modelDescription = model
? t(`documentParsing.ocr.catalog.${model.id}.description`, {
defaultValue: model.description
})
: ''
const modelOperation = snapshot.ocrModels.operations.find(
(operation) => operation.modelId === draft.localOcrModelId
)
@@ -682,9 +692,13 @@ export function DocumentParsingSettingsSection({
const installed = snapshot.ocrModels.installed.some(
(candidate) => candidate.id === entry.id
)
const entryDisplayName = t(
`documentParsing.ocr.catalog.${entry.id}.displayName`,
{ defaultValue: entry.displayName }
)
return (
<option key={entry.id} value={entry.id}>
{entry.displayName} ·{' '}
{entryDisplayName} ·{' '}
{installed
? t('documentParsing.ocr.installedOption')
: t('documentParsing.ocr.downloadableOption')}
@@ -712,18 +726,25 @@ export function DocumentParsingSettingsSection({
<div className="document-ocr-model__header">
<div className="document-ocr-model__summary">
<div className="document-ocr-model__name">
<strong>{model.displayName}</strong>
<strong>{modelDisplayName}</strong>
{model.recommended && (
<span className="speech-model-tag speech-model-tag--recommended">
{t('documentParsing.ocr.recommended')}
</span>
)}
</div>
<p>{model.description}</p>
<p>{modelDescription}</p>
<div className="document-ocr-model__tags">
<span className="speech-model-tag">ModelScope</span>
<span className="speech-model-tag">
{model.languages.join(' / ')}
{model.languages
.map((language) =>
t(
`documentParsing.ocr.languages.${language}`,
{ defaultValue: language }
)
)
.join(' / ')}
</span>
<span className="speech-model-tag">
{model.runtime}
@@ -753,7 +774,7 @@ export function DocumentParsingSettingsSection({
<button
aria-label={t(
'documentParsing.ocr.accessibility.openRepository',
{ name: model.displayName }
{ name: modelDisplayName }
)}
className="secondary-button document-ocr-model__repository"
onClick={() =>
@@ -797,7 +818,7 @@ export function DocumentParsingSettingsSection({
<button
aria-label={t(
'documentParsing.ocr.accessibility.cancelOperation',
{ name: model.displayName }
{ name: modelDisplayName }
)}
className="secondary-button"
onClick={() =>
@@ -815,7 +836,7 @@ export function DocumentParsingSettingsSection({
<button
aria-label={t(
'documentParsing.ocr.accessibility.exportModelZip',
{ name: model.displayName }
{ name: modelDisplayName }
)}
className="secondary-button"
disabled={busyModelId === model.id}
@@ -827,7 +848,7 @@ export function DocumentParsingSettingsSection({
.exportOcrModelArchive(model.id),
t(
'documentParsing.ocr.notifications.exportedZip',
{ name: model.displayName }
{ name: modelDisplayName }
)
)
}
@@ -839,7 +860,7 @@ export function DocumentParsingSettingsSection({
<button
aria-label={t(
'documentParsing.ocr.accessibility.deleteModel',
{ name: model.displayName }
{ name: modelDisplayName }
)}
className={
confirmingRemove === model.id
@@ -861,7 +882,7 @@ export function DocumentParsingSettingsSection({
<button
aria-label={t(
'documentParsing.ocr.accessibility.downloadModel',
{ name: model.displayName }
{ name: modelDisplayName }
)}
className="primary-button"
disabled={busyModelId === model.id}
@@ -882,7 +903,7 @@ export function DocumentParsingSettingsSection({
pendingModelSelection
? 'documentParsing.ocr.notifications.installedAndSelected'
: 'documentParsing.ocr.notifications.installed',
{ name: model.displayName }
{ name: modelDisplayName }
)
)
}
@@ -896,7 +917,7 @@ export function DocumentParsingSettingsSection({
<button
aria-label={t(
'documentParsing.ocr.accessibility.importModelZip',
{ name: model.displayName }
{ name: modelDisplayName }
)}
className="secondary-button"
disabled={busyModelId === model.id}
@@ -917,7 +938,7 @@ export function DocumentParsingSettingsSection({
pendingModelSelection
? 'documentParsing.ocr.notifications.importedAndSelected'
: 'documentParsing.ocr.notifications.importedZip',
{ name: model.displayName }
{ name: modelDisplayName }
)
)
}
@@ -938,7 +959,7 @@ export function DocumentParsingSettingsSection({
<progress
aria-label={t(
'documentParsing.ocr.accessibility.downloadProgress',
{ name: model.displayName }
{ name: modelDisplayName }
)}
max={100}
{...(modelProgress === undefined
+9 -5
View File
@@ -37,7 +37,10 @@ import {
} from './SettingsPrimitives'
import { PageTabs } from './WorkspacePrimitives'
const configurableMcpTargets: RuntimeTarget[] = ['model']
const configurableMcpTargets: RuntimeTarget[] = [
'model',
'deepseek-harness'
]
type McpSettingsTab = 'builtin' | 'computer' | 'custom'
type McpEditor = {
@@ -76,9 +79,9 @@ function editorFromServer(server: McpServerSummary): McpEditor {
description: server.description,
enabled: server.enabled,
allowDynamicTools: server.allowDynamicTools,
assignments: server.assignments.includes('model')
? ['model']
: [],
assignments: server.assignments.filter((target) =>
configurableMcpTargets.includes(target)
),
transport: server.transport,
command: server.transport === 'stdio' ? server.command : '',
args: server.transport === 'stdio' ? server.args.join('\n') : '',
@@ -101,7 +104,8 @@ export function McpSettingsSection({
const runtimeLabels: Record<RuntimeTarget, string> = {
model: t('mcp.runtimeLabels.model'),
opencode: t('mcp.runtimeLabels.opencode'),
continue: t('mcp.runtimeLabels.continue')
continue: t('mcp.runtimeLabels.continue'),
'deepseek-harness': 'DeepSeek Harness'
}
const diagnosticStatusLabels: Record<
CapabilityDiagnosticReport['status'],
+273 -32
View File
@@ -81,6 +81,7 @@ const runtimeSettings: RuntimeSettings = {
kind: 'profile',
profileId: modelProfileId
},
deepseekHarnessModelSource: { kind: 'platform' },
secureStorageAvailable: true,
toolApproval: 'always'
}
@@ -118,11 +119,22 @@ const detectAgentRuntimes = vi.fn<
available: true,
path: 'C:\\Tools\\opencode.exe',
version: '1.2.3',
source: 'automatic',
detail: '通过 PATH 检测'
},
continue: {
available: false,
detail: '未检测到 Continue'
available: true,
path: 'bundled://continue',
version: '1.5.47',
source: 'bundled',
detail: '内置 Continue CLI 1.5.47 已就绪'
},
deepseekHarness: {
available: true,
path: 'bundled://deepseek-harness',
version: '0.1.0-rc.6',
source: 'bundled',
detail: '内置 Harness Adapter 已就绪'
}
}))
const selectRuntimeFile = vi.fn<
@@ -162,10 +174,16 @@ const capabilitySnapshot = {
source: 'builtin' as const,
digest: 'a'.repeat(64),
enabled: true,
assignments: ['model', 'opencode', 'continue'] as (
assignments: [
'model',
'opencode',
'continue',
'deepseek-harness'
] as (
| 'model'
| 'opencode'
| 'continue'
| 'deepseek-harness'
)[]
}
],
@@ -612,8 +630,21 @@ describe('SettingsPanel runtime files', () => {
screen.getByRole('button', { name: 'Save settings' })
).toBeInTheDocument()
expect(
screen.getByText(/OpenCode and Continue are bundled with GoodBuddy/u)
screen.queryByText(/OpenCode and Continue are bundled with GoodBuddy/u)
).not.toBeInTheDocument()
expect(
screen.getByText(
'Automatically detected OpenCode 1.2.3'
)
).toBeInTheDocument()
expect(screen.queryByText('通过 PATH 检测')).not.toBeInTheDocument()
fireEvent.click(screen.getByRole('button', { name: 'Continue' }))
expect(
screen.getByText('Bundled Continue 1.5.47 is ready')
).toBeInTheDocument()
expect(
screen.queryByText('内置 Continue CLI 1.5.47 已就绪')
).not.toBeInTheDocument()
})
it('does not translate user-defined model connection names', async () => {
@@ -933,7 +964,9 @@ describe('SettingsPanel runtime files', () => {
continueConfigPath: '',
workspacePath: 'C:\\Workspace',
opencodeModelSource: runtimeSettings.opencodeModelSource,
continueModelSource: runtimeSettings.continueModelSource
continueModelSource: runtimeSettings.continueModelSource,
deepseekHarnessModelSource:
runtimeSettings.deepseekHarnessModelSource
}
})
render(
@@ -1267,7 +1300,7 @@ describe('SettingsPanel runtime files', () => {
})
it('automatically detects runtimes and displays path, version, and detail', async () => {
it('places Runtime detection details in the semantic overview card', async () => {
render(
<SettingsPanel
{...heartbeatSettingsProps}
@@ -1279,16 +1312,53 @@ describe('SettingsPanel runtime files', () => {
)
expect(detectAgentRuntimes).toHaveBeenCalledOnce()
const runtimeLabel = await screen.findByText('Runtime', {
selector: 'dt'
})
const overview = runtimeLabel.closest<HTMLElement>(
'.runtime-overview'
)
if (!overview) {
throw new Error('Missing OpenCode Runtime overview')
}
expect(
await screen.findByText(
within(overview).getByText('GoodBuddy 内置 OpenCode')
).toBeInTheDocument()
expect(
within(overview).getByText('模型配置:', { selector: 'dt' })
).toBeInTheDocument()
const status = within(overview).getByText('已就绪')
expect(status.tagName).toBe('DD')
expect(status).toHaveAttribute('aria-live', 'polite')
expect(
within(overview).getByText('C:\\Tools\\opencode.exe')
).toHaveClass('runtime-overview__path')
expect(within(overview).getByText('1.2.3')).toBeInTheDocument()
expect(
within(overview).getByText('已自动检测到 OpenCode 1.2.3')
).toBeInTheDocument()
expect(
screen.queryByText(
'已就绪 · C:\\Tools\\opencode.exe · 1.2.3 · 通过 PATH 检测'
)
).toBeInTheDocument()
await screen.findByText(/OpenCode 和 Continue 已随 GoodBuddy 内置/)
).not.toBeInTheDocument()
await screen.findByText('GoodBuddy 内置 OpenCode')
fireEvent.click(screen.getByRole('button', { name: 'Continue' }))
const continueOverview = screen
.getByText('GoodBuddy 内置 Continue')
.closest<HTMLElement>('.runtime-overview')
if (!continueOverview) {
throw new Error('Missing Continue Runtime overview')
}
expect(
screen.getByText('尚未就绪 · 未检测到 Continue')
within(continueOverview).getByText('已就绪')
).toBeInTheDocument()
expect(
within(continueOverview).getByText('1.5.47')
).toBeInTheDocument()
expect(
within(continueOverview).getByText('bundled://continue')
).toHaveClass('runtime-overview__path')
expect(
screen.queryByRole('button', { name: '重新检测 Continue' })
@@ -1302,6 +1372,177 @@ describe('SettingsPanel runtime files', () => {
)
})
it('configures DeepSeek Harness only with Chat Completions or platform settings', async () => {
const harnessProfileId =
'00000000-0000-4000-8000-000000000051'
getRuntime.mockResolvedValueOnce({
...runtimeSettings,
modelProfiles: [
runtimeSettings.modelProfiles[0]!,
{
...runtimeSettings.modelProfiles[0]!,
id: harnessProfileId,
name: 'DeepSeek Chat',
baseUrl: 'https://api.deepseek.com/v1',
modelName: 'deepseek-chat',
protocol: 'openai-chat-completions'
}
],
deepseekHarnessModelSource: {
kind: 'profile',
profileId: harnessProfileId
}
} as unknown as RuntimeSettings)
render(
<SettingsPanel
{...heartbeatSettingsProps}
open
onClearLocalData={vi.fn(async () => {})}
onClose={vi.fn()}
onSaved={vi.fn()}
/>
)
fireEvent.click(
await screen.findByRole('button', {
name: 'DeepSeek Harness(预览)'
})
)
expect(
screen.getByText('开发者预览 · 仅支持 DeepSeek')
).toBeInTheDocument()
expect(
screen.getByText(/当前仅支持 DeepSeek 模型/)
).toBeInTheDocument()
const harnessOverview = screen
.getByText('GoodBuddy 内置 DeepSeek Harness')
.closest<HTMLElement>('.runtime-overview')
if (!harnessOverview) {
throw new Error('Missing DeepSeek Harness overview')
}
expect(
within(harnessOverview).getByText('已就绪')
).toHaveAttribute('aria-live', 'polite')
expect(
within(harnessOverview).getByText(
'bundled://deepseek-harness'
)
).toHaveClass('runtime-overview__path')
expect(
within(harnessOverview).getByText('0.1.0-rc.6')
).toBeInTheDocument()
expect(
within(harnessOverview).getByText(
'内置 DeepSeek Harness 0.1.0-rc.6 已就绪'
)
).toBeInTheDocument()
expect(
screen.queryByText(/自定义 Harness Host/)
).not.toBeInTheDocument()
fireEvent.click(screen.getByText('高级设置'))
expect(
screen.getByText(
/始终使用 GoodBuddy 内置并固定版本的 Host/
)
).toBeInTheDocument()
expect(
screen.queryByRole('button', {
name: /选择.*Harness/
})
).not.toBeInTheDocument()
const source = screen.getByLabelText(
'DeepSeek Harness GoodBuddy 模型连接'
)
expect(
within(source).getByRole('option', { name: '默认模型(不兼容)' })
).toBeDisabled()
expect(
within(source).getByRole('option', { name: 'DeepSeek Chat' })
).not.toBeDisabled()
fireEvent.click(
screen.getByRole('radio', {
name: /使用平台 DeepSeek 环境配置/
})
)
fireEvent.click(screen.getByRole('button', { name: '保存设置' }))
await waitFor(() =>
expect(updateRuntime).toHaveBeenCalledWith(
expect.objectContaining({
deepseekHarnessModelSource: { kind: 'platform' }
})
)
)
})
it('normalizes an environment-managed DeepSeek profile to the platform source when saving', async () => {
getRuntime.mockResolvedValueOnce({
...runtimeSettings,
modelBaseUrl: 'https://api.deepseek.com',
modelName: 'deepseek-chat',
modelProtocol: 'openai-chat-completions',
modelProfiles: [
{
...runtimeSettings.modelProfiles[0]!,
baseUrl: 'https://api.deepseek.com',
modelName: 'deepseek-chat',
protocol: 'openai-chat-completions',
credentialSource: 'environment'
}
],
deepseekHarnessModelSource: { kind: 'platform' },
configured: {
modelProfiles: [
{
...runtimeSettings.modelProfiles[0]!,
baseUrl: 'https://bigtoken.ai',
modelName: 'sonnet-5',
protocol: 'openai-chat-completions',
credentialSource: 'environment'
}
],
opencodeBaseUrl: '',
opencodeBinaryPath: '',
opencodeConfigPath: '',
continueBinaryPath: '',
continueConfigPath: '',
workspacePath: 'C:\\Workspace',
opencodeModelSource: runtimeSettings.opencodeModelSource,
continueModelSource: runtimeSettings.continueModelSource,
deepseekHarnessModelSource: { kind: 'platform' }
}
} as unknown as RuntimeSettings)
render(
<SettingsPanel
{...heartbeatSettingsProps}
open
onClearLocalData={vi.fn(async () => {})}
onClose={vi.fn()}
onSaved={vi.fn()}
/>
)
fireEvent.click(
await screen.findByRole('button', {
name: 'DeepSeek Harness(预览)'
})
)
fireEvent.click(
screen.getByRole('radio', {
name: /使用 GoodBuddy 模型连接/
})
)
fireEvent.click(screen.getByRole('button', { name: '保存设置' }))
await waitFor(() =>
expect(updateRuntime).toHaveBeenCalledWith(
expect.objectContaining({
deepseekHarnessModelSource: { kind: 'platform' }
})
)
)
})
it('selects, warns about, clears, and saves a custom binary', async () => {
render(
<SettingsPanel
@@ -1313,7 +1554,7 @@ describe('SettingsPanel runtime files', () => {
/>
)
await screen.findByText(/OpenCode 和 Continue 已随 GoodBuddy 内置/)
await screen.findByText('GoodBuddy 内置 OpenCode')
fireEvent.click(screen.getByRole('button', { name: 'Continue' }))
fireEvent.click(screen.getByText('高级设置'))
const input = await screen.findByLabelText('Continue 可执行文件路径')
@@ -1362,28 +1603,22 @@ describe('SettingsPanel runtime files', () => {
/>
)
await screen.findByText('GoodBuddy 内置 OpenCode')
expect(
await screen.findByText(
/OpenCode 和 Continue 已随 GoodBuddy 内置/
)
).toBeInTheDocument()
expect(
screen.getByText(/配置可兼容的直连文本模型后即可使用/)
).toBeInTheDocument()
screen.queryByText(/配置可兼容的直连文本模型后即可使用/)
).not.toBeInTheDocument()
expect(screen.getByRole('button', { name: 'OpenCode' }))
.toHaveAttribute('aria-pressed', 'true')
expect(screen.queryByText('默认 Runtime')).not.toBeInTheDocument()
expect(
screen.getByText(/GoodBuddy 内置 OpenCode/)
).toBeInTheDocument()
screen.getAllByText(/GoodBuddy 内置 OpenCode/).length
).toBeGreaterThan(0)
expect(
screen.getByText(/模型配置:/).closest('.runtime-note')
).toHaveTextContent(
'跟随 GoodBuddy · 默认模型(sonnet-5'
)
expect(
screen.getByText(/^ ·/u)
).toBeInTheDocument()
expect(screen.getByText('已就绪')).toBeInTheDocument()
expect(screen.getByText('高级设置').closest('details'))
.not.toHaveAttribute('open')
expect(
@@ -1408,15 +1643,15 @@ describe('SettingsPanel runtime files', () => {
fireEvent.click(screen.getByRole('button', { name: 'Continue' }))
expect(
screen.getByText(/GoodBuddy 内置 Continue/)
).toBeInTheDocument()
screen.getAllByText(/GoodBuddy 内置 Continue/).length
).toBeGreaterThan(0)
expect(
screen.getByText(/模型配置:/).closest('.runtime-note')
).toHaveTextContent(
'跟随 GoodBuddy · 默认模型(sonnet-5'
)
expect(screen.getByText('尚未就绪 · 未检测到 Continue'))
.toBeInTheDocument()
expect(screen.getByText('已就绪')).toBeInTheDocument()
expect(screen.getByText('1.5.47')).toBeInTheDocument()
expect(screen.getByText('高级设置').closest('details'))
.not.toHaveAttribute('open')
})
@@ -1437,7 +1672,7 @@ describe('SettingsPanel runtime files', () => {
/>
)
await screen.findByText(/OpenCode 和 Continue 已随 GoodBuddy 内置/)
await screen.findByText('GoodBuddy 内置 OpenCode')
fireEvent.click(screen.getByRole('button', { name: 'Continue' }))
fireEvent.click(screen.getByText('高级设置'))
expect(
@@ -1497,7 +1732,7 @@ describe('SettingsPanel runtime files', () => {
/>
)
await screen.findByText(/OpenCode 和 Continue 已随 GoodBuddy 内置/)
await screen.findByText('GoodBuddy 内置 OpenCode')
fireEvent.click(screen.getByText('高级设置'))
expect(screen.getByLabelText('OpenCode Server 地址')).toHaveValue('')
fireEvent.click(screen.getByRole('button', { name: '保存设置' }))
@@ -2433,8 +2668,11 @@ describe('SettingsPanel runtime files', () => {
expect(
screen.getByText(/新导入的 Skill 默认启用/)
).toHaveTextContent(
'分配给直连模型、OpenCodeContinue'
'分配给直连模型、OpenCodeContinue 和 DeepSeek Harness'
)
expect(
screen.getByLabelText('DeepSeek Harness')
).toBeChecked()
fireEvent.click(
screen.getByRole('button', { name: '导入 Skill 目录' })
)
@@ -2633,10 +2871,10 @@ describe('SettingsPanel runtime files', () => {
within(mcpTabs).getByRole('tab', { name: '自定义 MCP' })
)
expect(
screen.getByText(/自定义 MCP 当前仅用于直连模型/)
screen.getByText(/自定义 MCP 可分配给直连模型或 DeepSeek Harness/)
).toHaveTextContent('新建时默认分配给直连模型')
expect(
screen.getByText(/Runtime 自有 MCP 配置不在此处管理/)
screen.getByText(/服务凭据不会进入 Harness Utility/)
).toBeInTheDocument()
expect(
await screen.findByText('尚未配置 MCP Server')
@@ -2663,6 +2901,9 @@ describe('SettingsPanel runtime files', () => {
})
).not.toBeChecked()
expect(within(dialog).getByLabelText('模型')).toBeChecked()
expect(
within(dialog).getByLabelText('DeepSeek Harness')
).not.toBeChecked()
expect(
within(dialog).queryByLabelText('OpenCode')
).not.toBeInTheDocument()
+384 -87
View File
@@ -25,11 +25,11 @@ import type {
RuntimeSettingsInput,
RuntimeModelSource
} from '../../shared/contracts'
import type { AgentRuntimeSelection } from '../../shared/runtime-selection-contracts'
import {
defaultModelProfileId as builtInDefaultModelProfileId,
defaultRuntimeSettings,
isAgentRuntimeModelProtocol
isAgentRuntimeModelProtocol,
isDeepSeekHarnessModelProfile
} from '../../shared/contracts'
import { McpSettingsSection } from './McpSettingsSection'
import { RolePromptSettingsSection } from './RolePromptSettingsSection'
@@ -59,7 +59,9 @@ import type {
import { useUiLocale } from './i18n/UiLocaleProvider'
type ModelType = 'llm' | 'embedding' | 'rerank' | 'speech'
type AgentRuntimeType = RuntimeConfigActionInput['runtime']
type AgentRuntimeType =
| RuntimeConfigActionInput['runtime']
| 'deepseek-harness'
type ModelProfileDraft = RuntimeSettings['modelProfiles'][number] & {
supportsImageInput: boolean
apiKey: string
@@ -140,7 +142,9 @@ function configuredRuntimeSettings(
continueConfigPath: settings.continueConfigPath,
workspacePath: settings.workspacePath,
opencodeModelSource: settings.opencodeModelSource,
continueModelSource: settings.continueModelSource
continueModelSource: settings.continueModelSource,
deepseekHarnessModelSource:
settings.deepseekHarnessModelSource
}
}
@@ -158,6 +162,7 @@ function hydrateRuntimeSettings(
defaultModelProfileId: (value: string) => void
opencodeModelSource: (value: RuntimeModelSource) => void
continueModelSource: (value: RuntimeModelSource) => void
deepseekHarnessModelSource: (value: RuntimeModelSource) => void
opencodeBaseUrl: (value: string) => void
opencodeBinaryPath: (value: string) => void
opencodeConfigPath: (value: string) => void
@@ -205,6 +210,9 @@ function hydrateRuntimeSettings(
setters.defaultModelProfileId(value.defaultModelProfileId)
setters.opencodeModelSource(configured.opencodeModelSource)
setters.continueModelSource(configured.continueModelSource)
setters.deepseekHarnessModelSource(
configured.deepseekHarnessModelSource ?? { kind: 'platform' }
)
setters.opencodeBaseUrl(configured.opencodeBaseUrl)
setters.opencodeBinaryPath(configured.opencodeBinaryPath)
setters.opencodeConfigPath(configured.opencodeConfigPath)
@@ -241,7 +249,7 @@ function hydrateRuntimeSettings(
}
type RuntimeConfigCardProps = {
runtime: AgentRuntimeType
runtime: RuntimeConfigActionInput['runtime']
runtimeLabel: string
description: string
fileKind: Extract<
@@ -351,6 +359,74 @@ function RuntimeConfigCard({
)
}
function RuntimeOverviewCard({
detection,
detectionLabel,
detecting,
modelConfiguration,
recommendation,
runtime
}: {
detection: AgentRuntimeDetection['opencode'] | undefined
detectionLabel: string
detecting: boolean
modelConfiguration: string
recommendation: string
runtime: string
}): React.JSX.Element {
const { t } = useTranslation('settings')
const localizedDetail =
detection?.available && detection.source
? t(`runtime.detection.details.${detection.source}`, {
runtime: detectionLabel,
versionSuffix: detection.version ? ` ${detection.version}` : ''
})
: detection?.detail
const status = detecting
? t('runtime.detection.detecting')
: detection?.available
? t('runtime.detection.ready')
: detection
? t('runtime.detection.unavailable')
: t('runtime.detection.notDetected')
return (
<div className="runtime-note runtime-overview">
<dl className="runtime-overview__details">
<dt>{t('runtime.runtimeLabel')}</dt>
<dd>{runtime}</dd>
<dt>{t('runtime.modelConfigurationLabel')}</dt>
<dd>{modelConfiguration}</dd>
<dt>{t('runtime.detection.statusLabel')}</dt>
<dd aria-atomic="true" aria-live="polite">
{status}
</dd>
{!detecting && detection?.available && (
<>
<dt>{t('runtime.detection.pathLabel')}</dt>
<dd className="runtime-overview__path">
{detection.path}
</dd>
{detection.version && (
<>
<dt>{t('runtime.detection.versionLabel')}</dt>
<dd>{detection.version}</dd>
</>
)}
</>
)}
{!detecting && localizedDetail && (
<>
<dt>{t('runtime.detection.detailLabel')}</dt>
<dd>{localizedDetail}</dd>
</>
)}
</dl>
<p>{recommendation}</p>
</div>
)
}
export function SettingsPanel({
open,
presentation = 'modal',
@@ -389,6 +465,8 @@ export function SettingsPanel({
useState<RuntimeModelSource>({ kind: 'platform' })
const [continueModelSource, setContinueModelSource] =
useState<RuntimeModelSource>({ kind: 'platform' })
const [deepseekHarnessModelSource, setDeepseekHarnessModelSource] =
useState<RuntimeModelSource>({ kind: 'platform' })
const [opencodeBaseUrl, setOpencodeBaseUrl] = useState<string>(
defaultRuntimeSettings.opencodeBaseUrl
)
@@ -495,6 +573,7 @@ export function SettingsPanel({
defaultModelProfileId: setDefaultModelProfileId,
opencodeModelSource: setOpencodeModelSource,
continueModelSource: setContinueModelSource,
deepseekHarnessModelSource: setDeepseekHarnessModelSource,
opencodeBaseUrl: setOpencodeBaseUrl,
opencodeBinaryPath: setOpencodeBinaryPath,
opencodeConfigPath: setOpencodeConfigPath,
@@ -714,6 +793,12 @@ 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,
@@ -758,6 +843,8 @@ export function SettingsPanel({
defaultModelProfileId: defaultProfile.id,
opencodeModelSource,
continueModelSource,
deepseekHarnessModelSource:
normalizedDeepseekHarnessModelSource,
toolApproval,
subagentSmartRoutingEnabled
})
@@ -823,8 +910,12 @@ export function SettingsPanel({
const runtimeSource =
agentRuntimeType === 'opencode'
? savedSettings.opencodeModelSource
: savedSettings.continueModelSource
const runtimeSelection: AgentRuntimeSelection =
: agentRuntimeType === 'continue'
? savedSettings.continueModelSource
: savedSettings.deepseekHarnessModelSource ?? {
kind: 'platform'
}
const runtimeSelection =
runtimeSource.kind === 'profile'
? {
provider: agentRuntimeType,
@@ -1017,6 +1108,24 @@ export function SettingsPanel({
) {
setContinueModelSource(runtimeFallback)
}
if (
deepseekHarnessModelSource.kind === 'profile' &&
deepseekHarnessModelSource.profileId === id
) {
const harnessFallback = remaining.find(
(profile) =>
profile.id === defaultModelProfileId &&
profile.protocol === 'openai-chat-completions'
) ?? remaining.find(
(profile) =>
profile.protocol === 'openai-chat-completions'
)
setDeepseekHarnessModelSource(
harnessFallback
? { kind: 'profile', profileId: harnessFallback.id }
: { kind: 'platform' }
)
}
}
const selectDefaultModelProfile = (
@@ -1046,6 +1155,16 @@ export function SettingsPanel({
) {
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 =>
@@ -1061,6 +1180,10 @@ export function SettingsPanel({
profile: ModelProfileDraft
): boolean => isAgentRuntimeModelProtocol(profile.protocol)
const isDeepseekHarnessCompatible = (
profile: ModelProfileDraft
): boolean => isDeepSeekHarnessModelProfile(profile)
const selectedModelProfile =
modelProfiles.find(
(profile) => profile.id === selectedModelProfileId
@@ -1074,16 +1197,27 @@ export function SettingsPanel({
modelProfiles.find((profile) =>
isAgentRuntimeModelProtocol(profile.protocol)
)
const defaultDeepseekHarnessModelProfile =
modelProfiles.find(
(profile) =>
profile.id === defaultModelProfileId &&
isDeepseekHarnessCompatible(profile)
) ??
modelProfiles.find(isDeepseekHarnessCompatible)
const activeRuntimeModelSource =
agentRuntimeType === 'opencode'
? opencodeModelSource
: continueModelSource
: agentRuntimeType === 'continue'
? continueModelSource
: deepseekHarnessModelSource
const activeRuntimeModelProfile =
activeRuntimeModelSource.kind === 'profile'
? modelProfiles.find(
(profile) =>
profile.id === activeRuntimeModelSource.profileId &&
isAgentRuntimeModelProtocol(profile.protocol)
(agentRuntimeType === 'deepseek-harness'
? isDeepseekHarnessCompatible(profile)
: isAgentRuntimeModelProtocol(profile.protocol))
)
: undefined
const savedRoleModelProfiles = (settings?.modelProfiles ?? [])
@@ -1101,32 +1235,6 @@ export function SettingsPanel({
? settings?.defaultModelProfileId
: undefined
const detectionSummary = (
value: AgentRuntimeDetection['opencode'] | undefined
): React.JSX.Element => (
<div className="credential-state" aria-live="polite">
<TerminalSquare size={15} />
<span>
{value
? value.available
? [
t('runtime.detection.ready'),
value.path,
value.version,
value.detail
]
.filter(Boolean)
.join(' · ')
: t('runtime.detection.notReady', {
detail: value.detail
})
: detecting
? t('runtime.detection.detecting')
: t('runtime.detection.notDetected')}
</span>
</div>
)
return (
<div
className={
@@ -1230,7 +1338,9 @@ export function SettingsPanel({
runtime:
agentRuntimeType === 'opencode'
? 'OpenCode'
: 'Continue'
: agentRuntimeType === 'continue'
? 'Continue'
: 'DeepSeek Harness'
})}
</button>
)}
@@ -1402,13 +1512,14 @@ export function SettingsPanel({
onChange={setAgentRuntimeType}
options={[
{ label: 'OpenCode', value: 'opencode' },
{ label: 'Continue', value: 'continue' }
{ label: 'Continue', value: 'continue' },
{
label: t('runtime.deepseekHarness.selectorLabel'),
value: 'deepseek-harness'
}
]}
value={agentRuntimeType}
/>
<small>
{t('runtime.selectorDescription')}
</small>
</div>
{agentRuntimeType === 'opencode' && (
@@ -1420,37 +1531,39 @@ export function SettingsPanel({
<small>{t('runtime.bundledDescription')}</small>
</div>
</div>
<div className="runtime-note">
<strong>{t('runtime.runtimeLabel')}</strong>
{t('runtime.bundledRuntime', { runtime: 'OpenCode' })}
<br />
<strong>{t('runtime.modelConfigurationLabel')}</strong>
{activeRuntimeModelSource.kind === 'platform'
? t('runtime.ownConfiguration', {
runtime: 'OpenCode'
})
: activeRuntimeModelProfile
? t('runtime.followGoodBuddy', {
name: modelProfileDisplayName(
activeRuntimeModelProfile
),
model: activeRuntimeModelProfile.modelName
<RuntimeOverviewCard
detection={detection?.opencode}
detectionLabel="OpenCode"
detecting={detecting}
modelConfiguration={
activeRuntimeModelSource.kind === 'platform'
? t('runtime.ownConfiguration', {
runtime: 'OpenCode'
})
: defaultTextModelProfile
: activeRuntimeModelProfile
? t('runtime.followGoodBuddy', {
name: modelProfileDisplayName(
defaultTextModelProfile
activeRuntimeModelProfile
),
model: defaultTextModelProfile.modelName
model: activeRuntimeModelProfile.modelName
})
: t('runtime.noCompatibleModel')}
<br />
{t('runtime.opencode.recommendation')}
</div>
: defaultTextModelProfile
? t('runtime.followGoodBuddy', {
name: modelProfileDisplayName(
defaultTextModelProfile
),
model: defaultTextModelProfile.modelName
})
: t('runtime.noCompatibleModel')
}
recommendation={t('runtime.opencode.recommendation')}
runtime={t('runtime.bundledRuntime', {
runtime: 'OpenCode'
})}
/>
<div className="runtime-note">
{t('runtime.permissions')}
</div>
{detectionSummary(detection?.opencode)}
<details className="settings-section">
<summary>{t('runtime.advanced')}</summary>
<p className="settings-panel__description">
@@ -1650,37 +1763,39 @@ export function SettingsPanel({
<small>{t('runtime.bundledDescription')}</small>
</div>
</div>
<div className="runtime-note">
<strong>{t('runtime.runtimeLabel')}</strong>
{t('runtime.bundledRuntime', { runtime: 'Continue' })}
<br />
<strong>{t('runtime.modelConfigurationLabel')}</strong>
{activeRuntimeModelSource.kind === 'platform'
? t('runtime.ownConfiguration', {
runtime: 'Continue'
})
: activeRuntimeModelProfile
? t('runtime.followGoodBuddy', {
name: modelProfileDisplayName(
activeRuntimeModelProfile
),
model: activeRuntimeModelProfile.modelName
<RuntimeOverviewCard
detection={detection?.continue}
detectionLabel="Continue"
detecting={detecting}
modelConfiguration={
activeRuntimeModelSource.kind === 'platform'
? t('runtime.ownConfiguration', {
runtime: 'Continue'
})
: defaultTextModelProfile
: activeRuntimeModelProfile
? t('runtime.followGoodBuddy', {
name: modelProfileDisplayName(
defaultTextModelProfile
activeRuntimeModelProfile
),
model: defaultTextModelProfile.modelName
model: activeRuntimeModelProfile.modelName
})
: t('runtime.noCompatibleModel')}
<br />
{t('runtime.continue.recommendation')}
</div>
: defaultTextModelProfile
? t('runtime.followGoodBuddy', {
name: modelProfileDisplayName(
defaultTextModelProfile
),
model: defaultTextModelProfile.modelName
})
: t('runtime.noCompatibleModel')
}
recommendation={t('runtime.continue.recommendation')}
runtime={t('runtime.bundledRuntime', {
runtime: 'Continue'
})}
/>
<div className="runtime-note">
{t('runtime.permissions')}
</div>
{detectionSummary(detection?.continue)}
<details className="settings-section">
<summary>{t('runtime.advanced')}</summary>
<p className="settings-panel__description">
@@ -1845,6 +1960,159 @@ export function SettingsPanel({
</button>
</details>
</div>
)}
{agentRuntimeType === 'deepseek-harness' && (
<div className="settings-section">
<div className="settings-section__title">
<TerminalSquare size={17} />
<div>
<strong>{t('runtime.deepseekHarness.title')}</strong>
<small>
{t('runtime.deepseekHarness.previewDescription')}
</small>
</div>
</div>
<p className="settings-notice">
{t('runtime.deepseekHarness.deepseekOnlyNotice')}
</p>
<RuntimeOverviewCard
detection={detection?.deepseekHarness}
detectionLabel="DeepSeek Harness"
detecting={detecting}
modelConfiguration={
activeRuntimeModelSource.kind === 'platform'
? t('runtime.deepseekHarness.platformSource')
: activeRuntimeModelProfile
? t('runtime.followGoodBuddy', {
name: modelProfileDisplayName(
activeRuntimeModelProfile
),
model: activeRuntimeModelProfile.modelName
})
: t('runtime.noCompatibleModel')
}
recommendation={t(
'runtime.deepseekHarness.description'
)}
runtime={t('runtime.bundledRuntime', {
runtime: 'DeepSeek Harness'
})}
/>
<fieldset className="runtime-source-options">
<legend>{t('runtime.sourceLegend')}</legend>
<label>
<input
checked={
deepseekHarnessModelSource.kind === 'profile'
}
disabled={!defaultDeepseekHarnessModelProfile}
name="deepseek-harness-model-source"
onChange={() => {
if (defaultDeepseekHarnessModelProfile) {
setDeepseekHarnessModelSource({
kind: 'profile',
profileId:
defaultDeepseekHarnessModelProfile.id
})
}
}}
type="radio"
/>
<span>
<strong>
{t(
'runtime.deepseekHarness.goodBuddySource'
)}
</strong>
<small>
{t(
'runtime.deepseekHarness.goodBuddySourceDescription'
)}
</small>
</span>
</label>
<label>
<input
checked={
deepseekHarnessModelSource.kind === 'platform'
}
name="deepseek-harness-model-source"
onChange={() =>
setDeepseekHarnessModelSource({
kind: 'platform'
})
}
type="radio"
/>
<span>
<strong>
{t('runtime.deepseekHarness.platformSource')}
</strong>
<small>
{t(
'runtime.deepseekHarness.platformSourceDescription'
)}
</small>
</span>
</label>
</fieldset>
{deepseekHarnessModelSource.kind === 'profile' && (
<label className="field">
<span>{t('runtime.goodBuddyConnection')}</span>
<select
aria-label={`DeepSeek Harness ${t(
'runtime.goodBuddyConnection'
)}`}
onChange={(event) =>
setDeepseekHarnessModelSource(
parseModelSource(event.target.value)
)
}
value={deepseekHarnessModelSource.profileId}
>
{modelProfiles.map((profile) => (
<option
disabled={
!isDeepseekHarnessCompatible(profile)
}
key={profile.id}
value={profile.id}
>
{modelProfileDisplayName(profile)}
{isDeepseekHarnessCompatible(profile)
? ''
: t('runtime.incompatibleSuffix')}
</option>
))}
</select>
<small>
{t(
'runtime.deepseekHarness.connectionDescription'
)}
</small>
</label>
)}
<details className="settings-section">
<summary>{t('runtime.advanced')}</summary>
<p className="settings-panel__description">
{t(
'runtime.deepseekHarness.advancedDescription'
)}
</p>
<button
className="secondary-button"
disabled={detecting}
onClick={() => void detectRuntimes()}
type="button"
>
{detecting
? t('actions.detecting')
: t('actions.redetectRuntime', {
runtime: 'DeepSeek Harness'
})}
</button>
</details>
</div>
)}
</>
)}
@@ -2078,6 +2346,29 @@ export function SettingsPanel({
) {
setContinueModelSource(runtimeFallback)
}
if (
protocol !== 'openai-chat-completions' &&
deepseekHarnessModelSource.kind ===
'profile' &&
deepseekHarnessModelSource.profileId ===
profile.id
) {
const harnessFallback =
modelProfiles.find(
(candidate) =>
candidate.id !== profile.id &&
candidate.protocol ===
'openai-chat-completions'
)
setDeepseekHarnessModelSource(
harnessFallback
? {
kind: 'profile',
profileId: harnessFallback.id
}
: { kind: 'platform' }
)
}
}
}
value={profile.protocol}
@@ -2246,7 +2537,13 @@ export function SettingsPanel({
: t('model.profile.incompatible'),
openCodeCompatibility: isOpenCodeCompatible(profile)
? t('model.profile.compatible')
: t('model.profile.incompatibleImageProtocol')
: t('model.profile.incompatibleImageProtocol'),
deepseekHarnessCompatibility:
isDeepseekHarnessCompatible(profile)
? t('model.profile.compatible')
: t(
'model.profile.incompatibleHarnessProtocol'
)
})}
</small>
</div>
+8 -1
View File
@@ -154,7 +154,14 @@ export function SkillsSettingsSection(): React.JSX.Element {
</div>
<div className="runtime-assignments">
<small>{t('skills.assignedTo')}</small>
{(['model', 'opencode', 'continue'] as RuntimeTarget[]).map(
{(
[
'model',
'opencode',
'continue',
'deepseek-harness'
] as RuntimeTarget[]
).map(
(target) => (
<label key={target}>
<input
@@ -117,6 +117,8 @@ export const app = {
switching: 'Switching…',
picker: 'Runtime and model',
directModels: 'Direct models',
deepseekHarnessGroup:
'DeepSeek Harness (Developer preview · DeepSeek only)',
manage: 'Manage Runtime and model connections',
errors: {
readStatus: 'Failed to read Agent Runtime status',
@@ -31,6 +31,8 @@ export const integrations = {
unavailableProfile: '{{name}} · {{modelName}} (unavailable)',
missingProfile: 'The previous direct model no longer exists',
noTextModels: 'No text models are available',
deepseekHarnessOption:
'DeepSeek Harness (Preview · DeepSeek only)',
missingSelection:
'The selected direct model no longer exists. Choose another model.',
imageOnlySelection:
@@ -142,7 +144,8 @@ export const integrations = {
runtimeLabels: {
model: 'Model',
opencode: 'OpenCode',
continue: 'Continue'
continue: 'Continue',
'deepseek-harness': 'DeepSeek Harness'
},
diagnosticStatuses: {
available: 'Available',
@@ -171,7 +174,7 @@ export const integrations = {
custom: 'Custom MCP'
},
customNotice:
'Custom MCP currently works only with direct models. New servers are assigned to direct models by default and loaded only in Execute mode. Runtime-owned MCP configuration is not managed here.',
'Custom MCP can be assigned to direct models or DeepSeek Harness. New servers target direct models by default and load only in Execute mode. GoodBuddy proxies Harness tools in the main process, so server credentials never enter the Harness Utility.',
securityNotice:
'Built-in tools are provided by GoodBuddy and are not MCP servers. Custom MCP servers and tools run with the current users permissions, so add only trusted services. Remote access tokens are encrypted in secure system storage, and tool calls still require GoodBuddy approval.',
computer: {
@@ -35,8 +35,10 @@ export const settings = {
},
runtime: {
label: 'Agent Runtime',
navigationDescription: 'OpenCode, Continue, and workspace settings',
description: 'OpenCode, Continue, and workspace settings'
navigationDescription:
'OpenCode, Continue, DeepSeek Harness, and workspace settings',
description:
'OpenCode, Continue, DeepSeek Harness, and workspace settings'
},
security: {
label: 'Security and data',
@@ -155,8 +157,19 @@ export const settings = {
detection: {
ready: 'Ready',
notReady: 'Not ready · {{detail}}',
unavailable: 'Not ready',
detecting: 'Detecting…',
notDetected: 'Not detected'
notDetected: 'Not detected',
statusLabel: 'Status:',
pathLabel: 'Path:',
versionLabel: 'Version:',
detailLabel: 'Detection details:',
details: {
bundled: 'Bundled {{runtime}}{{versionSuffix}} is ready',
configured: 'Custom {{runtime}}{{versionSuffix}} is ready',
automatic:
'Automatically detected {{runtime}}{{versionSuffix}}'
}
},
workspace: {
title: 'Default workspace',
@@ -164,8 +177,6 @@ export const settings = {
'Agents use this location only when the current project has no root folder',
directoryLabel: 'Default workspace folder'
},
selectorDescription:
'OpenCode and Continue are bundled with GoodBuddy. Configure a compatible direct text model to use them.',
bundledDescription:
'Bundled GoodBuddy Runtime that follows the text model connection by default',
runtimeLabel: 'Runtime:',
@@ -222,6 +233,25 @@ export const settings = {
binaryPath: 'Continue executable path',
missingConfigWarning:
'Continue remains unavailable without a configuration file and will not load a remote default model anonymously.'
},
deepseekHarness: {
selectorLabel: 'DeepSeek Harness (Preview)',
title: 'DeepSeek Harness',
previewDescription: 'Developer preview · DeepSeek only',
deepseekOnlyNotice:
'DeepSeek Harness currently supports DeepSeek models only and is not intended for other model providers.',
description:
'GoodBuddy maintains the fixed Host and control protocol internally and uses pinned Harness libraries underneath. Execute tool calls receive automatic one-time authorization, Ask remains read-only, and cancellation and workspace safety boundaries remain in place. It does not integrate with the DSH plugin or marketplace mechanisms.',
goodBuddySource: 'Use a GoodBuddy model connection',
goodBuddySourceDescription:
'Only OpenAI-compatible Chat Completions connections are available. The connection must point to a DeepSeek model.',
platformSource: 'Use platform DeepSeek environment settings',
platformSourceDescription:
'Reads platform-managed DeepSeek settings from the launch environment without exposing credentials to the renderer.',
connectionDescription:
'The internal GoodBuddy Harness Runtime currently accepts only the OpenAI-compatible Chat Completions protocol.',
advancedDescription:
'This Runtime always uses GoodBuddys bundled, version-pinned Host. It does not load external DSH plugins, marketplace packages, user profiles, or custom Hosts.'
}
},
documentParsing: {
@@ -317,6 +347,28 @@ export const settings = {
slow: 'Slow'
}
},
languages: {
: 'Chinese',
: 'English',
'50 种语言': '50 languages'
},
catalog: {
'pp-ocrv6-tiny': {
displayName: 'PP-OCRv6 Tiny',
description:
'The official lightweight PaddleOCR Chinese model for local CPU recognition of scanned PDFs and images.'
},
'pp-ocrv6-small': {
displayName: 'PP-OCRv6 Small',
description:
'The official PaddleOCR 50-language model balancing recognition quality, speed, and local resource use.'
},
'pp-ocrv6-medium': {
displayName: 'PP-OCRv6 Medium',
description:
'The official high-quality PaddleOCR 50-language model with slower recognition, higher memory use, and greater latency.'
}
},
installed: 'Installed and verified',
download: 'Download',
downloadAndSelect: 'Download and enable',
@@ -448,12 +500,14 @@ export const settings = {
imageQualityDescription:
'Used only for OpenAI-compatible image generation requests.',
compatibilitySummary:
'Direct model: {{directCapability}} · Continue: {{continueCompatibility}} · OpenCode: {{openCodeCompatibility}}',
'Direct model: {{directCapability}} · Continue: {{continueCompatibility}} · OpenCode: {{openCodeCompatibility}} · DeepSeek Harness: {{deepseekHarnessCompatibility}}',
textChat: 'Text chat',
compatible: 'Compatible',
incompatible: 'Incompatible',
incompatibleImageProtocol:
'Incompatible (image generation protocol is unsupported)',
incompatibleHarnessProtocol:
'Incompatible (Chat Completions only)',
secureStorageWarning:
'Secure system key storage is unavailable. Use an environment variable to avoid storing an API Key in plaintext.'
},
@@ -251,7 +251,8 @@ export const settingsSections = {
runtimeLabels: {
model: 'Model',
opencode: 'OpenCode',
continue: 'Continue'
continue: 'Continue',
'deepseek-harness': 'DeepSeek Harness'
},
errors: {
readFailed: 'Could not load Skills',
@@ -264,7 +265,7 @@ export const settingsSections = {
},
listLabel: 'Skills list',
notice:
'Skills inject local capability instructions into selected targets without changing the Runtimes own configuration. Newly imported Skills are enabled by default and assigned to the direct model, OpenCode, and Continue.',
'Skills inject local capability instructions into selected targets without changing the Runtimes own configuration. Newly imported Skills are enabled by default and assigned to the direct model, OpenCode, Continue, and DeepSeek Harness.',
loading: 'Loading Skills…',
source: {
builtin: 'Built in',
@@ -113,6 +113,7 @@ export const app = {
switching: '切换中…',
picker: 'Runtime 和模型',
directModels: '直连模型',
deepseekHarnessGroup: 'DeepSeek Harness(开发者预览 · 仅 DeepSeek',
manage: '管理 Runtime 和模型连接',
errors: {
readStatus: 'Agent Runtime 状态读取失败',
@@ -26,6 +26,7 @@ export const integrations = {
unavailableProfile: '{{name}} · {{modelName}}(不可用)',
missingProfile: '原直连模型已不存在',
noTextModels: '暂无可用文本模型',
deepseekHarnessOption: 'DeepSeek Harness(预览 · 仅 DeepSeek',
missingSelection: '所选直连模型已不存在,请重新选择。',
imageOnlySelection:
'所选连接仅支持图片生成,请选择文本模型或 Agent Runtime。',
@@ -132,7 +133,8 @@ export const integrations = {
runtimeLabels: {
model: '模型',
opencode: 'OpenCode',
continue: 'Continue'
continue: 'Continue',
'deepseek-harness': 'DeepSeek Harness'
},
diagnosticStatuses: {
available: '可用',
@@ -158,7 +160,7 @@ export const integrations = {
custom: '自定义 MCP'
},
customNotice:
'自定义 MCP 当前仅用于直连模型,新建时默认分配给直连模型,并仅在 Execute 模式加载。Runtime 自有 MCP 配置不在此处管理。',
'自定义 MCP 可分配给直连模型或 DeepSeek Harness,新建时默认分配给直连模型,并仅在 Execute 模式加载。Harness 工具由 GoodBuddy 主进程代理,服务凭据不会进入 Harness Utility。',
securityNotice:
'内置工具由 GoodBuddy 提供,不属于 MCP Server。自定义 MCP Server 及其工具具有当前用户权限,请仅添加可信服务;远程访问令牌将由系统安全存储加密,工具调用前仍需 GoodBuddy 审批。',
computer: {
@@ -29,8 +29,8 @@ export const settings = {
},
runtime: {
label: 'Agent Runtime',
navigationDescription: 'OpenCode、Continue 与工作区',
description: 'OpenCode、Continue 与工作区'
navigationDescription: 'OpenCode、Continue、DeepSeek Harness 与工作区',
description: 'OpenCode、Continue、DeepSeek Harness 与工作区'
},
security: {
label: '安全与数据',
@@ -141,16 +141,24 @@ export const settings = {
detection: {
ready: '已就绪',
notReady: '尚未就绪 · {{detail}}',
unavailable: '尚未就绪',
detecting: '正在检测…',
notDetected: '尚未检测'
notDetected: '尚未检测',
statusLabel: '状态:',
pathLabel: '路径:',
versionLabel: '版本:',
detailLabel: '检测详情:',
details: {
bundled: '内置 {{runtime}}{{versionSuffix}} 已就绪',
configured: '自定义 {{runtime}}{{versionSuffix}} 已就绪',
automatic: '已自动检测到 {{runtime}}{{versionSuffix}}'
}
},
workspace: {
title: '默认工作区',
description: '当前项目未设置根目录时,Agent 才使用此默认位置',
directoryLabel: '默认工作区目录'
},
selectorDescription:
'OpenCode 和 Continue 已随 GoodBuddy 内置;配置可兼容的直连文本模型后即可使用。',
bundledDescription: 'GoodBuddy 内置 Runtime,默认跟随文本模型连接',
runtimeLabel: 'Runtime',
modelConfigurationLabel: '模型配置:',
@@ -203,6 +211,25 @@ export const settings = {
binaryPath: 'Continue 可执行文件路径',
missingConfigWarning:
'未指定配置文件时 Continue 将保持不可用,不会匿名加载远程默认模型。'
},
deepseekHarness: {
selectorLabel: 'DeepSeek Harness(预览)',
title: 'DeepSeek Harness',
previewDescription: '开发者预览 · 仅支持 DeepSeek',
deepseekOnlyNotice:
'DeepSeek Harness 当前仅支持 DeepSeek 模型,不适用于其他模型提供商。',
description:
'由 GoodBuddy 内部维护固定 Host 与控制协议,复用锁定的 Harness 底层库;Execute 工具调用自动单次授权,Ask 保持只读,并保留取消和工作区安全边界;不接入 DSH 插件或市场机制。',
goodBuddySource: '使用 GoodBuddy 模型连接',
goodBuddySourceDescription:
'只能选择 OpenAI 兼容 Chat Completions 连接;该连接必须指向 DeepSeek 模型。',
platformSource: '使用平台 DeepSeek 环境配置',
platformSourceDescription:
'从启动环境读取平台管理的 DeepSeek 配置,不会在渲染进程中显示凭据。',
connectionDescription:
'GoodBuddy 内部 Harness Runtime 目前仅接受 OpenAI 兼容 Chat Completions 协议。',
advancedDescription:
'该 Runtime 始终使用 GoodBuddy 内置并固定版本的 Host,不加载外部 DSH 插件、市场包、用户 profile 或自定义 Host。'
}
},
documentParsing: {
@@ -287,6 +314,28 @@ export const settings = {
slow: '慢'
}
},
languages: {
: '中文',
: '英语',
'50 种语言': '50 种语言'
},
catalog: {
'pp-ocrv6-tiny': {
displayName: 'PP-OCRv6 Tiny',
description:
'PaddleOCR 官方轻量中文 OCR 模型,适合扫描 PDF 和图片的本地 CPU 识别。'
},
'pp-ocrv6-small': {
displayName: 'PP-OCRv6 Small',
description:
'PaddleOCR 官方 50 语言 OCR 模型,在识别质量、速度和本地资源占用之间取得平衡。'
},
'pp-ocrv6-medium': {
displayName: 'PP-OCRv6 Medium',
description:
'PaddleOCR 官方 50 语言高质量 OCR 模型,识别较慢,并需要更多内存且具有更高延迟。'
}
},
installed: '已安装并校验',
download: '下载',
downloadAndSelect: '下载并启用',
@@ -410,11 +459,13 @@ export const settings = {
},
imageQualityDescription: '仅用于 OpenAI 兼容图像生成请求。',
compatibilitySummary:
'直连模型:{{directCapability}} · Continue{{continueCompatibility}} · OpenCode{{openCodeCompatibility}}',
'直连模型:{{directCapability}} · Continue{{continueCompatibility}} · OpenCode{{openCodeCompatibility}} · DeepSeek Harness{{deepseekHarnessCompatibility}}',
textChat: '文本对话',
compatible: '兼容',
incompatible: '不兼容',
incompatibleImageProtocol: '不兼容(不支持图像生成协议)',
incompatibleHarnessProtocol:
'不兼容(仅支持 Chat Completions',
secureStorageWarning:
'当前系统密钥服务不可用。为了避免明文落盘,请使用环境变量提供 API Key。'
},
@@ -233,7 +233,8 @@ export const settingsSections = {
runtimeLabels: {
model: '模型',
opencode: 'OpenCode',
continue: 'Continue'
continue: 'Continue',
'deepseek-harness': 'DeepSeek Harness'
},
errors: {
readFailed: '读取 Skills 失败',
@@ -246,7 +247,7 @@ export const settingsSections = {
},
listLabel: 'Skills 列表',
notice:
'Skill 以本地能力说明注入所选目标,不会写入 Runtime 自有配置。新导入的 Skill 默认启用,并分配给直连模型、OpenCodeContinue。',
'Skill 以本地能力说明注入所选目标,不会写入 Runtime 自有配置。新导入的 Skill 默认启用,并分配给直连模型、OpenCodeContinue 和 DeepSeek Harness。',
loading: '正在读取 Skills…',
source: {
builtin: '内置',
@@ -0,0 +1,42 @@
import { describe, expect, it } from 'vitest'
import type { RuntimeSettings } from '../../shared/contracts'
import {
getDefaultRuntimeSelection,
getRuntimeSelectionForProvider
} from './runtime-selection'
const harnessProfileId = '00000000-0000-4000-8000-000000000071'
function harnessSettings(
source: { kind: 'platform' } | { kind: 'profile'; profileId: string }
): RuntimeSettings {
return {
provider: 'deepseek-harness',
deepseekHarnessModelSource: source
} as RuntimeSettings
}
describe('DeepSeek Harness runtime selection', () => {
it('uses the configured Chat Completions profile', () => {
const selection = getRuntimeSelectionForProvider(
'deepseek-harness',
harnessSettings({
kind: 'profile',
profileId: harnessProfileId
})
)
expect(selection).toEqual({
provider: 'deepseek-harness',
profileId: harnessProfileId
} satisfies Record<string, string>)
})
it('uses platform settings without a profile id', () => {
const settings = harnessSettings({ kind: 'platform' })
expect(getDefaultRuntimeSelection(settings)).toEqual({
provider: 'deepseek-harness'
})
})
})
+10 -6
View File
@@ -2,7 +2,7 @@ import type { RuntimeSettings } from '../../shared/contracts'
import type { AgentRuntimeSelection } from '../../shared/runtime-selection-contracts'
export function getRuntimeSelectionForProvider(
provider: 'model' | 'opencode' | 'continue',
provider: 'model' | 'opencode' | 'continue' | 'deepseek-harness',
settings: RuntimeSettings
): AgentRuntimeSelection {
if (provider === 'model') {
@@ -14,7 +14,9 @@ export function getRuntimeSelectionForProvider(
const source =
provider === 'opencode'
? settings.opencodeModelSource
: settings.continueModelSource
: provider === 'continue'
? settings.continueModelSource
: settings.deepseekHarnessModelSource ?? { kind: 'platform' }
return {
provider,
...(source.kind === 'profile' ? { profileId: source.profileId } : {})
@@ -24,12 +26,14 @@ export function getRuntimeSelectionForProvider(
export function getDefaultRuntimeSelection(
settings: RuntimeSettings
): AgentRuntimeSelection {
const provider = settings.provider
if (
settings.provider === 'model' ||
settings.provider === 'opencode' ||
settings.provider === 'continue'
provider === 'model' ||
provider === 'opencode' ||
provider === 'continue' ||
provider === 'deepseek-harness'
) {
return getRuntimeSelectionForProvider(settings.provider, settings)
return getRuntimeSelectionForProvider(provider, settings)
}
return settings.opencodeBaseUrl || settings.opencodeEmbedded
? getRuntimeSelectionForProvider('opencode', settings)
+31
View File
@@ -4903,6 +4903,37 @@ button > svg {
font-weight: 650;
}
.runtime-overview {
display: grid;
gap: var(--space-3);
}
.runtime-overview__details {
display: grid;
grid-template-columns: max-content minmax(0, 1fr);
gap: var(--space-1) var(--space-3);
margin: 0;
}
.runtime-overview__details dt {
color: var(--text-primary);
font-weight: 650;
}
.runtime-overview__details dd {
min-width: 0;
margin: 0;
overflow-wrap: anywhere;
}
.runtime-overview__path {
word-break: break-word;
}
.runtime-overview > p {
margin: 0;
}
.model-service-form {
display: grid;
gap: var(--space-3);