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:
@@ -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']
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>{`# 标题
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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…',
|
||||
|
||||
@@ -12,9 +12,11 @@ export const app = {
|
||||
releaseNotes: {
|
||||
eyebrow: '版本更新',
|
||||
title: 'GoodBuddy {{version}} 更新内容',
|
||||
description: '本次版本带来了以下功能更新与问题修复。',
|
||||
description: '查看本次版本的主要更新与使用提示。',
|
||||
highlights: '本次亮点',
|
||||
features: '功能更新',
|
||||
fixes: '问题修复',
|
||||
notices: '使用前请留意',
|
||||
close: '关闭版本更新说明',
|
||||
start: '开始使用',
|
||||
closing: '正在关闭…',
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user