feat: expand multi-runtime workflows for 0.10.0

GoodBuddy previously exposed Runtime capabilities, MCP assignments, plugin controls, context compaction, usage reporting, and task-completion behavior through incomplete or inconsistent paths. This release unifies managed OpenCode, Continue, and DeepSeek Harness controls; adds DSH plugin and image workflows; strengthens MCP and Runtime lifecycle bounds; fixes Windows notification activation; validates cross-architecture packages; and presents the approved bilingual four-section release notes.

The DSH plugin marketplace remains a default-off preview whose trusted third-party code runs with current-user permissions. Ask remains read-only, Execute keeps approval controls, and context compaction may use the selected model without deleting GoodBuddy chat history.

Release note: GoodBuddy 0.10.0 重点完善多 Runtime 工作流,统一 OpenCode、Continue 与 DeepSeek Harness 的能力、MCP、插件和上下文管理,并提升长对话、多会话与任务通知的连贯性。
This commit is contained in:
mesalogo
2026-08-17 12:57:12 +08:00
parent f5ee9b2198
commit cbbe30896e
50 changed files with 4077 additions and 605 deletions
+14 -6
View File
@@ -948,10 +948,14 @@ describe('App', () => {
expect(loading).toHaveAttribute('aria-busy', 'true')
await act(async () => lazyRouteMocks.releaseKnowledgeRoute())
expect(
await screen.findByRole('heading', {
level: 1,
name: '知识库'
})
await screen.findByRole(
'heading',
{
level: 1,
name: '知识库'
},
{ timeout: 3000 }
)
).toBeInTheDocument()
expect(
screen.queryByRole('status', { name: '正在加载页面…' })
@@ -1208,12 +1212,16 @@ describe('App', () => {
releasedAt: '2026-08-11',
notes: {
'zh-CN': {
highlights: ['多 Runtime 工作流更加连贯'],
features: ['新增版本更新说明'],
fixes: ['修复重复显示']
fixes: ['修复重复显示'],
notices: ['Ask 模式保持只读']
},
'en-US': {
highlights: ['Multi-Runtime workflows are more cohesive'],
features: ['Added release notes'],
fixes: ['Fixed repeated display']
fixes: ['Fixed repeated display'],
notices: ['Ask remains read-only']
}
}
}
+19 -1
View File
@@ -15,7 +15,10 @@ import {
vi
} from 'vitest'
import { changeUiLocale } from './i18n'
import { MarkdownRenderer } from './MarkdownRenderer'
import {
InlineMarkdown,
MarkdownRenderer
} from './MarkdownRenderer'
const mermaidMock = vi.hoisted(() => ({
initialize: vi.fn(),
@@ -41,6 +44,21 @@ describe('MarkdownRenderer', () => {
vi.clearAllMocks()
})
it('renders bounded inline emphasis without links or raw HTML', () => {
const { container } = render(
<p>
<InlineMarkdown>
{'**明确标题。** 查看[外部页面](https://example.com)。<script>bad()</script>'}
</InlineMarkdown>
</p>
)
expect(screen.getByText('明确标题。').tagName).toBe('STRONG')
expect(container).toHaveTextContent('外部页面')
expect(container.querySelector('a')).not.toBeInTheDocument()
expect(container.querySelector('script')).not.toBeInTheDocument()
})
it('renders CommonMark and GitHub Flavored Markdown', () => {
render(
<MarkdownRenderer>{`# 标题
+19
View File
@@ -92,6 +92,25 @@ type MarkdownRendererProps = {
children: string
}
const inlineMarkdownComponents: Components = {
p: ({ children }) => <>{children}</>
}
export const InlineMarkdown = memo(function InlineMarkdown({
children
}: MarkdownRendererProps): React.JSX.Element {
return (
<ReactMarkdown
allowedElements={['p', 'strong']}
components={inlineMarkdownComponents}
skipHtml
unwrapDisallowed
>
{children}
</ReactMarkdown>
)
})
const wholeMarkdownFence =
/^```(?:markdown|md)\s*\r?\n([\s\S]*?)\r?\n```$/iu
+43 -8
View File
@@ -19,12 +19,18 @@ const snapshot: ReleaseNotesSnapshot = {
releasedAt: '2026-08-11',
notes: {
'zh-CN': {
features: ['新增双语界面'],
fixes: ['修复开关尺寸']
highlights: ['多 Runtime 工作流更加连贯。'],
features: ['**Runtime 能力概览。** 查看实际可用能力。'],
fixes: ['**设置界面一致性。** 修复开关尺寸。'],
notices: ['**工作模式权限。** Ask 模式保持只读。']
},
'en-US': {
features: ['Added a bilingual interface'],
fixes: ['Fixed switch dimensions']
highlights: ['Multi-Runtime workflows are more cohesive.'],
features: [
'**Runtime capability overview.** View actual capabilities.'
],
fixes: ['**Settings consistency.** Fixed switch dimensions.'],
notices: ['**Work mode permissions.** Ask remains read-only.']
}
}
}
@@ -71,8 +77,26 @@ describe('ReleaseNotesDialog', () => {
name: 'GoodBuddy 0.8.18 更新内容'
})
).toBeInTheDocument()
expect(screen.getByText('新增双语界面')).toBeInTheDocument()
expect(screen.getByText('修复开关尺寸')).toBeInTheDocument()
expect(
screen.getByRole('heading', { name: '本次亮点' })
).toBeInTheDocument()
expect(
screen.getByText('多 Runtime 工作流更加连贯。')
).toBeInTheDocument()
expect(
screen.getByRole('heading', { name: '功能更新' })
).toBeInTheDocument()
expect(screen.getByText('Runtime 能力概览。').tagName).toBe(
'STRONG'
)
expect(screen.getByText('查看实际可用能力。')).toBeInTheDocument()
expect(
screen.getByRole('heading', { name: '问题修复' })
).toBeInTheDocument()
expect(
screen.getByRole('heading', { name: '使用前请留意' })
).toBeInTheDocument()
expect(screen.getByText('工作模式权限。').tagName).toBe('STRONG')
expect(screen.queryByRole('link')).not.toBeInTheDocument()
expect(
container.querySelector<HTMLElement>('.app-shell')?.inert
@@ -115,9 +139,20 @@ describe('ReleaseNotesDialog', () => {
})
).toBeInTheDocument()
expect(
screen.getByText('Added a bilingual interface')
screen.getByRole('heading', { name: 'Highlights' })
).toBeInTheDocument()
expect(screen.getByText('Fixed switch dimensions')).toBeInTheDocument()
expect(
screen.getByText('Multi-Runtime workflows are more cohesive.')
).toBeInTheDocument()
expect(
screen.getByText('Runtime capability overview.').tagName
).toBe('STRONG')
expect(
screen.getByRole('heading', { name: 'Before You Start' })
).toBeInTheDocument()
expect(screen.getByText('Work mode permissions.').tagName).toBe(
'STRONG'
)
expect(
screen.getByRole('button', { name: 'Get Started' })
).toBeInTheDocument()
+75 -28
View File
@@ -1,4 +1,10 @@
import { Sparkles, Wrench, X } from 'lucide-react'
import {
Lightbulb,
Sparkles,
TriangleAlert,
Wrench,
X
} from 'lucide-react'
import { useEffect, useId, useRef, useState } from 'react'
import { createPortal } from 'react-dom'
import { useTranslation } from 'react-i18next'
@@ -8,6 +14,7 @@ import type {
} from '../../shared/release-notes-contracts'
import { activateModalFocus, trapTabFocus } from './dialog-focus'
import type { UiLocale } from './i18n'
import { InlineMarkdown } from './MarkdownRenderer'
type ReleaseNotesDialogProps = {
locale: UiLocale
@@ -16,6 +23,47 @@ type ReleaseNotesDialogProps = {
onClose: () => void
}
function ReleaseNotesSection({
heading,
headingLevel,
icon,
items,
variant
}: {
heading: string
headingLevel: 'h3' | 'h4'
icon: React.ReactNode
items: string[]
variant?: 'notices'
}): React.JSX.Element | null {
if (items.length === 0) {
return null
}
const SectionHeading = headingLevel
return (
<div
className={[
'release-notes-dialog__section',
variant && `release-notes-dialog__section--${variant}`
]
.filter(Boolean)
.join(' ')}
>
<SectionHeading>
{icon}
{heading}
</SectionHeading>
<ul>
{items.map((item) => (
<li key={item}>
<InlineMarkdown>{item}</InlineMarkdown>
</li>
))}
</ul>
</div>
)
}
function ReleaseSection({
locale,
release,
@@ -28,7 +76,7 @@ function ReleaseSection({
const { t } = useTranslation('app')
const notes = release.notes[locale]
const releaseHeadingId = useId()
const SectionHeading = showVersion ? 'h4' : 'h3'
const headingLevel = showVersion ? 'h4' : 'h3'
return (
<section
aria-labelledby={showVersion ? releaseHeadingId : undefined}
@@ -42,32 +90,31 @@ function ReleaseSection({
GoodBuddy {release.version}
</h3>
)}
{notes.features.length > 0 && (
<div className="release-notes-dialog__section">
<SectionHeading>
<Sparkles aria-hidden="true" size={16} />
{t('releaseNotes.features')}
</SectionHeading>
<ul>
{notes.features.map((feature) => (
<li key={feature}>{feature}</li>
))}
</ul>
</div>
)}
{notes.fixes.length > 0 && (
<div className="release-notes-dialog__section">
<SectionHeading>
<Wrench aria-hidden="true" size={16} />
{t('releaseNotes.fixes')}
</SectionHeading>
<ul>
{notes.fixes.map((fix) => (
<li key={fix}>{fix}</li>
))}
</ul>
</div>
)}
<ReleaseNotesSection
heading={t('releaseNotes.highlights')}
headingLevel={headingLevel}
icon={<Lightbulb aria-hidden="true" size={16} />}
items={notes.highlights}
/>
<ReleaseNotesSection
heading={t('releaseNotes.features')}
headingLevel={headingLevel}
icon={<Sparkles aria-hidden="true" size={16} />}
items={notes.features}
/>
<ReleaseNotesSection
heading={t('releaseNotes.fixes')}
headingLevel={headingLevel}
icon={<Wrench aria-hidden="true" size={16} />}
items={notes.fixes}
/>
<ReleaseNotesSection
heading={t('releaseNotes.notices')}
headingLevel={headingLevel}
icon={<TriangleAlert aria-hidden="true" size={16} />}
items={notes.notices}
variant="notices"
/>
</section>
)
}
@@ -659,7 +659,7 @@ export const RuntimeCustomizationSection = forwardRef<
</p>
) : null}
{snapshot ? (
{snapshot && snapshot.inventoryStatus !== 'available' ? (
<NativeInventoryStatus snapshot={snapshot} />
) : null}
+31 -7
View File
@@ -2072,14 +2072,9 @@ describe('SettingsPanel runtime files', () => {
const agent = await screen.findByLabelText('默认 Agent')
expect(agent).toHaveValue('planner')
const nativeStatus = screen.getByRole('status')
expect(nativeStatus).toHaveTextContent('OpenCode 原生能力已就绪')
expect(
Boolean(
nativeStatus.compareDocumentPosition(agent) &
Node.DOCUMENT_POSITION_FOLLOWING
)
).toBe(true)
screen.queryByText('OpenCode 原生能力已就绪')
).not.toBeInTheDocument()
expect(screen.getByText('能力与默认配置')).toBeInTheDocument()
expect(screen.queryByText('Runtime 原生能力')).not.toBeInTheDocument()
expect(screen.queryByText('OpenCode 默认 Agent')).not.toBeInTheDocument()
@@ -2162,6 +2157,35 @@ describe('SettingsPanel runtime files', () => {
)
})
it.each([
['opencode', 'OpenCode 原生能力已就绪'],
[
'continue',
'内置 Continue CLI 已就绪;Rules 与 Prompts 来自原始静态配置;MCP Prompt 仅在 MCPService 运行并连接后可发现,非运行快照不会启动服务器。Continue MCPService 不提供 Resources。'
],
[
'deepseek-harness',
'显示 DeepSeek Harness Host 与插件原生能力;GoodBuddy 分配的 Skill 和 MCP 不在此清单中。'
]
] as const)(
'hides the redundant ready detail for %s',
async (provider, detail) => {
const fallbackSnapshot = await getRuntimeNativeSnapshot({
provider
})
getRuntimeNativeSnapshot.mockResolvedValueOnce({
...fallbackSnapshot,
detail
})
render(<RuntimeCustomizationSection provider={provider} />)
await screen.findByRole('tablist', { name: '能力清单' })
expect(screen.queryByText(detail)).not.toBeInTheDocument()
expect(screen.queryByRole('status')).not.toBeInTheDocument()
}
)
it('distinguishes external OpenCode connectivity from readable native inventory', async () => {
const fallbackSnapshot = await getRuntimeNativeSnapshot({
provider: 'opencode'
@@ -188,6 +188,12 @@ describe('WorkspacePrimitives', () => {
)
})
it('keeps DSH marketplace search text clear of its icon', () => {
expect(stylesheet).toMatch(
/\.field \.runtime-extension-marketplace__search-input > input\s*\{[^}]*padding-left:\s*34px;/u
)
})
it('separates model service fields from credential status', () => {
expect(stylesheet).toMatch(
/\.model-service-form\s*\{[^}]*display:\s*grid;[^}]*gap:\s*var\(--space-3\);/u
+3 -2
View File
@@ -15,10 +15,11 @@ export const app = {
releaseNotes: {
eyebrow: 'VERSION UPDATE',
title: "What's New in GoodBuddy {{version}}",
description:
'This release includes the following features and bug fixes.',
description: 'Review the key changes and usage notes in this release.',
highlights: 'Highlights',
features: 'Features',
fixes: 'Bug Fixes',
notices: 'Before You Start',
close: 'Close release notes',
start: 'Get Started',
closing: 'Closing…',
+3 -1
View File
@@ -12,9 +12,11 @@ export const app = {
releaseNotes: {
eyebrow: '版本更新',
title: 'GoodBuddy {{version}} 更新内容',
description: '本次版本带来了以下功能更新与问题修复。',
description: '查看本次版本的主要更新与使用提示。',
highlights: '本次亮点',
features: '功能更新',
fixes: '问题修复',
notices: '使用前请留意',
close: '关闭版本更新说明',
start: '开始使用',
closing: '正在关闭…',
+4 -41
View File
@@ -1,41 +1,4 @@
import type { RuntimeSettings } from '../../shared/contracts'
import type { AgentRuntimeSelection } from '../../shared/runtime-selection-contracts'
export function getRuntimeSelectionForProvider(
provider: 'model' | 'opencode' | 'continue' | 'deepseek-harness',
settings: RuntimeSettings
): AgentRuntimeSelection {
if (provider === 'model') {
return {
provider,
profileId: settings.defaultModelProfileId
}
}
const source =
provider === 'opencode'
? settings.opencodeModelSource
: provider === 'continue'
? settings.continueModelSource
: settings.deepseekHarnessModelSource ?? { kind: 'platform' }
return {
provider,
...(source.kind === 'profile' ? { profileId: source.profileId } : {})
}
}
export function getDefaultRuntimeSelection(
settings: RuntimeSettings
): AgentRuntimeSelection {
const provider = settings.provider
if (
provider === 'model' ||
provider === 'opencode' ||
provider === 'continue' ||
provider === 'deepseek-harness'
) {
return getRuntimeSelectionForProvider(provider, settings)
}
return settings.opencodeBaseUrl || settings.opencodeEmbedded
? getRuntimeSelectionForProvider('opencode', settings)
: getRuntimeSelectionForProvider('model', settings)
}
export {
getDefaultRuntimeSelection,
getRuntimeSelectionForProvider
} from '../../shared/runtime-selection-contracts'
+11 -6
View File
@@ -4373,6 +4373,16 @@ button > svg {
color: var(--accent);
}
.release-notes-dialog__section li strong {
color: var(--text-primary);
font-weight: 600;
}
.release-notes-dialog__section--notices :is(h3, h4) svg,
.release-notes-dialog__section--notices li::marker {
color: var(--warning);
}
.release-notes-dialog__footer {
display: flex;
align-items: center;
@@ -5496,11 +5506,6 @@ button > svg {
overflow-wrap: anywhere;
}
.runtime-native-inventory__status--available {
border-color: color-mix(in srgb, var(--success) 35%, transparent);
background: var(--success-subtle);
}
.runtime-native-inventory__status--unavailable {
border-color: var(--danger-border);
background: var(--danger-subtle);
@@ -5678,7 +5683,7 @@ button > svg {
transform: translateY(-50%);
}
.runtime-extension-marketplace__search-input > input {
.field .runtime-extension-marketplace__search-input > input {
padding-left: 34px;
}