feat: add DSH plugin marketplace and shared MCP

GoodBuddy could share Skills across runtimes, but custom MCP remained limited and DeepSeek Harness could not manage third-party extensions. The app now provides a default-off DSH npm marketplace with managed installation, configuration, failure isolation, and packaged npm support, while assigned custom MCP is available to managed OpenCode, Continue Agent, and DeepSeek Harness in Execute.

Third-party DSH install scripts, initialization, and tools run with the current user's permissions. Ask remains read-only at dispatch, and turning off the marketplace hides management without disabling installed plugins.

Release note: 新增默认关闭的 DSH 插件市场,并让自定义 MCP 可分配给 OpenCode、Continue 和 DeepSeek Harness;安装第三方插件前会明确提示当前用户权限边界。
This commit is contained in:
mesalogo
2026-08-16 11:47:11 +08:00
parent 9e6f664e06
commit ff61b5f81d
67 changed files with 9337 additions and 443 deletions
+12
View File
@@ -482,6 +482,18 @@ const api: DesktopApi = {
tools: []
}))
},
runtimeExtensions: {
getSnapshot: vi.fn(async () => ({
marketplaceEnabled: false,
catalog: [],
installed: []
})),
apply: vi.fn(async () => ({
marketplaceEnabled: false,
catalog: [],
installed: []
}))
},
context: {
selectFiles: vi.fn(async () => []),
onFileSelectionProgress: vi.fn((listener) => {
@@ -0,0 +1,389 @@
import {
cleanup,
fireEvent,
render,
screen,
waitFor
} from '@testing-library/react'
import { StrictMode } from 'react'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type { DesktopApi } from '../../shared/contracts'
import type {
RuntimeExtensionCatalogEntry,
RuntimeExtensionMarketplaceInstalledState,
RuntimeExtensionMarketplaceSnapshot
} from '../../shared/runtime-extension-contracts'
import { DshMarketplaceSection } from './DshMarketplaceSection'
const greet: RuntimeExtensionCatalogEntry = {
id: 'dsh-plugin-greet',
package: {
name: 'dsh-plugin-greet',
version: '0.1.0'
},
displayName: 'Greet',
description: 'A deterministic greeting tool.',
license: 'MIT'
}
const finder: RuntimeExtensionCatalogEntry = {
id: 'dsh-find-plugin',
package: {
name: 'dsh-find-plugin',
version: '0.3.6'
},
displayName: 'Plugin Finder',
description: 'Find DSH plugins.',
license: 'MIT'
}
const installedGreet: RuntimeExtensionMarketplaceInstalledState = {
id: greet.id,
package: greet.package,
installedAt: '2026-08-16T00:00:00.000Z',
enabled: true,
configuration: {}
}
function marketplaceSnapshot(
installed: RuntimeExtensionMarketplaceInstalledState[] = [],
marketplaceEnabled = true
): RuntimeExtensionMarketplaceSnapshot {
return {
marketplaceEnabled,
catalog: [greet, finder],
installed
}
}
let getSnapshot: ReturnType<typeof vi.fn>
let apply: ReturnType<typeof vi.fn>
beforeEach(() => {
getSnapshot = vi.fn(async () => marketplaceSnapshot())
apply = vi.fn(async () => marketplaceSnapshot([installedGreet]))
Object.defineProperty(window, 'goodbuddy', {
configurable: true,
value: {
runtimeExtensions: {
getSnapshot,
apply
}
} as unknown as DesktopApi
})
})
afterEach(() => {
cleanup()
vi.restoreAllMocks()
})
describe('DshMarketplaceSection', () => {
it('starts off, loads no catalog UI, and can be enabled explicitly', async () => {
const disabledSnapshot = {
...marketplaceSnapshot([], false),
catalog: []
}
getSnapshot
.mockResolvedValueOnce(disabledSnapshot)
.mockResolvedValueOnce(marketplaceSnapshot())
apply
.mockResolvedValueOnce(marketplaceSnapshot())
.mockResolvedValueOnce(disabledSnapshot)
render(<DshMarketplaceSection onNotify={vi.fn()} />)
const marketplaceSwitch = await screen.findByRole('switch', {
name: '启用 DSH 插件市场'
})
expect(marketplaceSwitch).not.toBeChecked()
expect(
screen.getByText(/插件市场默认关闭/)
).toBeInTheDocument()
expect(screen.queryByRole('searchbox')).not.toBeInTheDocument()
fireEvent.click(marketplaceSwitch)
await waitFor(() =>
expect(apply).toHaveBeenCalledWith({
type: 'set-marketplace-enabled',
enabled: true
})
)
expect(await screen.findByRole('searchbox')).toBeInTheDocument()
expect(getSnapshot).toHaveBeenCalledTimes(2)
fireEvent.click(
screen.getByRole('switch', {
name: '启用 DSH 插件市场'
})
)
await waitFor(() =>
expect(apply).toHaveBeenLastCalledWith({
type: 'set-marketplace-enabled',
enabled: false
})
)
expect(screen.queryByRole('searchbox')).not.toBeInTheDocument()
expect(
screen.getByText(/不会停用或卸载已有插件/)
).toBeInTheDocument()
})
it('loads the catalog, filters locally, and shows startup failures', async () => {
getSnapshot.mockResolvedValueOnce(
marketplaceSnapshot([
{
...installedGreet,
enabled: false,
lastError: 'Extension failed to start.'
}
])
)
render(<DshMarketplaceSection onNotify={vi.fn()} />)
expect(
await screen.findByRole('heading', {
name: 'DSH 插件市场'
})
).toBeInTheDocument()
expect(
screen.getByText(/第三方插件的安装脚本、初始化代码及工具均以当前用户权限运行/)
).toBeInTheDocument()
expect(screen.getByText('Greet')).toBeInTheDocument()
expect(screen.getByText('Plugin Finder')).toBeInTheDocument()
expect(
screen.getByText(/插件上次启动失败,已自动停用/)
).toBeInTheDocument()
fireEvent.change(screen.getByRole('searchbox'), {
target: { value: 'finder' }
})
expect(screen.queryByText('Greet')).not.toBeInTheDocument()
expect(screen.getByText('Plugin Finder')).toBeInTheDocument()
expect(getSnapshot).toHaveBeenCalledOnce()
})
it('requires explicit current-user permission confirmation before install', async () => {
const onNotify = vi.fn()
render(<DshMarketplaceSection onNotify={onNotify} />)
await screen.findByText('Greet')
fireEvent.click(
screen.getAllByRole('button', {
name: '安装并启用'
})[0]!
)
const confirm = screen.getByRole('button', {
name: '确认安装'
})
expect(confirm).toBeDisabled()
expect(
screen.getByText(/npm 会运行该包及其依赖声明的安装脚本/)
).toBeInTheDocument()
fireEvent.click(
screen.getByLabelText(
'我信任 dsh-plugin-greet@0.1.0,并了解其代码将以当前用户权限运行。'
)
)
fireEvent.click(
screen.getByRole('button', {
name: '刷新 DSH 插件市场'
})
)
await waitFor(() => expect(getSnapshot).toHaveBeenCalledTimes(2))
expect(
screen.queryByRole('button', {
name: '确认安装'
})
).not.toBeInTheDocument()
fireEvent.click(
screen.getAllByRole('button', {
name: '安装并启用'
})[0]!
)
const refreshedConfirm = screen.getByRole('button', {
name: '确认安装'
})
expect(refreshedConfirm).toBeDisabled()
fireEvent.click(
screen.getByLabelText(
'我信任 dsh-plugin-greet@0.1.0,并了解其代码将以当前用户权限运行。'
)
)
fireEvent.click(refreshedConfirm)
await waitFor(() =>
expect(apply).toHaveBeenCalledWith({
type: 'install',
extensionId: greet.id,
package: greet.package
})
)
expect(onNotify).toHaveBeenCalledWith(
expect.objectContaining({
tone: 'success',
message: '已安装并启用 Greet'
})
)
})
it('toggles, configures, and removes installed plugins', async () => {
getSnapshot.mockResolvedValueOnce(
marketplaceSnapshot([installedGreet])
)
apply.mockImplementation(async (action) => {
if (action.type === 'remove') {
return marketplaceSnapshot()
}
return marketplaceSnapshot([
{
...installedGreet,
enabled:
action.type === 'set-enabled'
? action.enabled
: installedGreet.enabled,
configuration:
action.type === 'configure'
? action.configuration
: installedGreet.configuration
}
])
})
render(<DshMarketplaceSection onNotify={vi.fn()} />)
const toggle = await screen.findByRole('switch', {
name: '启用 Greet'
})
fireEvent.click(toggle)
await waitFor(() =>
expect(apply).toHaveBeenCalledWith({
type: 'set-enabled',
extensionId: greet.id,
enabled: false
})
)
fireEvent.click(screen.getByRole('button', { name: '配置' }))
const editor = screen.getByRole('textbox', {
name: 'Greet 配置 JSON'
})
fireEvent.change(editor, { target: { value: '[]' } })
fireEvent.click(
screen.getByRole('button', { name: '保存配置' })
)
expect(
screen.getByText('配置必须是有效的 JSON 对象。')
).toBeInTheDocument()
fireEvent.change(editor, {
target: { value: '{"salutation":"你好"}' }
})
fireEvent.click(
screen.getByRole('button', { name: '保存配置' })
)
await waitFor(() =>
expect(apply).toHaveBeenCalledWith({
type: 'configure',
extensionId: greet.id,
configuration: { salutation: '你好' }
})
)
fireEvent.click(
screen.getByRole('button', { name: '移除 Greet' })
)
expect(
screen.getByRole('alertdialog', { name: '移除 Greet' })
).toHaveAccessibleDescription(
'移除 Greet 及其由 GoodBuddy 托管的文件?'
)
fireEvent.click(
screen.getByRole('button', { name: '移除 Greet' })
)
await waitFor(() =>
expect(apply).toHaveBeenCalledWith({
type: 'remove',
extensionId: greet.id
})
)
})
it('keeps actions responsive through the Strict Mode effect cycle', async () => {
const onNotify = vi.fn()
getSnapshot.mockResolvedValue(
marketplaceSnapshot([installedGreet])
)
apply.mockResolvedValue(
marketplaceSnapshot([
{ ...installedGreet, enabled: false }
])
)
render(
<StrictMode>
<DshMarketplaceSection onNotify={onNotify} />
</StrictMode>
)
fireEvent.click(
await screen.findByRole('switch', {
name: '启用 Greet'
})
)
await waitFor(() =>
expect(onNotify).toHaveBeenCalledWith(
expect.objectContaining({
tone: 'success'
})
)
)
expect(
screen.getByRole('switch', { name: '启用 Greet' })
).not.toBeDisabled()
})
it('keeps a failed catalog load recoverable', async () => {
getSnapshot
.mockRejectedValueOnce(new Error('npm registry unavailable'))
.mockResolvedValueOnce(marketplaceSnapshot())
render(<DshMarketplaceSection onNotify={vi.fn()} />)
expect(
await screen.findByRole('alert')
).toHaveTextContent('npm registry unavailable')
fireEvent.click(screen.getByRole('button', { name: '重试' }))
expect(await screen.findByText('Greet')).toBeInTheDocument()
expect(getSnapshot).toHaveBeenCalledTimes(2)
})
it('keeps installed plugins manageable when npm catalog refresh fails', async () => {
getSnapshot.mockResolvedValueOnce({
marketplaceEnabled: true,
catalog: [],
installed: [installedGreet],
catalogError: 'npm registry unavailable'
})
render(<DshMarketplaceSection onNotify={vi.fn()} />)
expect(
await screen.findByRole('alert')
).toHaveTextContent(
'无法刷新 npm 插件目录:npm registry unavailable。已安装插件仍可管理。'
)
expect(
screen.getByRole('switch', { name: '启用 dsh-plugin-greet' })
).toBeChecked()
expect(
screen.getByRole('button', {
name: '移除 dsh-plugin-greet'
})
).toBeEnabled()
})
})
+824
View File
@@ -0,0 +1,824 @@
import {
Package,
RefreshCw,
Search,
Settings2,
Trash2
} from 'lucide-react'
import {
useCallback,
useEffect,
useMemo,
useRef,
useState
} from 'react'
import { useTranslation } from 'react-i18next'
import {
legacyRuntimeExtensionStartupFailure,
runtimeExtensionConfigurationSchema,
runtimeExtensionStartupFailureCode,
type RuntimeExtensionAction,
type RuntimeExtensionCatalogEntry,
type RuntimeExtensionConfiguration,
type RuntimeExtensionMarketplaceInstalledState,
type RuntimeExtensionMarketplaceSnapshot
} from '../../shared/runtime-extension-contracts'
import type { AppNotificationInput } from './notifications'
import { DestructiveConfirmActions } from './WorkspacePrimitives'
const maximumVisibleEntries = 40
type DshMarketplaceSectionProps = {
onNotify: (notification: AppNotificationInput) => void
}
function packagesMatch(
catalog: RuntimeExtensionCatalogEntry,
installed: RuntimeExtensionMarketplaceInstalledState
): boolean {
return (
catalog.package.name === installed.package.name &&
catalog.package.version === installed.package.version
)
}
function packageLabel(entry: RuntimeExtensionCatalogEntry): string {
return `${entry.package.name}@${entry.package.version}`
}
function installConfirmationIdentity(
entry: RuntimeExtensionCatalogEntry
): string {
return `${entry.id}:${packageLabel(entry)}`
}
function actionIdentity(action: RuntimeExtensionAction): string {
return action.type === 'set-marketplace-enabled'
? 'marketplace'
: action.extensionId
}
export function DshMarketplaceSection({
onNotify
}: DshMarketplaceSectionProps): React.JSX.Element {
const { t } = useTranslation('settings')
const [snapshot, setSnapshot] =
useState<RuntimeExtensionMarketplaceSnapshot>()
const [loading, setLoading] = useState(true)
const [loadError, setLoadError] = useState<string>()
const [busy, setBusy] = useState<string>()
const [query, setQuery] = useState('')
const [confirmingInstall, setConfirmingInstall] = useState<string>()
const [installConfirmed, setInstallConfirmed] = useState(false)
const [confirmingRemove, setConfirmingRemove] = useState<string>()
const [configuring, setConfiguring] = useState<string>()
const [configurationDraft, setConfigurationDraft] = useState('')
const [configurationError, setConfigurationError] = useState<string>()
const mountedRef = useRef(true)
const installConfirmationRef = useRef<HTMLInputElement>(null)
useEffect(() => {
mountedRef.current = true
return () => {
mountedRef.current = false
}
}, [])
const readSnapshot = useCallback((): Promise<RuntimeExtensionMarketplaceSnapshot> => {
const api = window.goodbuddy.runtimeExtensions
return api
? api.getSnapshot()
: Promise.reject(
new Error(
t(
'runtime.deepseekHarness.marketplace.errors.unavailable'
)
)
)
}, [t])
const load = useCallback(async (): Promise<void> => {
setConfirmingInstall(undefined)
setInstallConfirmed(false)
setLoading(true)
setLoadError(undefined)
try {
const next = await readSnapshot()
if (mountedRef.current) {
setSnapshot(next)
}
} catch (reason) {
if (mountedRef.current) {
setLoadError(
reason instanceof Error
? reason.message
: t('runtime.deepseekHarness.marketplace.errors.readFailed')
)
}
} finally {
if (mountedRef.current) {
setLoading(false)
}
}
}, [readSnapshot, t])
useEffect(() => {
let active = true
void readSnapshot()
.then((next) => {
if (active) {
setSnapshot(next)
}
})
.catch((reason: unknown) => {
if (active) {
setLoadError(
reason instanceof Error
? reason.message
: t(
'runtime.deepseekHarness.marketplace.errors.readFailed'
)
)
}
})
.finally(() => {
if (active) {
setLoading(false)
}
})
return () => {
active = false
}
}, [readSnapshot, t])
useEffect(() => {
if (confirmingInstall) {
installConfirmationRef.current?.focus()
}
}, [confirmingInstall])
const apply = async (
key: string,
action: RuntimeExtensionAction,
successMessage: string
): Promise<boolean> => {
const api = window.goodbuddy.runtimeExtensions
if (!api) {
onNotify({
tone: 'error',
message: t(
'runtime.deepseekHarness.marketplace.errors.unavailable'
),
dedupeKey: 'dsh-marketplace-unavailable'
})
return false
}
setBusy(key)
try {
const next = await api.apply(action)
if (mountedRef.current) {
setSnapshot(next)
onNotify({
tone: 'success',
message: successMessage,
dedupeKey: `dsh-marketplace-${action.type}-${actionIdentity(action)}`
})
}
return true
} catch (reason) {
if (mountedRef.current) {
onNotify({
tone: 'error',
message:
reason instanceof Error
? reason.message
: t(
'runtime.deepseekHarness.marketplace.errors.operationFailed'
),
dedupeKey: `dsh-marketplace-error-${action.type}-${actionIdentity(action)}`
})
}
return false
} finally {
if (mountedRef.current) {
setBusy(undefined)
}
}
}
const setMarketplaceEnabled = async (
enabled: boolean
): Promise<void> => {
if (!enabled) {
setConfirmingInstall(undefined)
setInstallConfirmed(false)
}
const changed = await apply(
'marketplace',
{
type: 'set-marketplace-enabled',
enabled
},
t(
enabled
? 'runtime.deepseekHarness.marketplace.notifications.marketplaceEnabled'
: 'runtime.deepseekHarness.marketplace.notifications.marketplaceDisabled'
)
)
if (changed && enabled && mountedRef.current) {
await load()
}
}
const installedById = useMemo(
() =>
new Map(
(snapshot?.installed ?? []).map((extension) => [
extension.id,
extension
])
),
[snapshot]
)
const entries = useMemo(() => {
if (!snapshot) {
return []
}
const knownIds = new Set(snapshot.catalog.map((entry) => entry.id))
const installedWithoutCatalog = snapshot.installed
.filter((extension) => !knownIds.has(extension.id))
.map<RuntimeExtensionCatalogEntry>((extension) => ({
id: extension.id,
package: extension.package,
displayName: extension.package.name,
description: t(
'runtime.deepseekHarness.marketplace.notInCatalog'
)
}))
const normalizedQuery = query.trim().toLocaleLowerCase()
return [...snapshot.catalog, ...installedWithoutCatalog]
.filter((entry) => {
if (!normalizedQuery) {
return true
}
return [
entry.displayName,
entry.description,
entry.package.name,
entry.license ?? ''
].some((value) =>
value.toLocaleLowerCase().includes(normalizedQuery)
)
})
.sort((left, right) => {
const leftInstalled = installedById.has(left.id) ? 0 : 1
const rightInstalled = installedById.has(right.id) ? 0 : 1
return (
leftInstalled - rightInstalled ||
left.displayName.localeCompare(right.displayName)
)
})
}, [installedById, query, snapshot, t])
const visibleEntries = entries.slice(0, maximumVisibleEntries)
const beginConfiguration = (
extension: RuntimeExtensionMarketplaceInstalledState
): void => {
setConfiguring(extension.id)
setConfigurationDraft(
JSON.stringify(extension.configuration, null, 2)
)
setConfigurationError(undefined)
}
const saveConfiguration = async (
extension: RuntimeExtensionMarketplaceInstalledState
): Promise<void> => {
let configuration: RuntimeExtensionConfiguration
try {
const parsed = runtimeExtensionConfigurationSchema.safeParse(
JSON.parse(configurationDraft) as unknown
)
if (!parsed.success) {
throw new Error('not-an-object')
}
configuration = parsed.data
} catch {
setConfigurationError(
t(
'runtime.deepseekHarness.marketplace.configuration.invalid'
)
)
return
}
setConfigurationError(undefined)
const saved = await apply(
`configure:${extension.id}`,
{
type: 'configure',
extensionId: extension.id,
configuration
},
t('runtime.deepseekHarness.marketplace.notifications.configured', {
name: extension.package.name
})
)
if (saved && mountedRef.current) {
setConfiguring(undefined)
}
}
return (
<section
aria-labelledby="dsh-marketplace-heading"
className="settings-section runtime-extension-marketplace"
>
<div className="settings-section__title settings-section__title--actions">
<Package aria-hidden="true" size={17} />
<div>
<strong
aria-level={3}
id="dsh-marketplace-heading"
role="heading"
>
{t('runtime.deepseekHarness.marketplace.title')}
</strong>
<small>
{t('runtime.deepseekHarness.marketplace.previewDescription')}
</small>
</div>
<span className="runtime-extension-marketplace__header-actions">
<label className="toggle-row runtime-extension-marketplace__master-toggle">
<span>
{t(
snapshot?.marketplaceEnabled
? 'runtime.deepseekHarness.marketplace.switch.enabled'
: 'runtime.deepseekHarness.marketplace.switch.disabled'
)}
</span>
<input
aria-label={t(
'runtime.deepseekHarness.marketplace.switch.aria'
)}
checked={snapshot?.marketplaceEnabled ?? false}
disabled={!snapshot || loading || Boolean(busy)}
onChange={(event) =>
void setMarketplaceEnabled(event.target.checked)
}
role="switch"
type="checkbox"
/>
</label>
{snapshot?.marketplaceEnabled && (
<button
aria-label={t(
'runtime.deepseekHarness.marketplace.refreshAria'
)}
className="secondary-button"
disabled={loading || Boolean(busy)}
onClick={() => void load()}
type="button"
>
<RefreshCw aria-hidden="true" size={13} />
{t('runtime.deepseekHarness.marketplace.refresh')}
</button>
)}
</span>
</div>
{loadError && (
<div className="runtime-extension-marketplace__load-error">
<p className="settings-warning" role="alert">
{loadError}
</p>
<button
className="secondary-button"
disabled={loading}
onClick={() => void load()}
type="button"
>
{t('runtime.deepseekHarness.marketplace.retry')}
</button>
</div>
)}
{snapshot && !snapshot.marketplaceEnabled && (
<p className="settings-notice">
{t('runtime.deepseekHarness.marketplace.disabledDescription')}
</p>
)}
{snapshot?.marketplaceEnabled && (
<>
<p className="settings-warning">
{t(
'runtime.deepseekHarness.marketplace.permissionNotice'
)}
</p>
<label className="field runtime-extension-marketplace__search">
<span>
{t(
'runtime.deepseekHarness.marketplace.searchLabel'
)}
</span>
<span className="runtime-extension-marketplace__search-input">
<Search aria-hidden="true" size={14} />
<input
onChange={(event) =>
setQuery(event.currentTarget.value)
}
placeholder={t(
'runtime.deepseekHarness.marketplace.searchPlaceholder'
)}
type="search"
value={query}
/>
</span>
</label>
{snapshot.catalogError && (
<div className="runtime-extension-marketplace__load-error">
<p className="settings-warning" role="alert">
{t(
'runtime.deepseekHarness.marketplace.catalogUnavailable',
{ detail: snapshot.catalogError }
)}
</p>
<button
className="secondary-button"
disabled={loading || Boolean(busy)}
onClick={() => void load()}
type="button"
>
{t('runtime.deepseekHarness.marketplace.retry')}
</button>
</div>
)}
<p className="settings-notice" role="status">
{t('runtime.deepseekHarness.marketplace.results', {
shown: visibleEntries.length,
total: entries.length
})}
</p>
{entries.length === 0 ? (
<p className="settings-empty">
{query.trim()
? t('runtime.deepseekHarness.marketplace.noResults')
: t('runtime.deepseekHarness.marketplace.empty')}
</p>
) : (
<div className="runtime-extension-marketplace__list">
{visibleEntries.map((entry) => {
const installed = installedById.get(entry.id)
const updateAvailable =
Boolean(installed) &&
!packagesMatch(entry, installed!)
const installPanelOpen =
confirmingInstall ===
installConfirmationIdentity(entry)
const configurationOpen = configuring === entry.id
const installBusy = busy === `install:${entry.id}`
return (
<article
className="runtime-extension-card"
key={entry.id}
>
<header className="runtime-extension-card__header">
<div>
<strong>{entry.displayName}</strong>
<code>{packageLabel(entry)}</code>
</div>
<div className="runtime-extension-card__tags">
{installed && (
<span>
{t(
'runtime.deepseekHarness.marketplace.installed'
)}
</span>
)}
{entry.license && <span>{entry.license}</span>}
</div>
</header>
<p>{entry.description}</p>
{installed?.lastError && (
<p className="settings-warning" role="alert">
{installed.lastError ===
runtimeExtensionStartupFailureCode ||
installed.lastError ===
legacyRuntimeExtensionStartupFailure
? t(
'runtime.deepseekHarness.marketplace.startupFailure'
)
: installed.lastError}
</p>
)}
<div className="runtime-extension-card__actions">
{installed && (
<>
<label className="toggle-row runtime-extension-card__toggle">
<span>
{installed.enabled
? t(
'runtime.deepseekHarness.marketplace.enabled'
)
: t(
'runtime.deepseekHarness.marketplace.disabled'
)}
</span>
<input
aria-label={t(
'runtime.deepseekHarness.marketplace.enableAria',
{ name: entry.displayName }
)}
checked={installed.enabled}
disabled={Boolean(busy)}
onChange={(event) => {
const enabled = event.target.checked
void apply(
`toggle:${entry.id}`,
{
type: 'set-enabled',
extensionId: entry.id,
enabled
},
t(
enabled
? 'runtime.deepseekHarness.marketplace.notifications.enabled'
: 'runtime.deepseekHarness.marketplace.notifications.disabled',
{ name: entry.displayName }
)
)
}}
role="switch"
type="checkbox"
/>
</label>
<button
className="secondary-button"
disabled={Boolean(busy)}
onClick={() =>
configurationOpen
? setConfiguring(undefined)
: beginConfiguration(installed)
}
type="button"
>
<Settings2 aria-hidden="true" size={13} />
{configurationOpen
? t(
'runtime.deepseekHarness.marketplace.configuration.close'
)
: t(
'runtime.deepseekHarness.marketplace.configuration.open'
)}
</button>
<DestructiveConfirmActions
confirmAriaLabel={t(
'runtime.deepseekHarness.marketplace.removeAria',
{ name: entry.displayName }
)}
confirmLabel={t(
'runtime.deepseekHarness.marketplace.confirmRemove'
)}
confirming={
confirmingRemove === entry.id
}
disabled={Boolean(busy)}
icon={<Trash2 size={13} />}
message={t(
'runtime.deepseekHarness.marketplace.removeMessage',
{ name: entry.displayName }
)}
onCancel={() =>
setConfirmingRemove(undefined)
}
onConfirm={() => {
void apply(
`remove:${entry.id}`,
{
type: 'remove',
extensionId: entry.id
},
t(
'runtime.deepseekHarness.marketplace.notifications.removed',
{ name: entry.displayName }
)
).then((removed) => {
if (removed && mountedRef.current) {
setConfirmingRemove(undefined)
setConfiguring((current) =>
current === entry.id
? undefined
: current
)
}
})
}}
onRequestConfirm={() =>
setConfirmingRemove(entry.id)
}
triggerAriaLabel={t(
'runtime.deepseekHarness.marketplace.removeAria',
{ name: entry.displayName }
)}
triggerLabel={t(
'runtime.deepseekHarness.marketplace.remove'
)}
/>
</>
)}
{(!installed || updateAvailable) && (
<button
className="primary-button"
disabled={Boolean(busy)}
onClick={() => {
setConfirmingInstall(
installConfirmationIdentity(entry)
)
setInstallConfirmed(false)
}}
type="button"
>
{updateAvailable
? t(
'runtime.deepseekHarness.marketplace.update',
{ version: entry.package.version }
)
: t(
'runtime.deepseekHarness.marketplace.install'
)}
</button>
)}
</div>
{installPanelOpen && (
<fieldset className="runtime-extension-install-confirmation">
<legend>
{t(
'runtime.deepseekHarness.marketplace.installConfirmationTitle',
{ name: entry.displayName }
)}
</legend>
<p>
{t(
'runtime.deepseekHarness.marketplace.installConfirmation'
)}
</p>
<label>
<input
checked={installConfirmed}
disabled={installBusy}
onChange={(event) =>
setInstallConfirmed(event.target.checked)
}
ref={installConfirmationRef}
type="checkbox"
/>
<span>
{t(
'runtime.deepseekHarness.marketplace.trustConfirmation',
{ package: packageLabel(entry) }
)}
</span>
</label>
<div>
<button
className="secondary-button"
disabled={installBusy}
onClick={() => {
setConfirmingInstall(undefined)
setInstallConfirmed(false)
}}
type="button"
>
{t(
'runtime.deepseekHarness.marketplace.cancel'
)}
</button>
<button
className="primary-button"
disabled={!installConfirmed || installBusy}
onClick={() => {
void apply(
`install:${entry.id}`,
{
type: 'install',
extensionId: entry.id,
package: entry.package
},
t(
updateAvailable
? 'runtime.deepseekHarness.marketplace.notifications.updated'
: 'runtime.deepseekHarness.marketplace.notifications.installed',
{ name: entry.displayName }
)
).then((installedSuccessfully) => {
if (
installedSuccessfully &&
mountedRef.current
) {
setConfirmingInstall(undefined)
setInstallConfirmed(false)
}
})
}}
type="button"
>
{installBusy
? t(
'runtime.deepseekHarness.marketplace.installing'
)
: t(
'runtime.deepseekHarness.marketplace.confirmInstall'
)}
</button>
</div>
</fieldset>
)}
{configurationOpen && installed && (
<div className="runtime-extension-configuration">
<label className="field">
<span>
{t(
'runtime.deepseekHarness.marketplace.configuration.label',
{ name: entry.displayName }
)}
</span>
<textarea
aria-label={t(
'runtime.deepseekHarness.marketplace.configuration.label',
{ name: entry.displayName }
)}
aria-invalid={Boolean(configurationError)}
disabled={Boolean(busy)}
onChange={(event) => {
setConfigurationDraft(
event.currentTarget.value
)
setConfigurationError(undefined)
}}
spellCheck={false}
value={configurationDraft}
/>
<small>
{t(
'runtime.deepseekHarness.marketplace.configuration.help'
)}
</small>
{configurationError && (
<small className="field-error" role="alert">
{configurationError}
</small>
)}
</label>
<button
className="primary-button"
disabled={Boolean(busy)}
onClick={() =>
void saveConfiguration(installed)
}
type="button"
>
{t(
'runtime.deepseekHarness.marketplace.configuration.save'
)}
</button>
</div>
)}
</article>
)
})}
</div>
)}
{entries.length > maximumVisibleEntries && (
<p className="settings-notice">
{t(
'runtime.deepseekHarness.marketplace.refineSearch',
{ count: maximumVisibleEntries }
)}
</p>
)}
</>
)}
{loading && !snapshot && (
<p className="settings-empty" role="status">
{t('runtime.deepseekHarness.marketplace.loading')}
</p>
)}
</section>
)
}
+3 -3
View File
@@ -39,6 +39,8 @@ import { PageTabs } from './WorkspacePrimitives'
const configurableMcpTargets: RuntimeTarget[] = [
'model',
'opencode',
'continue',
'deepseek-harness'
]
type McpSettingsTab = 'builtin' | 'computer' | 'custom'
@@ -79,9 +81,7 @@ function editorFromServer(server: McpServerSummary): McpEditor {
description: server.description,
enabled: server.enabled,
allowDynamicTools: server.allowDynamicTools,
assignments: server.assignments.filter((target) =>
configurableMcpTargets.includes(target)
),
assignments: server.assignments,
transport: server.transport,
command: server.transport === 'stdio' ? server.command : '',
args: server.transport === 'stdio' ? server.args.join('\n') : '',
+38 -7
View File
@@ -424,6 +424,17 @@ const selectSpeechModel = vi.fn<
speechModelSnapshot = createSpeechModelSnapshot(modelId)
return speechModelSnapshot
})
const runtimeExtensionSnapshot = {
marketplaceEnabled: true,
catalog: [],
installed: []
}
const getRuntimeExtensionSnapshot = vi.fn(
async () => runtimeExtensionSnapshot
)
const applyRuntimeExtension = vi.fn(
async () => runtimeExtensionSnapshot
)
describe('SettingsPanel runtime files', () => {
beforeEach(async () => {
@@ -496,6 +507,10 @@ describe('SettingsPanel runtime files', () => {
openRepository: vi.fn(),
openModelsDirectory: vi.fn()
},
runtimeExtensions: {
getSnapshot: getRuntimeExtensionSnapshot,
apply: applyRuntimeExtension
},
updates: {
getSettings: getApplicationSettings,
updateSettings: updateApplicationSettings,
@@ -1579,6 +1594,19 @@ describe('SettingsPanel runtime files', () => {
expect(
screen.getByText('开发者预览 · OpenAI 兼容')
).toBeInTheDocument()
expect(
screen.getByRole('heading', { name: 'DSH 插件市场' })
).toBeInTheDocument()
expect(
await screen.findByText(
/第三方插件的安装脚本、初始化代码及工具均以当前用户权限运行/
)
).toBeInTheDocument()
expect(
screen.getByRole('switch', {
name: '启用 DSH 插件市场'
})
).toBeChecked()
const harnessOverview = screen
.getByText('GoodBuddy 内置 DeepSeek Harness')
.closest<HTMLElement>('.runtime-overview')
@@ -1607,7 +1635,7 @@ describe('SettingsPanel runtime files', () => {
fireEvent.click(screen.getByText('高级设置'))
expect(
screen.getByText(
/始终使用 GoodBuddy 内置并固定版本的 Host/
/已启用的市场插件由 GoodBuddy 托管并随 Host 启动/
)
).toBeInTheDocument()
expect(
@@ -2838,7 +2866,9 @@ describe('SettingsPanel runtime files', () => {
fireEvent.click(screen.getByRole('tab', { name: 'Skills' }))
expect(await screen.findByText('文档写作')).toBeInTheDocument()
expect(
screen.getByText('支持直连模型、OpenCode 和 Continue')
screen.getByText(
'支持直连模型、OpenCode、Continue 和 DeepSeek Harness'
)
).toBeInTheDocument()
expect(
screen.getByText(/新导入的 Skill 默认启用/)
@@ -3046,10 +3076,12 @@ describe('SettingsPanel runtime files', () => {
within(mcpTabs).getByRole('tab', { name: '自定义 MCP' })
)
expect(
screen.getByText(/自定义 MCP 可分配给直连模型或 DeepSeek Harness/)
screen.getByText(
/自定义 MCP 可分配给直连模型、GoodBuddy 管理的 OpenCode、Continue Agent 或 DeepSeek Harness/
)
).toHaveTextContent('新建时默认分配给直连模型')
expect(
screen.getByText(/服务凭据不会进入 Harness Utility/)
screen.getByText(/服务地址、命令和凭据始终由 GoodBuddy 主进程保管/)
).toBeInTheDocument()
expect(
await screen.findByText('尚未配置 MCP Server')
@@ -3079,9 +3111,8 @@ describe('SettingsPanel runtime files', () => {
expect(
within(dialog).getByLabelText('DeepSeek Harness')
).not.toBeChecked()
expect(
within(dialog).queryByLabelText('OpenCode')
).not.toBeInTheDocument()
expect(within(dialog).getByLabelText('OpenCode')).not.toBeChecked()
expect(within(dialog).getByLabelText('Continue')).not.toBeChecked()
await waitFor(() =>
expect(within(dialog).getByLabelText('名称')).toHaveFocus()
)
+4
View File
@@ -45,6 +45,7 @@ import { PlatformFeaturesSettingsSection } from './PlatformFeaturesSettingsSecti
import { SpeechModelSettingsSection } from './SpeechModelSettingsSection'
import { EmbeddingSettingsSection } from './EmbeddingSettingsSection'
import { DocumentParsingSettingsSection } from './DocumentParsingSettingsSection'
import { DshMarketplaceSection } from './DshMarketplaceSection'
import { PageHeader, SegmentedControl } from './WorkspacePrimitives'
import {
SettingsCategoryHeader,
@@ -2192,6 +2193,9 @@ export function SettingsPanel({
</button>
</details>
</div>
)}
{agentRuntimeType === 'deepseek-harness' && (
<DshMarketplaceSection onNotify={onNotify} />
)}
</>
)}
@@ -179,7 +179,7 @@ export const integrations = {
custom: 'Custom MCP'
},
customNotice:
'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.',
'Custom MCP can be assigned to direct models, GoodBuddy-managed OpenCode, Continue Agent, or DeepSeek Harness. New servers target direct models by default and load only in Execute mode. Agent runtimes receive only a request-scoped loopback capability; GoodBuddy keeps server addresses, commands, and credentials in the main process.',
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: {
@@ -71,7 +71,8 @@ export const settings = {
skills: {
label: 'Skills',
navigationDescription: 'Built-in and custom capabilities',
description: 'Works with direct models, OpenCode, and Continue'
description:
'Works with direct models, OpenCode, Continue, and DeepSeek Harness'
},
mcp: {
label: 'MCP',
@@ -246,7 +247,7 @@ export const settings = {
title: 'DeepSeek Harness',
previewDescription: 'Developer preview · OpenAI-compatible',
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.',
'GoodBuddy maintains the fixed Host and control protocol internally and uses pinned Harness libraries underneath. Ask limits model tool calls to read-only tools, while Execute can use every enabled tool and DSH plugin capability. Cancellation and workspace boundaries remain in place.',
managedSource:
'Administrator-provided OpenAI-compatible connection',
connection: 'OpenAI-compatible model connection',
@@ -255,7 +256,81 @@ export const settings = {
connectionDescription:
'Choose a GoodBuddy model connection. It must use OpenAI Chat Completions with API-key authentication.',
advancedDescription:
'This Runtime always uses GoodBuddys bundled, version-pinned Host. It does not load external DSH plugins, marketplace packages, user profiles, or custom Hosts.'
'This Runtime always uses GoodBuddys bundled, version-pinned Host and does not load user profiles or custom Hosts. GoodBuddy manages enabled marketplace plugins and loads them with the Host.',
marketplace: {
title: 'DSH plugin marketplace',
previewDescription: 'Preview · public npm registry',
switch: {
aria: 'Enable the DSH plugin marketplace',
enabled: 'On',
disabled: 'Off'
},
disabledDescription:
'The plugin marketplace is off by default. Turn it on to connect to the public npm catalog and show its management interface. Turning off the marketplace does not disable or uninstall existing plugins.',
permissionNotice:
'Third-party install scripts, initialization code, and tools run with your user permissions. Ask limits the model from calling non-read-only tools, but cannot limit plugin initialization code. Execute can call every tool from enabled plugins. Install only packages you trust.',
refresh: 'Refresh',
refreshAria: 'Refresh the DSH plugin marketplace',
searchLabel: 'Search plugins',
searchPlaceholder:
'Filter by name, package, description, or license',
retry: 'Try again',
loading: 'Loading the plugin catalog…',
catalogUnavailable:
'Could not refresh the npm plugin catalog: {{detail}}. Installed plugins remain manageable.',
results: 'Showing {{shown}} of {{total}} plugins',
noResults: 'No plugins match your search.',
empty: 'No DSH plugins were found in the public npm registry.',
refineSearch:
'Only the first {{count}} plugins are shown. Refine your search to see others.',
notInCatalog:
'This installed plugin is not currently in the npm marketplace catalog.',
installed: 'Installed',
enabled: 'Enabled',
disabled: 'Disabled',
enableAria: 'Enable {{name}}',
install: 'Install and enable',
update: 'Update to {{version}}',
installing: 'Installing…',
installConfirmationTitle: 'Install {{name}}',
installConfirmation:
'npm runs install scripts declared by this package and its dependencies. After installation, plugin initialization code runs when DeepSeek Harness starts.',
trustConfirmation:
'I trust {{package}} and understand that its code runs with my user permissions.',
confirmInstall: 'Confirm install',
cancel: 'Cancel',
remove: 'Remove',
removeAria: 'Remove {{name}}',
confirmRemove: 'Confirm removal',
removeMessage:
'Remove {{name}} and its GoodBuddy-managed files?',
startupFailure:
'The plugin failed on its last startup and was disabled automatically. Check its configuration or version before enabling it again.',
configuration: {
open: 'Configure',
close: 'Close configuration',
label: '{{name}} configuration JSON',
help: 'Save a JSON object and restart the current Runtime to pass it to the plugin.',
save: 'Save configuration',
invalid: 'Configuration must be a valid JSON object.'
},
errors: {
unavailable:
'The DSH plugin marketplace is unavailable in this version',
readFailed: 'Could not load the DSH plugin marketplace',
operationFailed: 'The DSH plugin operation failed'
},
notifications: {
marketplaceEnabled: 'Enabled the DSH plugin marketplace',
marketplaceDisabled: 'Disabled the DSH plugin marketplace',
installed: 'Installed and enabled {{name}}',
updated: 'Updated {{name}}',
enabled: 'Enabled {{name}}',
disabled: 'Disabled {{name}}',
configured: 'Saved the configuration for {{name}}',
removed: 'Removed {{name}}'
}
}
}
},
documentParsing: {
@@ -166,7 +166,7 @@ export const integrations = {
custom: '自定义 MCP'
},
customNotice:
'自定义 MCP 可分配给直连模型或 DeepSeek Harness,新建时默认分配给直连模型,并仅在 Execute 模式加载。Harness 工具由 GoodBuddy 主进程代理,服务凭据不会进入 Harness Utility。',
'自定义 MCP 可分配给直连模型、GoodBuddy 管理的 OpenCode、Continue Agent 或 DeepSeek Harness,新建时默认分配给直连模型,并仅在 Execute 模式加载。Agent Runtime 只接收按请求签发的本机回环权限;服务地址、命令和凭据始终由 GoodBuddy 主进程保管。',
securityNotice:
'内置工具由 GoodBuddy 提供,不属于 MCP Server。自定义 MCP Server 及其工具具有当前用户权限,请仅添加可信服务;远程访问令牌将由系统安全存储加密,工具调用前仍需 GoodBuddy 审批。',
computer: {
@@ -61,7 +61,7 @@ export const settings = {
skills: {
label: 'Skills',
navigationDescription: '内置与自定义能力',
description: '支持直连模型、OpenCodeContinue'
description: '支持直连模型、OpenCodeContinue 和 DeepSeek Harness'
},
mcp: {
label: 'MCP',
@@ -223,14 +223,83 @@ export const settings = {
title: 'DeepSeek Harness',
previewDescription: '开发者预览 · OpenAI 兼容',
description:
'由 GoodBuddy 内部维护固定 Host 与控制协议,复用锁定的 Harness 底层库;Execute 工具调用自动单次授权,Ask 保持只读,并保留取消和工作区安全边界;不接入 DSH 插件或市场机制。',
'由 GoodBuddy 内部维护固定 Host 与控制协议,复用锁定的 Harness 底层库;Ask 仅允许模型调用只读工具,Execute 可调用全部已启用工具及 DSH 插件能力,并保留取消和工作区边界。',
managedSource: '管理员预置的 OpenAI 兼容连接',
connection: 'OpenAI 兼容模型连接',
connectionPlaceholder: '选择 OpenAI 兼容模型连接',
connectionDescription:
'从 GoodBuddy 模型连接中选择;协议必须为 OpenAI Chat Completions,并使用 API Key。',
advancedDescription:
'该 Runtime 始终使用 GoodBuddy 内置并固定版本的 Host,不加载外部 DSH 插件、市场包、用户 profile 或自定义 Host。'
'该 Runtime 始终使用 GoodBuddy 内置并固定版本的 Host,不加载用户 profile 或自定义 Host;已启用的市场插件由 GoodBuddy 托管并随 Host 启动。',
marketplace: {
title: 'DSH 插件市场',
previewDescription: '预览 · npm 公共仓库',
switch: {
aria: '启用 DSH 插件市场',
enabled: '已开启',
disabled: '已关闭'
},
disabledDescription:
'插件市场默认关闭。开启后才会连接公共 npm 目录并显示管理界面;关闭市场不会停用或卸载已有插件。',
permissionNotice:
'第三方插件的安装脚本、初始化代码及工具均以当前用户权限运行。Ask 只限制模型调用非只读工具,无法限制插件初始化代码;Execute 可调用已启用插件提供的全部工具。请仅安装可信包。',
refresh: '刷新',
refreshAria: '刷新 DSH 插件市场',
searchLabel: '搜索插件',
searchPlaceholder: '按名称、包名、描述或许可证筛选',
retry: '重试',
loading: '正在加载插件目录…',
catalogUnavailable:
'无法刷新 npm 插件目录:{{detail}}。已安装插件仍可管理。',
results: '显示 {{shown}} / {{total}} 个插件',
noResults: '没有匹配的插件。',
empty: 'npm 公共仓库中暂未找到 DSH 插件。',
refineSearch: '当前最多显示 {{count}} 个插件,请缩小搜索范围。',
notInCatalog: '此已安装插件目前不在 npm 市场目录中。',
installed: '已安装',
enabled: '已启用',
disabled: '已停用',
enableAria: '启用 {{name}}',
install: '安装并启用',
update: '更新到 {{version}}',
installing: '正在安装…',
installConfirmationTitle: '安装 {{name}}',
installConfirmation:
'npm 会运行该包及其依赖声明的安装脚本。安装后,插件初始化代码会随 DeepSeek Harness 启动。',
trustConfirmation:
'我信任 {{package}},并了解其代码将以当前用户权限运行。',
confirmInstall: '确认安装',
cancel: '取消',
remove: '移除',
removeAria: '移除 {{name}}',
confirmRemove: '确认移除',
removeMessage: '移除 {{name}} 及其由 GoodBuddy 托管的文件?',
startupFailure:
'插件上次启动失败,已自动停用。确认配置或版本后可重新启用。',
configuration: {
open: '配置',
close: '收起配置',
label: '{{name}} 配置 JSON',
help: '保存一个 JSON 对象并重启当前 Runtime,使配置传给插件。',
save: '保存配置',
invalid: '配置必须是有效的 JSON 对象。'
},
errors: {
unavailable: '当前版本未提供 DSH 插件市场服务',
readFailed: '读取 DSH 插件市场失败',
operationFailed: 'DSH 插件操作失败'
},
notifications: {
marketplaceEnabled: '已开启 DSH 插件市场',
marketplaceDisabled: '已关闭 DSH 插件市场',
installed: '已安装并启用 {{name}}',
updated: '已更新 {{name}}',
enabled: '已启用 {{name}}',
disabled: '已停用 {{name}}',
configured: '已保存 {{name}} 的配置',
removed: '已移除 {{name}}'
}
}
}
},
documentParsing: {
+236
View File
@@ -5262,6 +5262,242 @@ button > svg {
color: var(--text-secondary) !important;
}
.runtime-extension-marketplace {
min-width: 0;
}
.runtime-extension-marketplace__header-actions {
display: flex;
align-items: center;
gap: var(--space-2);
}
.runtime-extension-marketplace__master-toggle {
min-height: 30px;
padding: 0;
border-bottom: 0;
white-space: nowrap;
}
.runtime-extension-marketplace__search-input {
position: relative;
display: block;
}
.runtime-extension-marketplace__search-input > svg {
position: absolute;
z-index: 1;
color: var(--text-muted);
left: 11px;
pointer-events: none;
top: 50%;
transform: translateY(-50%);
}
.runtime-extension-marketplace__search-input > input {
padding-left: 34px;
}
.runtime-extension-marketplace__load-error {
display: flex;
align-items: center;
gap: var(--space-2);
}
.runtime-extension-marketplace__load-error > p {
flex: 1;
}
.runtime-extension-marketplace__list {
display: grid;
min-width: 0;
gap: var(--space-3);
}
.runtime-extension-card {
display: grid;
min-width: 0;
padding: var(--space-3);
border: 1px solid var(--border-default);
border-radius: var(--radius-card);
background: var(--surface-raised);
gap: var(--space-3);
}
.runtime-extension-card__header {
display: flex;
min-width: 0;
align-items: flex-start;
justify-content: space-between;
gap: var(--space-3);
}
.runtime-extension-card__header > div:first-child {
display: grid;
min-width: 0;
gap: var(--space-1);
}
.runtime-extension-card__header strong {
color: var(--text-primary);
font-size: var(--font-body);
overflow-wrap: anywhere;
}
.runtime-extension-card__header code {
color: var(--text-muted);
font-family: var(--font-family-mono);
font-size: var(--font-caption);
overflow-wrap: anywhere;
}
.runtime-extension-card__tags {
display: flex;
justify-content: flex-end;
flex-wrap: wrap;
gap: var(--space-1);
}
.runtime-extension-card__tags span {
padding: 2px 6px;
border: 1px solid var(--border-subtle);
border-radius: 999px;
background: var(--surface-subtle);
color: var(--text-secondary);
font-size: var(--font-caption);
white-space: nowrap;
}
.runtime-extension-card > p {
margin: 0;
color: var(--text-secondary);
font-size: var(--font-body);
line-height: 1.6;
overflow-wrap: anywhere;
}
.runtime-extension-card__actions {
display: flex;
min-width: 0;
align-items: center;
flex-wrap: wrap;
gap: var(--space-2);
}
.runtime-extension-card__actions > button,
.runtime-extension-card__actions > .danger-confirm,
.runtime-extension-configuration > button {
display: inline-flex;
align-items: center;
justify-content: center;
gap: var(--space-1);
}
.runtime-extension-card__actions > .primary-button {
margin-left: auto;
}
.runtime-extension-card__toggle {
min-height: 30px;
padding: 0;
border-top: 0;
justify-content: flex-start;
font-size: var(--font-caption);
}
.runtime-extension-install-confirmation {
display: grid;
min-width: 0;
margin: 0;
padding: var(--space-3);
border: 1px solid var(--warning-border);
border-radius: var(--radius-control);
background: var(--warning-subtle);
gap: var(--space-2);
}
.runtime-extension-install-confirmation legend {
padding: 0 var(--space-1);
color: var(--text-primary);
font-size: var(--font-body);
font-weight: 650;
}
.runtime-extension-install-confirmation p {
margin: 0;
color: var(--text-secondary);
font-size: var(--font-caption);
line-height: 1.6;
}
.runtime-extension-install-confirmation > label {
display: grid;
align-items: start;
color: var(--text-secondary);
cursor: pointer;
font-size: var(--font-caption);
grid-template-columns: auto minmax(0, 1fr);
gap: var(--space-2);
line-height: 1.55;
}
.runtime-extension-install-confirmation > label input {
margin: 2px 0 0;
}
.runtime-extension-install-confirmation > div {
display: flex;
justify-content: flex-end;
gap: var(--space-2);
}
.runtime-extension-configuration {
display: grid;
min-width: 0;
padding-top: var(--space-3);
border-top: 1px solid var(--border-subtle);
gap: var(--space-2);
}
.runtime-extension-configuration textarea {
min-height: 120px;
font-family: var(--font-family-mono);
line-height: 1.5;
}
.runtime-extension-configuration > button {
width: fit-content;
justify-self: end;
}
@media (max-width: 720px) {
.runtime-extension-marketplace
> .settings-section__title--actions {
align-items: flex-start;
flex-wrap: wrap;
}
.runtime-extension-marketplace__header-actions {
width: 100%;
padding-left: 26px;
flex-wrap: wrap;
}
.runtime-extension-card__header,
.runtime-extension-marketplace__load-error {
align-items: stretch;
flex-direction: column;
}
.runtime-extension-card__tags {
justify-content: flex-start;
}
.runtime-extension-card__actions > .primary-button {
margin-left: 0;
}
}
details.settings-section {
padding: 0;
gap: 0;