diff --git a/AGENTS.md b/AGENTS.md index 49b795e..0bc6cc2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -370,7 +370,7 @@ interface BetterSidebarService { } ``` -> **声明式设置(v0.5.0+)**:每个注册的 tab/viewer 自动出现在 DSH 设置页「侧边卡片」分区的清单里(图标 + 标题 + 类型 id + 开关,viewer 行还显示扩展名),开关持久化到 `SidebarPrefs.tabsEnabled / viewersEnabled`(开放 map,缺省 = 启用)。关闭语义:tab 从 `+` 菜单消失、`openTab` 拒绝新开、子代理自动展开 / agent 终端自动补 tab 等派生流程停止,**已打开的 tab 保留**;viewer 被 `matchFileViewer` 跳过,文件落到下一个匹配。`settings.toggles` 声明的嵌套开关(如子代理的 `autoOpenSubagent`)仅父级启用时显示;**key 必须是宿主 PrefsSchema 的字段**(内置键:`autoOpenSubagent` / `agentTerminalTools`),外部插件的自定义键会被 settings seam 丢弃。 +> **声明式设置(v0.5.0+)**:每个注册的 tab/viewer 自动出现在 DSH 设置页「侧边卡片」分区的清单里(图标 + 标题 + 类型 id + **可点击卡片**,高亮 = 启用,viewer 卡片还显示扩展名),开关持久化到 `SidebarPrefs.tabsEnabled / viewersEnabled`(开放 map,缺省 = 启用)。关闭语义:tab 从 `+` 菜单消失、`openTab` 拒绝新开、子代理自动展开 / agent 终端自动补 tab 等派生流程停止,**已打开的 tab 保留**;viewer 被 `matchFileViewer` 跳过,文件落到下一个匹配。`settings.toggles` 声明的嵌套开关(如子代理的 `autoOpenSubagent`)仅父级启用时显示,同为小卡片样式;**key 必须是宿主 PrefsSchema 的字段**(内置键:`autoOpenSubagent` / `agentTerminalTools`),外部插件的自定义键会被 settings seam 丢弃。 --- diff --git a/README.md b/README.md index 205f8e3..0435565 100644 --- a/README.md +++ b/README.md @@ -154,9 +154,9 @@ dsh registry enable dsh-external/dsh-better-sidebar - 设置页新增 **Side card(侧边卡片)** 分区,持久化到 DSH 用户设置文档: - **新会话默认打开**:新建会话是否自动展开侧边卡片(已存在的会话保持各自布局) - **默认宽度占比**:新建会话时侧边卡片占窗口宽度的百分比(20–60,默认 30) -- **声明式的功能清单**:分区按注册表实时渲染当前支持的侧边栏内容与文件预览器(内置与外部插件注册的 tab/viewer 一视同仁),每项显示**类型 id + 图标 + 开关**,可直接打开/关闭: - - **侧边栏内容**:资源管理器 / 源代码管理 / 子代理 / 终端 / 编辑器 / 差异 等 tab 类型。关闭某类型后:`+` 菜单不再显示、无法新开(已打开的 tab 保留可继续使用,关闭后需重新开启才能再开);子代理的「检测到子代理时自动展开子代理页面」、终端的「为模型注入终端工具」作为**相关设置**嵌套在其行下(父级关闭时隐藏) - - **文件预览**:图片 / PDF / Word / Excel / PPT / Markdown / 代码 / 二进制下载 等查看器,行内显示其覆盖的扩展名。关闭某查看器后文件预览落到下一个匹配的查看器(关闭兜底的代码查看器后无法预览的文件显示下载按钮) +- **声明式的功能清单**:分区按注册表实时渲染当前支持的侧边栏内容与文件预览器(内置与外部插件注册的 tab/viewer 一视同仁),每项是一张**可点击的卡片**——图标 + 类型 id + 描述,**点击整卡切换开/关,卡片高亮(品牌描边 + 勾选徽标)即代表启用**,未启用呈灰色降级态: + - **侧边栏内容**:资源管理器 / 源代码管理 / 子代理 / 终端 / 编辑器 / 差异 等 tab 类型。关闭某类型后:`+` 菜单不再显示、无法新开(已打开的 tab 保留可继续使用,关闭后需重新开启才能再开);子代理的「检测到子代理时自动展开子代理页面」、终端的「为模型注入终端工具」作为**相关设置**以小卡片嵌套在其行下(父级关闭时隐藏) + - **文件预览**:图片 / PDF / Word / Excel / PPT / Markdown / 代码 / 二进制下载 等查看器,卡片上显示其覆盖的扩展名。关闭某查看器后文件预览落到下一个匹配的查看器(关闭兜底的代码查看器后无法预览的文件显示下载按钮) - **检测到子代理时自动展开子代理页面**(默认开启):当前会话产生新的子代理时,自动展开侧边栏并打开 Subagent 页面 - **为模型注入终端工具**(默认关闭):开启后模型可通过 8 个 `terminal_*` 工具创建并操作侧边栏终端;关闭会注销工具并释放已创建的 agent 终端 diff --git a/docs/plans/2026-08-11-declarative-sidebar-settings-design.md b/docs/plans/2026-08-11-declarative-sidebar-settings-design.md index 6711b70..3e1c93c 100644 --- a/docs/plans/2026-08-11-declarative-sidebar-settings-design.md +++ b/docs/plans/2026-08-11-declarative-sidebar-settings-design.md @@ -133,3 +133,4 @@ isViewerEnabled(id: string): boolean 2. **宿主 `syncToolsGate` 不改**:`agentTerminalTools` 仅随自身开关走,不与 `tabsEnabled['terminal']` 联动(模型终端工具是模型面向能力,与侧边栏 UI 开关解耦)。 3. **`renderRow` 统一行渲染**:tab/viewer/嵌套开关共用一行渲染器(`sub` 参数区分缩进),CSS 新增 `.sectionHeading` / `.rowIcon` / `.titleLine` / `.subRow`。 4. **清单排序**:tabs 按 `hidden` 置后 + `order` 升序(editor/diff 殿后);viewers 按 priority 降序(code 兜底最后)。 +5. **开关改为卡片点击(v0.5.0 追加)**:清单项与嵌套相关设置、`openByDefault` 全部从"复选框行"改为**可点击卡片**——` ) return (
- +
{t('settingsGeneralTitle')}
+ {renderCard({ + title: t('settingsOpenTitle'), + desc: t('settingsOpenDesc'), + icon: , + enabled: prefs.openByDefault, + onToggle, + })}
{t('settingsWidthTitle')} @@ -277,27 +282,25 @@ export function SideCardSection({ store, service }: SideCardSectionProps) {
- {/* 侧边栏内容: one row per registered tab type, icon + type id + switch, - plus each tab's declaratively-declared nested toggles (shown only - while the tab itself is enabled). */} + {/* 侧边栏内容: one card per registered tab type, icon + type id; the + card's `settings.toggles` render as nested smaller cards while the + tab itself is enabled. */}
{t('settingsTabsTitle')}
{tabs.map(tab => ( - {renderRow({ + {renderCard({ title: textOf(tab.title), desc: tab.id, icon: iconOf(tab.icon, 16), - checked: prefs.tabsEnabled[tab.id] !== false, - label: textOf(tab.title), + enabled: prefs.tabsEnabled[tab.id] !== false, onToggle: (next) => { onToggleTab(tab.id, next) }, })} {prefs.tabsEnabled[tab.id] !== false && (tab.settings?.toggles ?? []).map(toggle => ( - {renderRow({ + {renderCard({ title: textOf(toggle.title), desc: textOf(toggle.desc), - checked: prefBool(prefs, toggle.key), - label: textOf(toggle.title), + enabled: prefBool(prefs, toggle.key), onToggle: (next) => { onToggleSetting(toggle, next) }, sub: true, })} @@ -306,16 +309,15 @@ export function SideCardSection({ store, service }: SideCardSectionProps) { ))} - {/* 文件预览: one row per registered file viewer, icon + title + exts + switch. */} + {/* 文件预览: one card per registered file viewer, icon + title + exts. */}
{t('settingsViewersTitle')}
{viewers.map(viewer => ( - {renderRow({ + {renderCard({ title: textOf(viewer.title) || viewer.id, desc: viewer.exts.length === 0 ? t('settingsViewerCatchAll') : viewer.exts.join(' · '), icon: iconOf(viewer.icon, 16), - checked: prefs.viewersEnabled[viewer.id] !== false, - label: textOf(viewer.title) || viewer.id, + enabled: prefs.viewersEnabled[viewer.id] !== false, onToggle: (next) => { onToggleViewer(viewer.id, next) }, })} diff --git a/src/client/locales.ts b/src/client/locales.ts index 6850a31..a6140cc 100644 --- a/src/client/locales.ts +++ b/src/client/locales.ts @@ -123,6 +123,7 @@ const zh = { settingsToolsDesc: '开启后,模型可通过 terminal_create 等 8 个工具创建并操作侧边栏终端(默认关闭)', settingsTabsTitle: '侧边栏内容', settingsViewersTitle: '文件预览', + settingsGeneralTitle: '常规', settingsViewerCatchAll: '兜底:任意文件', viewerImage: '图片', viewerPdf: 'PDF', @@ -267,6 +268,7 @@ const en: Record = { settingsToolsDesc: 'When enabled, the model can create and drive sidebar terminals through the 8 terminal_* tools (off by default)', settingsTabsTitle: 'Sidebar content', settingsViewersTitle: 'File viewers', + settingsGeneralTitle: 'General', settingsViewerCatchAll: 'Catch-all: any file', viewerImage: 'Image', viewerPdf: 'PDF', diff --git a/tests/side-card-section.spec.tsx b/tests/side-card-section.spec.tsx index 8c11774..3b70119 100644 --- a/tests/side-card-section.spec.tsx +++ b/tests/side-card-section.spec.tsx @@ -1,11 +1,13 @@ /** * Side card settings section render tests: the section is DECLARATIVE — - * every row (icon, title, type id, extensions, enable switch) derives from + * every card (icon, title, type id, extensions, on/off state) derives from * the sidebar service's tab/viewer registries instead of hardcoded copy. - * These specs pin that derivation with a fake store + a small registry: + * The toggles are CARDS: the whole card is the switch, the visual state IS + * the state (highlighted = enabled), announced via `aria-pressed`. These + * specs pin that derivation with a fake store + a small registry: * registered tabs/viewers appear with their declared icon/title/settings, - * disabled states render unchecked, and a disabled tab hides its declared - * nested toggles. + * disabled states render pressed=false, and a disabled tab hides its + * declared nested cards. * * Rendered with renderToString (mount effects — the settings RPC sync — do * not run in SSR; the initial store prefs are the render input). @@ -60,58 +62,61 @@ function renderSection(store: SidebarStore, service: BetterSidebarService): stri )) } +/** Count `aria-pressed` occurrences of one value in the rendered HTML. */ +function pressedCount(html: string, value: string): number { + return html.match(new RegExp(`aria-pressed="${value}"`, 'g'))?.length ?? 0 +} + describe('SideCardSection declarative inventory', () => { - it('renders one row per registered tab: icon + title + type id + switch', () => { + it('renders one card per registered tab: icon + title + type id + pressed state', () => { const { store, service } = mount() const html = renderSection(store, service) expect(html).toContain('data-icon="explorer"') expect(html).toContain('>Explorer<') - // The type id is the row's desc (the declarative "type" surface). + // The type id is the card's desc (the declarative "type" surface). expect(html).toContain('>explorer<') expect(html).toContain('data-icon="subagent"') expect(html).toContain('>Subagents<') - // Both tabs are enabled by default: their switches render checked. - expect(html.match(/type="checkbox"/g)?.length).toBeGreaterThanOrEqual(4) + // Default prefs: openByDefault + both tabs + the nested auto-open + the + // image viewer are all enabled → 5 cards pressed, none pressed=false. + expect(pressedCount(html, 'true')).toBe(5) + expect(pressedCount(html, 'false')).toBe(0) }) - it('renders the declared nested toggle under an enabled tab', () => { + it('renders the declared nested card under an enabled tab', () => { const { store, service } = mount() const html = renderSection(store, service) // The subagent tab's declared related setting appears while enabled, - // bound to its prefs key (default true → checked). + // bound to its prefs key (default true → pressed). expect(html).toContain('Auto-open Subagents') expect(html).toContain('Expand on new subagent') }) - it('hides the nested toggles while the parent tab is disabled', () => { + it('hides the nested cards while the parent tab is disabled', () => { const { store, service } = mount() store.setPrefs({ ...store.getPrefs(), tabsEnabled: { subagent: false } }) const html = renderSection(store, service) - // The parent row stays (so the user can re-enable it) but unchecked. + // The parent card stays (so the user can re-enable it) but pressed=false. expect(html).toContain('>Subagents<') expect(html).not.toContain('Auto-open Subagents') + expect(pressedCount(html, 'false')).toBe(1) }) - it('renders one row per registered viewer: icon + title + exts + switch', () => { + it('renders one card per registered viewer: icon + title + exts', () => { const { store, service } = mount() const html = renderSection(store, service) expect(html).toContain('data-icon="image"') expect(html).toContain('>Image<') - // The covered extensions are the row's desc. + // The covered extensions are the card's desc. expect(html).toContain('png · jpg') }) - it('a disabled viewer renders unchecked', () => { + it('a disabled viewer renders pressed=false', () => { const { store, service } = mount() store.setPrefs({ ...store.getPrefs(), viewersEnabled: { image: false } }) const html = renderSection(store, service) expect(html).toContain('>Image<') - }) - - it('a disabled tab renders unchecked (its switch reflects the prefs map)', () => { - const { store, service } = mount() - store.setPrefs({ ...store.getPrefs(), tabsEnabled: { explorer: false } }) - const html = renderSection(store, service) - expect(html).toContain('>explorer<') + expect(pressedCount(html, 'false')).toBe(1) + expect(pressedCount(html, 'true')).toBe(4) }) })