fix: unify settings center layout
This commit is contained in:
@@ -508,9 +508,10 @@ GoodBuddy 是可调整窗口大小的桌面应用。响应式设计优先保证
|
||||
- 全页设置使用固定标题区、左侧分类导航和独立滚动的内容区。右上角关闭按钮是离开设置中心的稳定入口。
|
||||
- 全页设置标题区依靠留白与内容区分层,不在标题下方绘制贯穿整个工作区的分隔线;模态设置可以保留标题边界。
|
||||
- 设置中心不显示全局操作页脚,避免重复关闭入口和没有功能意义的整宽分隔线。
|
||||
- 当前分类存在“保存”或“测试”等未提交配置操作时,统一放在当前内容区顶部右侧;主保存操作在最右侧,测试等次操作排列在其左侧。
|
||||
- 所有分类使用共享的 `SettingsCategoryHeader` 呈现分类标题、说明、错误与操作,不得在内容卡片内复制分类标题或创建页面专属操作栏。左侧分类名称与说明来自同一份分类定义,新增分类时不得分别维护导航和内容标题。
|
||||
- 当前分类存在“保存”或“测试”等未提交配置操作时,统一放在分类页头右侧;主保存操作在最右侧,测试等次操作排列在其左侧。
|
||||
- 自动生效、仅执行即时命令或自行管理编辑流程的分类不显示全局保存操作。窄窗口下操作区可以换行,但保存入口必须保持清晰可见。
|
||||
- 自动生效的设置不为了显示成功文案而长期占用页脚,通过应用通知提供短期反馈。加载或保存错误仍需保留可处理的上下文。
|
||||
- 保存或测试成功统一进入应用通知视口,并按全局规则自动消失,不在分类页头或内容卡片中保留持久成功文案。加载、保存和测试错误显示在分类页头下方,并保留可处理的上下文。
|
||||
|
||||
## 14. 文案规则
|
||||
|
||||
@@ -560,7 +561,7 @@ GoodBuddy 是可调整窗口大小的桌面应用。响应式设计优先保证
|
||||
- [ ] 知识库迁移到 `master-detail`,清除内联浅色样式并补齐窄窗口单面板流程。
|
||||
- [ ] 智能心跳迁移到 `dashboard`,统一状态卡片、配置和运行历史层级。
|
||||
- [ ] 任务迁移到 `standard`,活动记录迁移到 `dashboard`,统一导航、筛选和表格行为。
|
||||
- [ ] 设置中心移除全局操作页脚,将未提交配置的保存与测试操作统一放到当前内容区顶部右侧。
|
||||
- [ ] 设置中心使用共享分类定义与 `SettingsCategoryHeader`,将保存与测试操作统一放到分类页头右侧,并把成功反馈接入应用通知。
|
||||
|
||||
### 15.5 验收
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import {
|
||||
FlaskConical,
|
||||
FolderOpen,
|
||||
MessageSquare,
|
||||
Save,
|
||||
Smartphone,
|
||||
Unplug
|
||||
@@ -34,6 +33,7 @@ import type { WeixinBindingSnapshot } from '../../shared/weixin-channel-contract
|
||||
import type { AppNotificationInput } from './notifications'
|
||||
import { trapTabFocus } from './dialog-focus'
|
||||
import { PageTabs, SegmentedControl } from './WorkspacePrimitives'
|
||||
import { SettingsCategoryHeader } from './SettingsPrimitives'
|
||||
|
||||
type ChannelDraft = {
|
||||
enabled: boolean
|
||||
@@ -1218,40 +1218,44 @@ export function ChannelSettingsSection({
|
||||
!dingtalkProject
|
||||
) {
|
||||
return (
|
||||
<div className="settings-section">
|
||||
<p className={error ? 'settings-warning' : 'settings-empty'}>
|
||||
{error ?? '正在读取消息通道设置…'}
|
||||
</p>
|
||||
</div>
|
||||
<>
|
||||
<SettingsCategoryHeader
|
||||
category="channels"
|
||||
error={error}
|
||||
headingId="channel-settings-heading"
|
||||
/>
|
||||
{!error && (
|
||||
<div className="settings-section">
|
||||
<p className="settings-empty">正在读取消息通道设置…</p>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<section
|
||||
aria-labelledby="channel-settings-heading"
|
||||
className="settings-section channel-settings"
|
||||
>
|
||||
<div className="settings-section__title settings-section__title--actions">
|
||||
<MessageSquare aria-hidden="true" size={17} />
|
||||
<div>
|
||||
<strong id="channel-settings-heading">消息通道</strong>
|
||||
<small>
|
||||
为每个通道配置连接、工作目录、消息处理后端与默认模式
|
||||
</small>
|
||||
</div>
|
||||
<button
|
||||
className="primary-button"
|
||||
disabled={busy}
|
||||
onClick={() => void save()}
|
||||
type="button"
|
||||
>
|
||||
<Save aria-hidden="true" size={13} />
|
||||
{busy ? '保存中…' : '保存通道设置'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<>
|
||||
<SettingsCategoryHeader
|
||||
actions={
|
||||
<button
|
||||
className="primary-button"
|
||||
disabled={busy}
|
||||
onClick={() => void save()}
|
||||
type="button"
|
||||
>
|
||||
<Save aria-hidden="true" size={13} />
|
||||
{busy ? '保存中…' : '保存通道设置'}
|
||||
</button>
|
||||
}
|
||||
category="channels"
|
||||
error={error}
|
||||
headingId="channel-settings-heading"
|
||||
/>
|
||||
<section
|
||||
aria-label="消息通道配置"
|
||||
className="settings-section channel-settings"
|
||||
>
|
||||
{snapshot.warning && <p className="settings-warning">{snapshot.warning}</p>}
|
||||
{error && <p className="settings-warning" role="alert">{error}</p>}
|
||||
|
||||
<div className="channel-settings__tabs">
|
||||
<PageTabs
|
||||
@@ -1325,6 +1329,7 @@ export function ChannelSettingsSection({
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ import type {
|
||||
RuntimeTarget
|
||||
} from '../../shared/capability-contracts'
|
||||
import { trapTabFocus } from './dialog-focus'
|
||||
import { SettingsCategoryHeader } from './SettingsPrimitives'
|
||||
|
||||
const runtimeLabels: Record<RuntimeTarget, string> = {
|
||||
model: '模型',
|
||||
@@ -318,25 +319,26 @@ export function McpSettingsSection(): React.JSX.Element {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="settings-section">
|
||||
<div className="settings-section__title settings-section__title--actions">
|
||||
<Network size={17} />
|
||||
<div>
|
||||
<strong>工具与 MCP</strong>
|
||||
<small>查看内置工具、内置 MCP 并管理外部 MCP Server</small>
|
||||
</div>
|
||||
<button
|
||||
className="secondary-button"
|
||||
disabled={Boolean(busy) || Boolean(editor)}
|
||||
onClick={(event) =>
|
||||
openEditor({ ...emptyEditor }, event.currentTarget)
|
||||
}
|
||||
type="button"
|
||||
>
|
||||
<Plus size={14} />
|
||||
添加 Server
|
||||
</button>
|
||||
</div>
|
||||
<>
|
||||
<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} />
|
||||
添加 Server
|
||||
</button>
|
||||
}
|
||||
category="mcp"
|
||||
error={!editor ? error : undefined}
|
||||
headingId="mcp-settings-heading"
|
||||
/>
|
||||
<section aria-label="MCP 配置" className="settings-section">
|
||||
|
||||
<p className="settings-notice">
|
||||
自定义 MCP 当前仅用于直连模型,新建时默认分配给直连模型,并仅在 Execute
|
||||
@@ -348,8 +350,6 @@ export function McpSettingsSection(): React.JSX.Element {
|
||||
及其工具具有当前用户权限,请仅添加可信服务;远程访问令牌将由系统安全存储加密,
|
||||
工具调用前仍需 GoodBuddy 审批。
|
||||
</p>
|
||||
{error && !editor && <p className="settings-warning">{error}</p>}
|
||||
|
||||
<section
|
||||
aria-labelledby="computer-capabilities-heading"
|
||||
className="mcp-tool-section"
|
||||
@@ -1108,6 +1108,7 @@ export function McpSettingsSection(): React.JSX.Element {
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { Sparkles } from 'lucide-react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import type {
|
||||
ApplicationSettings,
|
||||
@@ -6,6 +5,7 @@ import type {
|
||||
} from '../../shared/application-settings-contracts'
|
||||
import type { MagicNoteCommentFormat } from '../../shared/magic-notes-contracts'
|
||||
import { SegmentedControl } from './WorkspacePrimitives'
|
||||
import { SettingsCategoryHeader } from './SettingsPrimitives'
|
||||
|
||||
type PlatformFeaturesSettingsSectionProps = {
|
||||
onMagicNotesEnabledChange: (enabled: boolean) => void
|
||||
@@ -108,17 +108,13 @@ export function PlatformFeaturesSettingsSection({
|
||||
}
|
||||
|
||||
return (
|
||||
<section
|
||||
aria-labelledby="platform-features-heading"
|
||||
className="settings-section"
|
||||
>
|
||||
<div className="settings-section__title">
|
||||
<Sparkles aria-hidden="true" size={17} />
|
||||
<div>
|
||||
<strong id="platform-features-heading">平台功能</strong>
|
||||
<small>控制 GoodBuddy 工作区中显示的功能入口</small>
|
||||
</div>
|
||||
</div>
|
||||
<>
|
||||
<SettingsCategoryHeader
|
||||
category="platform-features"
|
||||
error={error}
|
||||
headingId="platform-features-heading"
|
||||
/>
|
||||
<section aria-label="平台功能选项" className="settings-section">
|
||||
<article className="capability-card">
|
||||
<div className="capability-card__header">
|
||||
<div>
|
||||
@@ -175,11 +171,7 @@ export function PlatformFeaturesSettingsSection({
|
||||
</small>
|
||||
</div>
|
||||
</article>
|
||||
{error && (
|
||||
<p className="settings-warning" role="alert">
|
||||
{error}
|
||||
</p>
|
||||
)}
|
||||
</section>
|
||||
</section>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -476,6 +476,7 @@ describe('SettingsPanel runtime files', () => {
|
||||
magicNoteCommentMode: 'after-save-auto'
|
||||
})
|
||||
)
|
||||
|
||||
expect(
|
||||
screen.getByRole('button', { name: '长评 + 要点' })
|
||||
).toHaveAttribute('aria-pressed', 'true')
|
||||
@@ -532,7 +533,7 @@ describe('SettingsPanel runtime files', () => {
|
||||
).toBeNull()
|
||||
})
|
||||
|
||||
it('places explicit configuration actions at the top of the content', () => {
|
||||
it('uses one category header for titles and explicit actions', () => {
|
||||
render(
|
||||
<SettingsPanel
|
||||
{...heartbeatSettingsProps}
|
||||
@@ -548,18 +549,24 @@ describe('SettingsPanel runtime files', () => {
|
||||
name: '设置中心'
|
||||
})
|
||||
const content = screen.getByRole('tabpanel')
|
||||
const toolbar = content.querySelector(
|
||||
'.settings-panel__content-toolbar'
|
||||
const categoryHeader = content.querySelector(
|
||||
'.settings-category-header'
|
||||
)
|
||||
|
||||
expect(toolbar).toBe(content.firstElementChild)
|
||||
expect(categoryHeader).toBe(content.firstElementChild)
|
||||
expect(
|
||||
within(toolbar as HTMLElement).getByRole('button', {
|
||||
within(categoryHeader as HTMLElement).getByRole('heading', {
|
||||
level: 2,
|
||||
name: 'Agent Runtime'
|
||||
})
|
||||
).toBeInTheDocument()
|
||||
expect(
|
||||
within(categoryHeader as HTMLElement).getByRole('button', {
|
||||
name: '保存设置'
|
||||
})
|
||||
).toBeInTheDocument()
|
||||
expect(
|
||||
within(toolbar as HTMLElement).getByRole('button', {
|
||||
within(categoryHeader as HTMLElement).getByRole('button', {
|
||||
name: '保存并测试 OpenCode'
|
||||
})
|
||||
).toBeInTheDocument()
|
||||
@@ -571,6 +578,35 @@ describe('SettingsPanel runtime files', () => {
|
||||
expect(
|
||||
screen.queryByRole('button', { name: '保存设置' })
|
||||
).not.toBeInTheDocument()
|
||||
expect(
|
||||
screen.getByRole('heading', { level: 2, name: '外观' })
|
||||
).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('routes save success through the transient app notification', async () => {
|
||||
const onNotify = vi.fn()
|
||||
render(
|
||||
<SettingsPanel
|
||||
{...heartbeatSettingsProps}
|
||||
open
|
||||
onClearLocalData={vi.fn(async () => {})}
|
||||
onClose={vi.fn()}
|
||||
onNotify={onNotify}
|
||||
onSaved={vi.fn()}
|
||||
/>
|
||||
)
|
||||
|
||||
await screen.findByDisplayValue('C:\\Workspace')
|
||||
fireEvent.click(screen.getByRole('button', { name: '保存设置' }))
|
||||
|
||||
await waitFor(() =>
|
||||
expect(onNotify).toHaveBeenCalledWith({
|
||||
tone: 'success',
|
||||
message: '设置已保存',
|
||||
dedupeKey: 'runtime-settings-saved'
|
||||
})
|
||||
)
|
||||
expect(screen.queryByText('设置已保存')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('uses one first-level heading for the settings page', () => {
|
||||
@@ -1386,6 +1422,7 @@ describe('SettingsPanel runtime files', () => {
|
||||
})
|
||||
|
||||
it('tests the selected model instead of a selected Continue Runtime', async () => {
|
||||
const onNotify = vi.fn()
|
||||
getRuntime.mockResolvedValueOnce({
|
||||
...runtimeSettings,
|
||||
provider: 'continue',
|
||||
@@ -1405,6 +1442,7 @@ describe('SettingsPanel runtime files', () => {
|
||||
open
|
||||
onClearLocalData={vi.fn(async () => {})}
|
||||
onClose={vi.fn()}
|
||||
onNotify={onNotify}
|
||||
onSaved={vi.fn()}
|
||||
/>
|
||||
)
|
||||
@@ -1419,7 +1457,14 @@ describe('SettingsPanel runtime files', () => {
|
||||
expect(testModelConnection).toHaveBeenCalledWith(modelProfileId)
|
||||
)
|
||||
expect(testRuntime).not.toHaveBeenCalled()
|
||||
expect(await screen.findByText('连接成功:sonnet-5')).toBeInTheDocument()
|
||||
await waitFor(() =>
|
||||
expect(onNotify).toHaveBeenCalledWith({
|
||||
tone: 'success',
|
||||
message: '连接成功:sonnet-5',
|
||||
dedupeKey: 'model-connection-tested'
|
||||
})
|
||||
)
|
||||
expect(screen.queryByText('连接成功:sonnet-5')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('shows an actionable model error without Electron IPC prefixes', async () => {
|
||||
|
||||
+100
-270
@@ -1,5 +1,4 @@
|
||||
import {
|
||||
Check,
|
||||
FolderOpen,
|
||||
KeyRound,
|
||||
LockKeyhole,
|
||||
@@ -39,6 +38,11 @@ import { PlatformFeaturesSettingsSection } from './PlatformFeaturesSettingsSecti
|
||||
import { SpeechModelSettingsSection } from './SpeechModelSettingsSection'
|
||||
import { EmbeddingSettingsSection } from './EmbeddingSettingsSection'
|
||||
import { PageHeader, SegmentedControl } from './WorkspacePrimitives'
|
||||
import { SettingsCategoryHeader } from './SettingsPrimitives'
|
||||
import {
|
||||
settingsCategoryList,
|
||||
type SettingsCategoryId
|
||||
} from './settings-categories'
|
||||
import type { AppearanceTheme } from './theme'
|
||||
import type { AppNotificationInput } from './notifications'
|
||||
import type {
|
||||
@@ -46,18 +50,6 @@ import type {
|
||||
EmbeddingSettingsSnapshot
|
||||
} from '../../shared/embedding-contracts'
|
||||
|
||||
type SettingsTab =
|
||||
| 'appearance'
|
||||
| 'platform-features'
|
||||
| 'model'
|
||||
| 'runtime'
|
||||
| 'security'
|
||||
| 'automation'
|
||||
| 'channels'
|
||||
| 'roles'
|
||||
| 'skills'
|
||||
| 'mcp'
|
||||
| 'about'
|
||||
type ModelType = 'llm' | 'embedding' | 'speech'
|
||||
type AgentRuntimeType = RuntimeConfigActionInput['runtime']
|
||||
type ModelProfileDraft = RuntimeSettings['modelProfiles'][number] & {
|
||||
@@ -66,19 +58,7 @@ type ModelProfileDraft = RuntimeSettings['modelProfiles'][number] & {
|
||||
clearApiKey: boolean
|
||||
}
|
||||
|
||||
const settingsTabs: readonly SettingsTab[] = [
|
||||
'appearance',
|
||||
'platform-features',
|
||||
'model',
|
||||
'runtime',
|
||||
'security',
|
||||
'automation',
|
||||
'channels',
|
||||
'roles',
|
||||
'skills',
|
||||
'mcp',
|
||||
'about'
|
||||
]
|
||||
const settingsTabs = settingsCategoryList.map(({ id }) => id)
|
||||
|
||||
type SettingsPanelProps = {
|
||||
open: boolean
|
||||
@@ -253,7 +233,7 @@ export function SettingsPanel({
|
||||
presentation = 'modal',
|
||||
onClose,
|
||||
onSaved,
|
||||
onNotify,
|
||||
onNotify = () => {},
|
||||
onClearLocalData,
|
||||
heartbeats,
|
||||
onCreateHeartbeat,
|
||||
@@ -333,13 +313,12 @@ export function SettingsPanel({
|
||||
const [embeddingDiagnosticRunning, setEmbeddingDiagnosticRunning] =
|
||||
useState(false)
|
||||
const [error, setError] = useState<string>()
|
||||
const [saved, setSaved] = useState(false)
|
||||
const [connectionResult, setConnectionResult] = useState<string>()
|
||||
const [confirmingClear, setConfirmingClear] = useState(false)
|
||||
const [clearingLocalData, setClearingLocalData] = useState(false)
|
||||
const [detection, setDetection] = useState<AgentRuntimeDetection>()
|
||||
const [detecting, setDetecting] = useState(false)
|
||||
const [activeTab, setActiveTab] = useState<SettingsTab>('runtime')
|
||||
const [activeTab, setActiveTab] =
|
||||
useState<SettingsCategoryId>('runtime')
|
||||
const [modelType, setModelType] = useState<ModelType>('llm')
|
||||
const [agentRuntimeType, setAgentRuntimeType] =
|
||||
useState<AgentRuntimeType>('opencode')
|
||||
@@ -349,12 +328,16 @@ export function SettingsPanel({
|
||||
activeTab === 'runtime' ||
|
||||
activeTab === 'security' ||
|
||||
activeTab === 'roles'
|
||||
const showContentActions =
|
||||
configurationTab || Boolean(error) || saved
|
||||
const categoryRendersOwnHeader =
|
||||
activeTab === 'platform-features' ||
|
||||
activeTab === 'channels' ||
|
||||
activeTab === 'skills' ||
|
||||
activeTab === 'mcp' ||
|
||||
activeTab === 'about'
|
||||
|
||||
const handleTabKeyDown = (
|
||||
event: React.KeyboardEvent<HTMLButtonElement>,
|
||||
tab: SettingsTab
|
||||
tab: SettingsCategoryId
|
||||
): void => {
|
||||
const currentIndex = settingsTabs.indexOf(tab)
|
||||
let nextIndex: number | undefined
|
||||
@@ -374,6 +357,7 @@ export function SettingsPanel({
|
||||
}
|
||||
event.preventDefault()
|
||||
const nextTab = settingsTabs[nextIndex]!
|
||||
setError(undefined)
|
||||
setActiveTab(nextTab)
|
||||
event.currentTarget.parentElement
|
||||
?.querySelector<HTMLButtonElement>(
|
||||
@@ -390,8 +374,6 @@ export function SettingsPanel({
|
||||
.getRuntime()
|
||||
.then((value) => {
|
||||
setError(undefined)
|
||||
setSaved(false)
|
||||
setConnectionResult(undefined)
|
||||
setConfirmingClear(false)
|
||||
setClearingLocalData(false)
|
||||
setModelType('llm')
|
||||
@@ -496,10 +478,11 @@ export function SettingsPanel({
|
||||
onClose()
|
||||
}
|
||||
|
||||
const save = async (): Promise<RuntimeSettings | undefined> => {
|
||||
const save = async (
|
||||
notifySuccess = true
|
||||
): Promise<RuntimeSettings | undefined> => {
|
||||
setSaving(true)
|
||||
setError(undefined)
|
||||
setSaved(false)
|
||||
try {
|
||||
const defaultProfile =
|
||||
modelProfiles.find(
|
||||
@@ -605,8 +588,14 @@ export function SettingsPanel({
|
||||
)
|
||||
}
|
||||
}
|
||||
setSaved(true)
|
||||
onSaved(value)
|
||||
if (notifySuccess) {
|
||||
onNotify({
|
||||
tone: 'success',
|
||||
message: '设置已保存',
|
||||
dedupeKey: 'runtime-settings-saved'
|
||||
})
|
||||
}
|
||||
return value
|
||||
} catch (reason) {
|
||||
setError(settingsErrorMessage(reason, '保存设置失败'))
|
||||
@@ -620,8 +609,7 @@ export function SettingsPanel({
|
||||
const testingModel = activeTab === 'model' && modelType === 'llm'
|
||||
const profileId = selectedModelProfileId
|
||||
setTesting(true)
|
||||
setConnectionResult(undefined)
|
||||
const savedSettings = await save()
|
||||
const savedSettings = await save(false)
|
||||
if (!savedSettings) {
|
||||
setTesting(false)
|
||||
return
|
||||
@@ -644,11 +632,16 @@ export function SettingsPanel({
|
||||
if (!status.available) {
|
||||
throw new Error(status.detail)
|
||||
}
|
||||
setConnectionResult(
|
||||
status.capability === 'image-generation'
|
||||
? status.detail
|
||||
: `连接成功:${status.label}`
|
||||
)
|
||||
onNotify({
|
||||
tone: 'success',
|
||||
message:
|
||||
status.capability === 'image-generation'
|
||||
? status.detail
|
||||
: `连接成功:${status.label}`,
|
||||
dedupeKey: testingModel
|
||||
? 'model-connection-tested'
|
||||
: `runtime-connection-tested-${agentRuntimeType}`
|
||||
})
|
||||
} catch (reason) {
|
||||
setError(
|
||||
settingsErrorMessage(
|
||||
@@ -671,7 +664,7 @@ export function SettingsPanel({
|
||||
setEmbeddingDiagnostic(undefined)
|
||||
setError(undefined)
|
||||
try {
|
||||
if (!(await save())) {
|
||||
if (!(await save(false))) {
|
||||
return
|
||||
}
|
||||
const diagnostic = await embeddings.diagnose()
|
||||
@@ -692,7 +685,7 @@ export function SettingsPanel({
|
||||
}
|
||||
setError(undefined)
|
||||
try {
|
||||
if (!(await save())) {
|
||||
if (!(await save(false))) {
|
||||
return
|
||||
}
|
||||
const indexStatus = await embeddings.rebuild()
|
||||
@@ -993,182 +986,28 @@ export function SettingsPanel({
|
||||
className="settings-tabs"
|
||||
role="tablist"
|
||||
>
|
||||
<button
|
||||
aria-controls="settings-panel-appearance"
|
||||
aria-label="外观"
|
||||
aria-selected={activeTab === 'appearance'}
|
||||
id="settings-tab-appearance"
|
||||
onClick={() => setActiveTab('appearance')}
|
||||
onKeyDown={(event) =>
|
||||
handleTabKeyDown(event, 'appearance')
|
||||
}
|
||||
role="tab"
|
||||
tabIndex={activeTab === 'appearance' ? 0 : -1}
|
||||
type="button"
|
||||
>
|
||||
<strong>外观</strong>
|
||||
<small>亮色、暗色与系统主题</small>
|
||||
</button>
|
||||
<button
|
||||
aria-controls="settings-panel-platform-features"
|
||||
aria-label="平台功能"
|
||||
aria-selected={activeTab === 'platform-features'}
|
||||
id="settings-tab-platform-features"
|
||||
onClick={() => setActiveTab('platform-features')}
|
||||
onKeyDown={(event) =>
|
||||
handleTabKeyDown(event, 'platform-features')
|
||||
}
|
||||
role="tab"
|
||||
tabIndex={activeTab === 'platform-features' ? 0 : -1}
|
||||
type="button"
|
||||
>
|
||||
<strong>平台功能</strong>
|
||||
<small>功能入口与工作区能力</small>
|
||||
</button>
|
||||
<button
|
||||
aria-controls="settings-panel-model"
|
||||
aria-label="模型连接"
|
||||
aria-selected={activeTab === 'model'}
|
||||
id="settings-tab-model"
|
||||
onClick={() => setActiveTab('model')}
|
||||
onKeyDown={(event) =>
|
||||
handleTabKeyDown(event, 'model')
|
||||
}
|
||||
role="tab"
|
||||
tabIndex={activeTab === 'model' ? 0 : -1}
|
||||
type="button"
|
||||
>
|
||||
<strong>模型连接</strong>
|
||||
<small>LLM、向量模型与凭据</small>
|
||||
</button>
|
||||
<button
|
||||
aria-controls="settings-panel-runtime"
|
||||
aria-label="Agent Runtime"
|
||||
aria-selected={activeTab === 'runtime'}
|
||||
id="settings-tab-runtime"
|
||||
onClick={() => setActiveTab('runtime')}
|
||||
onKeyDown={(event) =>
|
||||
handleTabKeyDown(event, 'runtime')
|
||||
}
|
||||
role="tab"
|
||||
tabIndex={activeTab === 'runtime' ? 0 : -1}
|
||||
type="button"
|
||||
>
|
||||
<strong>Agent Runtime</strong>
|
||||
<small>OpenCode、Continue 与工作区</small>
|
||||
</button>
|
||||
<button
|
||||
aria-controls="settings-panel-security"
|
||||
aria-label="安全与数据"
|
||||
aria-selected={activeTab === 'security'}
|
||||
id="settings-tab-security"
|
||||
onClick={() => setActiveTab('security')}
|
||||
onKeyDown={(event) =>
|
||||
handleTabKeyDown(event, 'security')
|
||||
}
|
||||
role="tab"
|
||||
tabIndex={activeTab === 'security' ? 0 : -1}
|
||||
type="button"
|
||||
>
|
||||
<strong>安全与数据</strong>
|
||||
<small>工具策略与本地隐私</small>
|
||||
</button>
|
||||
<button
|
||||
aria-controls="settings-panel-automation"
|
||||
aria-label="自动化"
|
||||
aria-selected={activeTab === 'automation'}
|
||||
id="settings-tab-automation"
|
||||
onClick={() => setActiveTab('automation')}
|
||||
onKeyDown={(event) =>
|
||||
handleTabKeyDown(event, 'automation')
|
||||
}
|
||||
role="tab"
|
||||
tabIndex={activeTab === 'automation' ? 0 : -1}
|
||||
type="button"
|
||||
>
|
||||
<strong>自动化</strong>
|
||||
<small>智能心跳与周期回顾</small>
|
||||
</button>
|
||||
<button
|
||||
aria-controls="settings-panel-channels"
|
||||
aria-label="消息通道"
|
||||
aria-selected={activeTab === 'channels'}
|
||||
id="settings-tab-channels"
|
||||
onClick={() => setActiveTab('channels')}
|
||||
onKeyDown={(event) =>
|
||||
handleTabKeyDown(event, 'channels')
|
||||
}
|
||||
role="tab"
|
||||
tabIndex={activeTab === 'channels' ? 0 : -1}
|
||||
type="button"
|
||||
>
|
||||
<strong>消息通道</strong>
|
||||
<small>微信、企业微信与钉钉</small>
|
||||
</button>
|
||||
<button
|
||||
aria-controls="settings-panel-roles"
|
||||
aria-label="角色与提示词"
|
||||
aria-selected={activeTab === 'roles'}
|
||||
id="settings-tab-roles"
|
||||
onClick={() => setActiveTab('roles')}
|
||||
onKeyDown={(event) =>
|
||||
handleTabKeyDown(event, 'roles')
|
||||
}
|
||||
role="tab"
|
||||
tabIndex={activeTab === 'roles' ? 0 : -1}
|
||||
type="button"
|
||||
>
|
||||
<strong>角色与提示词</strong>
|
||||
<small>角色、说明与系统提示词</small>
|
||||
</button>
|
||||
<button
|
||||
aria-controls="settings-panel-skills"
|
||||
aria-label="Skills"
|
||||
aria-selected={activeTab === 'skills'}
|
||||
id="settings-tab-skills"
|
||||
onClick={() => setActiveTab('skills')}
|
||||
onKeyDown={(event) =>
|
||||
handleTabKeyDown(event, 'skills')
|
||||
}
|
||||
role="tab"
|
||||
tabIndex={activeTab === 'skills' ? 0 : -1}
|
||||
type="button"
|
||||
>
|
||||
<strong>Skills</strong>
|
||||
<small>内置与自定义能力</small>
|
||||
</button>
|
||||
<button
|
||||
aria-controls="settings-panel-mcp"
|
||||
aria-label="MCP"
|
||||
aria-selected={activeTab === 'mcp'}
|
||||
id="settings-tab-mcp"
|
||||
onClick={() => setActiveTab('mcp')}
|
||||
onKeyDown={(event) =>
|
||||
handleTabKeyDown(event, 'mcp')
|
||||
}
|
||||
role="tab"
|
||||
tabIndex={activeTab === 'mcp' ? 0 : -1}
|
||||
type="button"
|
||||
>
|
||||
<strong>MCP</strong>
|
||||
<small>工具服务与凭据</small>
|
||||
</button>
|
||||
<button
|
||||
aria-controls="settings-panel-about"
|
||||
aria-label="关于与更新"
|
||||
aria-selected={activeTab === 'about'}
|
||||
id="settings-tab-about"
|
||||
onClick={() => setActiveTab('about')}
|
||||
onKeyDown={(event) =>
|
||||
handleTabKeyDown(event, 'about')
|
||||
}
|
||||
role="tab"
|
||||
tabIndex={activeTab === 'about' ? 0 : -1}
|
||||
type="button"
|
||||
>
|
||||
<strong>关于与更新</strong>
|
||||
<small>版本检查与下载页</small>
|
||||
</button>
|
||||
{settingsCategoryList.map((category) => (
|
||||
<button
|
||||
aria-controls={`settings-panel-${category.id}`}
|
||||
aria-label={category.label}
|
||||
aria-selected={activeTab === category.id}
|
||||
id={`settings-tab-${category.id}`}
|
||||
key={category.id}
|
||||
onClick={() => {
|
||||
setError(undefined)
|
||||
setActiveTab(category.id)
|
||||
}}
|
||||
onKeyDown={(event) =>
|
||||
handleTabKeyDown(event, category.id)
|
||||
}
|
||||
role="tab"
|
||||
tabIndex={activeTab === category.id ? 0 : -1}
|
||||
type="button"
|
||||
>
|
||||
<strong>{category.label}</strong>
|
||||
<small>{category.navigationDescription}</small>
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<div
|
||||
@@ -1178,52 +1017,43 @@ export function SettingsPanel({
|
||||
ref={settingsBodyRef}
|
||||
role="tabpanel"
|
||||
>
|
||||
{showContentActions && (
|
||||
<div className="settings-panel__content-toolbar">
|
||||
<div className="settings-feedback">
|
||||
{error && (
|
||||
<span className="settings-error" role="alert">
|
||||
{error}
|
||||
</span>
|
||||
)}
|
||||
{saved && (
|
||||
<span className="settings-success" role="status">
|
||||
<Check aria-hidden="true" size={14} />
|
||||
{connectionResult ?? '设置已保存'}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{configurationTab && (
|
||||
<div className="settings-panel__content-actions">
|
||||
{(activeTab === 'runtime' ||
|
||||
(activeTab === 'model' && modelType === 'llm')) && (
|
||||
{!categoryRendersOwnHeader && (
|
||||
<SettingsCategoryHeader
|
||||
actions={
|
||||
configurationTab ? (
|
||||
<>
|
||||
{(activeTab === 'runtime' ||
|
||||
(activeTab === 'model' && modelType === 'llm')) && (
|
||||
<button
|
||||
className="secondary-button"
|
||||
disabled={saving || testing}
|
||||
onClick={() => void testConnection()}
|
||||
type="button"
|
||||
>
|
||||
{testing
|
||||
? '测试中…'
|
||||
: activeTab === 'model'
|
||||
? '保存并测试模型'
|
||||
: agentRuntimeType === 'opencode'
|
||||
? '保存并测试 OpenCode'
|
||||
: '保存并测试 Continue'}
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
className="secondary-button"
|
||||
className="primary-button"
|
||||
disabled={saving || testing}
|
||||
onClick={() => void testConnection()}
|
||||
onClick={() => void save()}
|
||||
type="button"
|
||||
>
|
||||
{testing
|
||||
? '测试中…'
|
||||
: activeTab === 'model'
|
||||
? '保存并测试模型'
|
||||
: agentRuntimeType === 'opencode'
|
||||
? '保存并测试 OpenCode'
|
||||
: '保存并测试 Continue'}
|
||||
<Save aria-hidden="true" size={13} />
|
||||
{saving ? '保存中…' : '保存设置'}
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
className="primary-button"
|
||||
disabled={saving || testing}
|
||||
onClick={() => void save()}
|
||||
type="button"
|
||||
>
|
||||
<Save aria-hidden="true" size={13} />
|
||||
{saving ? '保存中…' : '保存设置'}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
) : undefined
|
||||
}
|
||||
category={activeTab}
|
||||
error={error}
|
||||
/>
|
||||
)}
|
||||
{activeTab === 'appearance' && (
|
||||
<div className="settings-section appearance-settings">
|
||||
@@ -2215,7 +2045,8 @@ export function SettingsPanel({
|
||||
|
||||
{activeTab === 'security' && (
|
||||
<>
|
||||
<label className="field">
|
||||
<div className="settings-section">
|
||||
<label className="field">
|
||||
<span>Runtime OS 沙箱</span>
|
||||
<select
|
||||
aria-label="Runtime OS 沙箱"
|
||||
@@ -2235,8 +2066,8 @@ export function SettingsPanel({
|
||||
首期严格隔离适用于安装 bubblewrap 的 Linux 嵌入式
|
||||
OpenCode。外部 Runtime 与 Continue 不会被误标为已沙箱。
|
||||
</small>
|
||||
</label>
|
||||
<label className="field">
|
||||
</label>
|
||||
<label className="field">
|
||||
<span>直连模型工具安全策略</span>
|
||||
<select
|
||||
aria-label="直连模型工具安全策略"
|
||||
@@ -2258,7 +2089,8 @@ export function SettingsPanel({
|
||||
不再逐次询问。禁止策略会拒绝所有工具调用。OpenCode 与
|
||||
Continue 继续使用各自的工具系统。
|
||||
</small>
|
||||
</label>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="settings-section settings-section--danger">
|
||||
<div>
|
||||
@@ -2284,8 +2116,6 @@ export function SettingsPanel({
|
||||
onClick={() => {
|
||||
setClearingLocalData(true)
|
||||
setError(undefined)
|
||||
setSaved(false)
|
||||
setConnectionResult(undefined)
|
||||
void onClearLocalData()
|
||||
.then(() => {
|
||||
setConfirmingClear(false)
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import {
|
||||
settingsCategories,
|
||||
type SettingsCategoryId
|
||||
} from './settings-categories'
|
||||
|
||||
export function SettingsCategoryHeader({
|
||||
actions,
|
||||
category,
|
||||
error,
|
||||
headingId = `settings-category-${category}`
|
||||
}: {
|
||||
actions?: ReactNode
|
||||
category: SettingsCategoryId
|
||||
error?: string
|
||||
headingId?: string
|
||||
}): React.JSX.Element {
|
||||
const definition = settingsCategories[category]
|
||||
return (
|
||||
<header className="settings-category-header">
|
||||
<div className="settings-category-header__content">
|
||||
<h2 id={headingId}>{definition.label}</h2>
|
||||
<p>{definition.description}</p>
|
||||
</div>
|
||||
{actions && (
|
||||
<div className="settings-category-header__actions">
|
||||
{actions}
|
||||
</div>
|
||||
)}
|
||||
{error && (
|
||||
<p className="settings-warning" role="alert">
|
||||
{error}
|
||||
</p>
|
||||
)}
|
||||
</header>
|
||||
)
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
import { BookOpen, Download, Trash2 } from 'lucide-react'
|
||||
import { Download, Trash2 } from 'lucide-react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import type {
|
||||
CapabilityAssignments,
|
||||
CapabilitySnapshot,
|
||||
RuntimeTarget
|
||||
} from '../../shared/capability-contracts'
|
||||
import { SettingsCategoryHeader } from './SettingsPrimitives'
|
||||
|
||||
const runtimeLabels: Record<RuntimeTarget, string> = {
|
||||
model: '模型',
|
||||
@@ -56,46 +57,48 @@ export function SkillsSettingsSection(): React.JSX.Element {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="settings-section">
|
||||
<div className="settings-section__title settings-section__title--actions">
|
||||
<BookOpen size={17} />
|
||||
<div>
|
||||
<strong>Skills</strong>
|
||||
<small>支持直连模型、OpenCode 和 Continue</small>
|
||||
</div>
|
||||
<button
|
||||
className="secondary-button"
|
||||
disabled={Boolean(busy)}
|
||||
onClick={() =>
|
||||
void run('import', () =>
|
||||
window.goodbuddy.capabilities.importSkill('directory')
|
||||
)
|
||||
}
|
||||
type="button"
|
||||
>
|
||||
<Download size={14} />
|
||||
导入 Skill 目录
|
||||
</button>
|
||||
<button
|
||||
className="secondary-button"
|
||||
disabled={Boolean(busy)}
|
||||
onClick={() =>
|
||||
void run('import', () =>
|
||||
window.goodbuddy.capabilities.importSkill('zip')
|
||||
)
|
||||
}
|
||||
type="button"
|
||||
>
|
||||
<Download size={14} />
|
||||
导入 Skill ZIP
|
||||
</button>
|
||||
</div>
|
||||
<>
|
||||
<SettingsCategoryHeader
|
||||
actions={
|
||||
<>
|
||||
<button
|
||||
className="secondary-button"
|
||||
disabled={Boolean(busy)}
|
||||
onClick={() =>
|
||||
void run('import', () =>
|
||||
window.goodbuddy.capabilities.importSkill('directory')
|
||||
)
|
||||
}
|
||||
type="button"
|
||||
>
|
||||
<Download aria-hidden="true" size={14} />
|
||||
导入 Skill 目录
|
||||
</button>
|
||||
<button
|
||||
className="secondary-button"
|
||||
disabled={Boolean(busy)}
|
||||
onClick={() =>
|
||||
void run('import', () =>
|
||||
window.goodbuddy.capabilities.importSkill('zip')
|
||||
)
|
||||
}
|
||||
type="button"
|
||||
>
|
||||
<Download aria-hidden="true" size={14} />
|
||||
导入 Skill ZIP
|
||||
</button>
|
||||
</>
|
||||
}
|
||||
category="skills"
|
||||
error={error}
|
||||
headingId="skills-settings-heading"
|
||||
/>
|
||||
<section aria-label="Skills 列表" className="settings-section">
|
||||
|
||||
<p className="settings-notice">
|
||||
Skill 以本地能力说明注入所选目标,不会写入 Runtime
|
||||
自有配置。新导入的 Skill 默认启用,并分配给直连模型、OpenCode 和 Continue。
|
||||
</p>
|
||||
{error && <p className="settings-warning">{error}</p>}
|
||||
{!snapshot && !error && <p className="settings-empty">正在读取 Skills…</p>}
|
||||
<div className="capability-list">
|
||||
{snapshot?.skills.map((skill) => (
|
||||
@@ -174,6 +177,7 @@ export function SkillsSettingsSection(): React.JSX.Element {
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { ExternalLink, Info, RefreshCw } from 'lucide-react'
|
||||
import { ExternalLink, RefreshCw } from 'lucide-react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import type {
|
||||
ApplicationSettings,
|
||||
VersionCheckResult
|
||||
} from '../../shared/application-settings-contracts'
|
||||
import type { AppInfo } from '../../shared/contracts'
|
||||
import { SettingsCategoryHeader } from './SettingsPrimitives'
|
||||
|
||||
function formatBytes(bytes: number): string {
|
||||
if (bytes >= 1024 * 1024 * 1024) {
|
||||
@@ -106,17 +107,16 @@ export function UpdateSettingsSection(): React.JSX.Element {
|
||||
}
|
||||
|
||||
return (
|
||||
<section
|
||||
aria-labelledby="update-settings-heading"
|
||||
className="settings-section update-settings"
|
||||
>
|
||||
<div className="settings-section__title">
|
||||
<Info aria-hidden="true" size={17} />
|
||||
<div>
|
||||
<strong id="update-settings-heading">关于与更新</strong>
|
||||
<small>只检查 GoodBuddy 官方 GitHub Release,不自动下载安装</small>
|
||||
</div>
|
||||
</div>
|
||||
<>
|
||||
<SettingsCategoryHeader
|
||||
category="about"
|
||||
error={error}
|
||||
headingId="update-settings-heading"
|
||||
/>
|
||||
<section
|
||||
aria-label="更新设置"
|
||||
className="settings-section update-settings"
|
||||
>
|
||||
|
||||
<article className="capability-card">
|
||||
<div className="capability-card__header">
|
||||
@@ -165,11 +165,6 @@ export function UpdateSettingsSection(): React.JSX.Element {
|
||||
</div>
|
||||
</article>
|
||||
|
||||
{error && (
|
||||
<p className="settings-warning" role="alert">
|
||||
{error}
|
||||
</p>
|
||||
)}
|
||||
{result && (
|
||||
<article
|
||||
aria-live="polite"
|
||||
@@ -202,6 +197,7 @@ export function UpdateSettingsSection(): React.JSX.Element {
|
||||
</p>
|
||||
</article>
|
||||
)}
|
||||
</section>
|
||||
</section>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
export const settingsCategoryList = [
|
||||
{
|
||||
id: 'appearance',
|
||||
label: '外观',
|
||||
navigationDescription: '亮色、暗色与系统主题',
|
||||
description: '亮色、暗色与系统主题'
|
||||
},
|
||||
{
|
||||
id: 'platform-features',
|
||||
label: '平台功能',
|
||||
navigationDescription: '功能入口与工作区能力',
|
||||
description: '控制 GoodBuddy 工作区中显示的功能入口'
|
||||
},
|
||||
{
|
||||
id: 'model',
|
||||
label: '模型连接',
|
||||
navigationDescription: 'LLM、向量模型与凭据',
|
||||
description: 'LLM、向量模型与凭据'
|
||||
},
|
||||
{
|
||||
id: 'runtime',
|
||||
label: 'Agent Runtime',
|
||||
navigationDescription: 'OpenCode、Continue 与工作区',
|
||||
description: 'OpenCode、Continue 与工作区'
|
||||
},
|
||||
{
|
||||
id: 'security',
|
||||
label: '安全与数据',
|
||||
navigationDescription: '工具策略与本地隐私',
|
||||
description: '工具策略与本地隐私'
|
||||
},
|
||||
{
|
||||
id: 'automation',
|
||||
label: '自动化',
|
||||
navigationDescription: '智能心跳与周期回顾',
|
||||
description: '智能心跳与周期回顾'
|
||||
},
|
||||
{
|
||||
id: 'channels',
|
||||
label: '消息通道',
|
||||
navigationDescription: '微信、企业微信与钉钉',
|
||||
description: '配置连接、工作目录、消息处理后端与默认模式'
|
||||
},
|
||||
{
|
||||
id: 'roles',
|
||||
label: '角色与提示词',
|
||||
navigationDescription: '角色、说明与系统提示词',
|
||||
description: '角色、说明与系统提示词'
|
||||
},
|
||||
{
|
||||
id: 'skills',
|
||||
label: 'Skills',
|
||||
navigationDescription: '内置与自定义能力',
|
||||
description: '支持直连模型、OpenCode 和 Continue'
|
||||
},
|
||||
{
|
||||
id: 'mcp',
|
||||
label: 'MCP',
|
||||
navigationDescription: '工具服务与凭据',
|
||||
description: '查看内置工具、内置 MCP,并管理外部 MCP Server'
|
||||
},
|
||||
{
|
||||
id: 'about',
|
||||
label: '关于与更新',
|
||||
navigationDescription: '版本检查与下载页',
|
||||
description: '只检查 GoodBuddy 官方 GitHub Release,不自动下载安装'
|
||||
}
|
||||
] as const
|
||||
|
||||
export type SettingsCategoryDefinition =
|
||||
(typeof settingsCategoryList)[number]
|
||||
export type SettingsCategoryId = SettingsCategoryDefinition['id']
|
||||
|
||||
export const settingsCategories = Object.fromEntries(
|
||||
settingsCategoryList.map((category) => [category.id, category])
|
||||
) as Record<SettingsCategoryId, SettingsCategoryDefinition>
|
||||
+37
-22
@@ -4065,15 +4065,34 @@ button > svg * {
|
||||
scrollbar-gutter: stable;
|
||||
}
|
||||
|
||||
.settings-panel__content-toolbar {
|
||||
display: flex;
|
||||
min-height: var(--control-height);
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: var(--space-3);
|
||||
.settings-category-header {
|
||||
display: grid;
|
||||
min-width: 0;
|
||||
align-items: start;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
gap: var(--space-3) var(--space-4);
|
||||
}
|
||||
|
||||
.settings-panel__content-actions {
|
||||
.settings-category-header__content {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.settings-category-header__content h2 {
|
||||
margin: 0;
|
||||
color: var(--text-primary);
|
||||
font-size: var(--font-section-title);
|
||||
font-weight: 650;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.settings-category-header__content p {
|
||||
margin: var(--space-1) 0 0;
|
||||
color: var(--text-muted);
|
||||
font-size: var(--font-caption);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.settings-category-header__actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
@@ -4081,13 +4100,19 @@ button > svg * {
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.settings-panel__content-actions .primary-button {
|
||||
.settings-category-header__actions .primary-button,
|
||||
.settings-category-header__actions .secondary-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.settings-category-header > .settings-warning {
|
||||
margin: 0;
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.settings-page .settings-tabs button {
|
||||
min-height: 58px;
|
||||
padding: 10px 12px;
|
||||
@@ -5567,12 +5592,6 @@ details.settings-section > :not(summary) + :not(summary) {
|
||||
color: var(--success) !important;
|
||||
}
|
||||
|
||||
.settings-feedback {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.settings-error {
|
||||
color: var(--danger);
|
||||
}
|
||||
@@ -8773,17 +8792,13 @@ details.settings-section > :not(summary) + :not(summary) {
|
||||
flex-basis: 100%;
|
||||
}
|
||||
|
||||
.settings-panel__content-toolbar {
|
||||
align-items: stretch;
|
||||
flex-wrap: wrap;
|
||||
.settings-category-header {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.settings-feedback {
|
||||
flex-basis: 100%;
|
||||
}
|
||||
|
||||
.settings-panel__content-actions {
|
||||
.settings-category-header__actions {
|
||||
width: 100%;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.mcp-editor {
|
||||
|
||||
Reference in New Issue
Block a user