feat: organize MCP settings into tabs
This commit is contained in:
@@ -32,8 +32,10 @@ import type {
|
||||
} from '../../shared/capability-contracts'
|
||||
import { trapTabFocus } from './dialog-focus'
|
||||
import { SettingsCategoryHeader } from './SettingsPrimitives'
|
||||
import { PageTabs } from './WorkspacePrimitives'
|
||||
|
||||
const configurableMcpTargets: RuntimeTarget[] = ['model']
|
||||
type McpSettingsTab = 'builtin' | 'computer' | 'custom'
|
||||
|
||||
type McpEditor = {
|
||||
id?: string
|
||||
@@ -120,6 +122,8 @@ export function McpSettingsSection(): React.JSX.Element {
|
||||
const [profileNames, setProfileNames] = useState<Record<string, string>>(
|
||||
{}
|
||||
)
|
||||
const [activeTab, setActiveTab] =
|
||||
useState<McpSettingsTab>('builtin')
|
||||
const editorDialogRef = useRef<HTMLDivElement>(null)
|
||||
const editorNameRef = useRef<HTMLInputElement>(null)
|
||||
const editorTriggerRef = useRef<HTMLButtonElement | undefined>(
|
||||
@@ -370,6 +374,12 @@ export function McpSettingsSection(): React.JSX.Element {
|
||||
}
|
||||
|
||||
const computerCapabilities = snapshot?.computerCapabilities ?? []
|
||||
const visibleComputerCapabilities = computerCapabilities.filter(
|
||||
(capability) =>
|
||||
activeTab === 'builtin'
|
||||
? capability.id === 'host-browser-control'
|
||||
: capability.id !== 'host-browser-control'
|
||||
)
|
||||
const browserProfiles = snapshot?.browserProfiles ?? {
|
||||
profiles: [],
|
||||
defaultProfileId: null
|
||||
@@ -385,48 +395,86 @@ export function McpSettingsSection(): React.JSX.Element {
|
||||
<>
|
||||
<SettingsCategoryHeader
|
||||
actions={
|
||||
<button
|
||||
className="secondary-button"
|
||||
disabled={Boolean(busy) || Boolean(editor)}
|
||||
onClick={(event) =>
|
||||
openEditor({ ...emptyEditor }, event.currentTarget)
|
||||
}
|
||||
type="button"
|
||||
>
|
||||
<Plus aria-hidden="true" size={14} />
|
||||
{t('mcp.addServer')}
|
||||
</button>
|
||||
activeTab === 'custom' ? (
|
||||
<button
|
||||
className="secondary-button"
|
||||
disabled={Boolean(busy) || Boolean(editor)}
|
||||
onClick={(event) =>
|
||||
openEditor({ ...emptyEditor }, event.currentTarget)
|
||||
}
|
||||
type="button"
|
||||
>
|
||||
<Plus aria-hidden="true" size={14} />
|
||||
{t('mcp.addServer')}
|
||||
</button>
|
||||
) : undefined
|
||||
}
|
||||
category="mcp"
|
||||
error={!editor ? error : undefined}
|
||||
headingId="mcp-settings-heading"
|
||||
/>
|
||||
<PageTabs
|
||||
ariaLabel={t('mcp.tabs.ariaLabel')}
|
||||
idPrefix="mcp-settings"
|
||||
onChange={(tab) => {
|
||||
setError(undefined)
|
||||
setActiveTab(tab)
|
||||
}}
|
||||
tabs={[
|
||||
{ id: 'builtin', label: t('mcp.tabs.builtin') },
|
||||
{ id: 'computer', label: t('mcp.tabs.computer') },
|
||||
{ id: 'custom', label: t('mcp.tabs.custom') }
|
||||
]}
|
||||
value={activeTab}
|
||||
variant="segmented"
|
||||
/>
|
||||
<section
|
||||
aria-label={t('mcp.sectionAriaLabel')}
|
||||
aria-labelledby={`mcp-settings-tab-${activeTab}`}
|
||||
className="settings-section"
|
||||
id={`mcp-settings-panel-${activeTab}`}
|
||||
role="tabpanel"
|
||||
>
|
||||
|
||||
<p className="settings-notice">
|
||||
{t('mcp.customNotice')}
|
||||
</p>
|
||||
<p className="settings-notice">
|
||||
{t('mcp.securityNotice')}
|
||||
</p>
|
||||
<section
|
||||
{activeTab === 'custom' && (
|
||||
<>
|
||||
<p className="settings-notice">
|
||||
{t('mcp.customNotice')}
|
||||
</p>
|
||||
<p className="settings-notice">
|
||||
{t('mcp.securityNotice')}
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
{activeTab !== 'custom' && (
|
||||
<section
|
||||
aria-labelledby="computer-capabilities-heading"
|
||||
className="mcp-tool-section"
|
||||
>
|
||||
<div className="mcp-subsection-heading">
|
||||
<div>
|
||||
<MonitorCog size={15} />
|
||||
{activeTab === 'builtin' ? (
|
||||
<Globe2 size={15} />
|
||||
) : (
|
||||
<MonitorCog size={15} />
|
||||
)}
|
||||
<strong id="computer-capabilities-heading">
|
||||
{t('mcp.computer.title')}
|
||||
{t(
|
||||
activeTab === 'builtin'
|
||||
? 'mcp.computer.browserTitle'
|
||||
: 'mcp.computer.title'
|
||||
)}
|
||||
</strong>
|
||||
</div>
|
||||
<small>{t('mcp.computer.subtitle')}</small>
|
||||
<small>
|
||||
{t(
|
||||
activeTab === 'builtin'
|
||||
? 'mcp.computer.browserSubtitle'
|
||||
: 'mcp.computer.subtitle'
|
||||
)}
|
||||
</small>
|
||||
</div>
|
||||
<div className="capability-list">
|
||||
{computerCapabilities.map((capability) => {
|
||||
{visibleComputerCapabilities.map((capability) => {
|
||||
const report = diagnostics[capability.id]
|
||||
return (
|
||||
<article className="capability-card" key={capability.id}>
|
||||
@@ -550,8 +598,11 @@ export function McpSettingsSection(): React.JSX.Element {
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
<section
|
||||
{activeTab === 'builtin' && (
|
||||
<>
|
||||
<section
|
||||
aria-labelledby="browser-profiles-heading"
|
||||
className="mcp-tool-section"
|
||||
>
|
||||
@@ -998,6 +1049,8 @@ export function McpSettingsSection(): React.JSX.Element {
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{editor &&
|
||||
createPortal(
|
||||
@@ -1212,7 +1265,9 @@ export function McpSettingsSection(): React.JSX.Element {
|
||||
document.body
|
||||
)}
|
||||
|
||||
<div className="mcp-subsection-heading">
|
||||
{activeTab === 'custom' && (
|
||||
<>
|
||||
<div className="mcp-subsection-heading">
|
||||
<div>
|
||||
<Network size={15} />
|
||||
<strong>{t('mcp.custom.title')}</strong>
|
||||
@@ -1394,6 +1449,8 @@ export function McpSettingsSection(): React.JSX.Element {
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</section>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -2260,22 +2260,27 @@ describe('SettingsPanel runtime files', () => {
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByRole('tab', { name: 'MCP' }))
|
||||
expect(await screen.findByText('电脑控制能力')).toBeInTheDocument()
|
||||
const mcpTabs = screen.getByRole('tablist', {
|
||||
name: 'MCP 设置分类'
|
||||
})
|
||||
expect(
|
||||
screen.getByText(/自定义 MCP 当前仅用于直连模型/)
|
||||
).toHaveTextContent('新建时默认分配给直连模型')
|
||||
within(mcpTabs)
|
||||
.getAllByRole('tab')
|
||||
.map((tab) => tab.textContent)
|
||||
).toEqual(['内置能力', '电脑控制', '自定义 MCP'])
|
||||
expect(
|
||||
screen.getByText(/内置共享 MCP 提供知识库读取与全局笔记管理/)
|
||||
).toHaveTextContent(/直连模型、\s*OpenCode 和 Continue/u)
|
||||
expect(
|
||||
screen.getByText(/Runtime 自有 MCP 配置不在此处管理/)
|
||||
).toBeInTheDocument()
|
||||
within(mcpTabs).getByRole('tab', { name: '内置能力' })
|
||||
).toHaveAttribute('aria-selected', 'true')
|
||||
expect(await screen.findByText('浏览器能力')).toBeInTheDocument()
|
||||
expect(screen.getAllByText('托管浏览器配置').length).toBeGreaterThan(0)
|
||||
expect(
|
||||
screen.getByRole('switch', {
|
||||
screen.queryByRole('switch', {
|
||||
name: '启用 Linux 桌面控制'
|
||||
})
|
||||
).toBeDisabled()
|
||||
).not.toBeInTheDocument()
|
||||
expect(
|
||||
screen.queryByRole('button', { name: /添加 Server/ })
|
||||
).not.toBeInTheDocument()
|
||||
fireEvent.click(
|
||||
screen.getByRole('switch', { name: '启用 浏览器控制' })
|
||||
)
|
||||
@@ -2321,6 +2326,21 @@ describe('SettingsPanel runtime files', () => {
|
||||
await waitFor(() =>
|
||||
expect(removeBrowserProfile).toHaveBeenCalledWith(browserProfileId)
|
||||
)
|
||||
fireEvent.click(
|
||||
within(mcpTabs).getByRole('tab', { name: '电脑控制' })
|
||||
)
|
||||
expect(await screen.findByText('电脑控制能力')).toBeInTheDocument()
|
||||
expect(
|
||||
screen.getByRole('switch', {
|
||||
name: '启用 Linux 桌面控制'
|
||||
})
|
||||
).toBeDisabled()
|
||||
expect(
|
||||
screen.queryByRole('switch', { name: '启用 浏览器控制' })
|
||||
).not.toBeInTheDocument()
|
||||
fireEvent.click(
|
||||
within(mcpTabs).getByRole('tab', { name: '内置能力' })
|
||||
)
|
||||
expect(await screen.findByText('文件系统操作')).toBeInTheDocument()
|
||||
expect(screen.getByText('浏览器操作')).toBeInTheDocument()
|
||||
expect(screen.getByText('联网搜索')).toBeInTheDocument()
|
||||
@@ -2413,6 +2433,15 @@ describe('SettingsPanel runtime files', () => {
|
||||
).toHaveLength(
|
||||
builtinModelToolGroups.filter((group) => group.id !== 'web').length
|
||||
)
|
||||
fireEvent.click(
|
||||
within(mcpTabs).getByRole('tab', { name: '自定义 MCP' })
|
||||
)
|
||||
expect(
|
||||
screen.getByText(/自定义 MCP 当前仅用于直连模型/)
|
||||
).toHaveTextContent('新建时默认分配给直连模型')
|
||||
expect(
|
||||
screen.getByText(/Runtime 自有 MCP 配置不在此处管理/)
|
||||
).toBeInTheDocument()
|
||||
expect(
|
||||
await screen.findByText('尚未配置 MCP Server')
|
||||
).toBeInTheDocument()
|
||||
@@ -2458,6 +2487,7 @@ describe('SettingsPanel runtime files', () => {
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByRole('tab', { name: 'MCP' }))
|
||||
fireEvent.click(screen.getByRole('tab', { name: '自定义 MCP' }))
|
||||
const addServer = await screen.findByRole('button', {
|
||||
name: '添加 Server'
|
||||
})
|
||||
@@ -2551,6 +2581,7 @@ describe('SettingsPanel runtime files', () => {
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByRole('tab', { name: 'MCP' }))
|
||||
fireEvent.click(screen.getByRole('tab', { name: '自定义 MCP' }))
|
||||
const editButton = await screen.findByRole('button', {
|
||||
name: '编辑 团队知识服务'
|
||||
})
|
||||
@@ -2609,6 +2640,7 @@ describe('SettingsPanel runtime files', () => {
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByRole('tab', { name: 'MCP' }))
|
||||
fireEvent.click(screen.getByRole('tab', { name: '自定义 MCP' }))
|
||||
const serverToggle = await screen.findByRole('button', {
|
||||
name: '展开服务器 团队工具服务'
|
||||
})
|
||||
|
||||
@@ -163,13 +163,21 @@ export const integrations = {
|
||||
},
|
||||
addServer: 'Add server',
|
||||
sectionAriaLabel: 'MCP configuration',
|
||||
tabs: {
|
||||
ariaLabel: 'MCP settings categories',
|
||||
builtin: 'Built-in capabilities',
|
||||
computer: 'Computer control',
|
||||
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. Built-in shared MCP provides knowledge reading and global note management to direct models, OpenCode, and Continue. Ask is read-only, and note writes are available only in Execute mode. Runtime-owned MCP configuration is not managed here.',
|
||||
'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.',
|
||||
securityNotice:
|
||||
'Built-in tools are provided by GoodBuddy and are not MCP servers. Custom MCP servers and tools run with the current user’s permissions, so add only trusted services. Remote access tokens are encrypted in secure system storage, and tool calls still require GoodBuddy approval.',
|
||||
computer: {
|
||||
title: 'Computer control capabilities',
|
||||
subtitle: 'Off by default; approval still applies when enabled',
|
||||
browserTitle: 'Browser capabilities',
|
||||
browserSubtitle: 'Managed browser and built-in browser tools',
|
||||
supported: 'Supported on this device',
|
||||
unsupported: 'Not supported on this device',
|
||||
enabled: 'Enabled',
|
||||
|
||||
@@ -150,13 +150,21 @@ export const integrations = {
|
||||
},
|
||||
addServer: '添加 Server',
|
||||
sectionAriaLabel: 'MCP 配置',
|
||||
tabs: {
|
||||
ariaLabel: 'MCP 设置分类',
|
||||
builtin: '内置能力',
|
||||
computer: '电脑控制',
|
||||
custom: '自定义 MCP'
|
||||
},
|
||||
customNotice:
|
||||
'自定义 MCP 当前仅用于直连模型,新建时默认分配给直连模型,并仅在 Execute 模式加载。内置共享 MCP 提供知识库读取与全局笔记管理,可供直连模型、OpenCode 和 Continue 使用;Ask 只读,笔记写入仅在 Execute 模式开放。Runtime 自有 MCP 配置不在此处管理。',
|
||||
'自定义 MCP 当前仅用于直连模型,新建时默认分配给直连模型,并仅在 Execute 模式加载。Runtime 自有 MCP 配置不在此处管理。',
|
||||
securityNotice:
|
||||
'内置工具由 GoodBuddy 提供,不属于 MCP Server。自定义 MCP Server 及其工具具有当前用户权限,请仅添加可信服务;远程访问令牌将由系统安全存储加密,工具调用前仍需 GoodBuddy 审批。',
|
||||
computer: {
|
||||
title: '电脑控制能力',
|
||||
subtitle: '默认停用,启用后仍遵循审批',
|
||||
browserTitle: '浏览器能力',
|
||||
browserSubtitle: '托管浏览器与内置浏览器工具',
|
||||
supported: '当前设备支持',
|
||||
unsupported: '当前设备不支持',
|
||||
enabled: '已启用',
|
||||
|
||||
@@ -6975,6 +6975,7 @@ details.settings-section > :not(summary) + :not(summary) {
|
||||
display: flex;
|
||||
max-width: 100%;
|
||||
overflow-x: auto;
|
||||
flex: 0 0 auto;
|
||||
flex-wrap: nowrap;
|
||||
gap: var(--space-1);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user