From 8ad1766390e4df8e351223f0d14d15a7db7e37a2 Mon Sep 17 00:00:00 2001 From: Menghuan1918 Date: Sat, 15 Aug 2026 15:46:56 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BD=8D=E7=BD=AE=E5=85=BC=E5=AE=B9?= =?UTF-8?q?=E6=A8=A1=E5=BC=8F=E6=94=AF=E6=8C=81=E8=87=AA=E5=AE=9A=E4=B9=89?= =?UTF-8?q?=E4=B8=8B=E7=A7=BB=E8=B7=9D=E7=A6=BB=EF=BC=88=E9=BD=BF=E8=BD=AE?= =?UTF-8?q?=E5=BC=B9=E7=AA=97=E6=95=B0=E5=AD=97=E8=BE=93=E5=85=A5=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit titleBarStripPx 新偏好(0–120px,默认 40):侧边栏按钮与内容的下移量改为 CSS 变量 --dsh-title-bar-strip 驱动,Sidebar shell 随 prefs 写入。设置页 「常规」的位置兼容模式行在开启时显示齿轮小图标,弹窗内用数字行设置下移 距离(复用 FeatureSettingsRows 行控件与提交钳制)。 --- README.md | 2 +- README_EN.md | 2 +- src/client/SideCardSection.module.css | 26 +++++++++- src/client/SideCardSection.tsx | 68 +++++++++++++++++++++++++-- src/client/Sidebar.tsx | 26 +++++++--- src/client/locales.ts | 4 ++ src/client/prefs.ts | 6 ++- src/client/sidebar.module.css | 11 +++-- src/config.ts | 7 +++ src/prefs-shared.ts | 18 +++++++ tests/plugin-shape.spec.ts | 6 ++- tests/side-card-section.spec.tsx | 31 +++++++++++- tests/smoke.spec.ts | 1 + tests/unit.spec.ts | 24 ++++++++-- 14 files changed, 204 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index dbe2ca1..2f6d76f 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ | 功能 | 说明 | 截图 | |---|---|---| -| 📐 位置兼容模式 | 设置页新增「位置兼容模式」开关:为 Windows 右上角原生标题栏预留顶部空间,侧边栏按钮与内容整体下移(默认关闭) | | +| 📐 位置兼容模式 | 设置页新增「位置兼容模式」开关:为 Windows 右上角原生标题栏预留顶部空间,侧边栏按钮与内容整体下移(默认关闭);下移距离可在齿轮弹窗中自定义(0–120px) | | | 🔌 服务化基座 | 完整类型导出 + `version`/`features` 能力探测、状态订阅(`getSnapshot`/`subscribeState`)、tab 角标、`onOpen`/`onActivate`/`onClose` 生命周期回调、`updateTab`/`activateTab`/`openFile`、定向打开、`meta` 跨刷新持久化、插件自有设置(`pluginToggles`/`render`)、外链点击目标认领(`urlTarget`) | 服务化基座截图 | | ➕ 添加插件 | 设置页「推荐插件目录」+ 一键复制安装命令;内置 Office 预览迁至推荐插件 | 添加插件截图 | | 🖱️ 标签页滚轮 | 标签页栏支持鼠标滚轮横向滚动 | | diff --git a/README_EN.md b/README_EN.md index d6817f5..38e258d 100644 --- a/README_EN.md +++ b/README_EN.md @@ -38,7 +38,7 @@ | Feature | Description | Screenshot | |---|---|---| -| 📐 Position compat mode | New "Position compatibility mode" setting: reserves top space for the native Windows title bar (top-right) so the sidebar buttons and content sit below it (off by default) | | +| 📐 Position compat mode | New "Position compatibility mode" setting: reserves top space for the native Windows title bar (top-right) so the sidebar buttons and content sit below it (off by default); the shift distance is customizable in the gear popup (0–120px) | | | 🔌 Service API base | Complete type exports + `version`/`features` capability detection, state subscription (`getSnapshot`/`subscribeState`), tab `badge`, `onOpen`/`onActivate`/`onClose` lifecycle callbacks, `updateTab`/`activateTab`/`openFile`, targeted open, `meta` persisted across reloads, plugin-owned settings (`pluginToggles`/`render`), external-link claim (`urlTarget`) | Service API base screenshot | | ➕ Add Plugins | Recommended plugin catalog in settings + one-click copy install command; built-in Office preview moved to the recommended plugin | Add Plugins screenshot | | 🖱️ Tab-bar scroll | Mouse-wheel horizontal scrolling on the tab bar | | diff --git a/src/client/SideCardSection.module.css b/src/client/SideCardSection.module.css index 18f9823..54aba59 100644 --- a/src/client/SideCardSection.module.css +++ b/src/client/SideCardSection.module.css @@ -126,7 +126,8 @@ } .cardMain:focus-visible, -.cardGear:focus-visible { +.cardGear:focus-visible, +.rowGear:focus-visible { outline: 2px solid var(--dsw-alias-border-l4); outline-offset: 2px; } @@ -266,6 +267,29 @@ background: var(--dsw-alias-interactive-bg-hover); } +/* The gear icon button on a general settings ROW (the position-compat + row's "customize the strip height" entry): the same 16px icon-button + recipe as the card gear, sitting in the row's control area. */ +.rowGear { + display: inline-flex; + align-items: center; + justify-content: center; + width: 22px; + height: 22px; + padding: 0; + border: none; + border-radius: 50%; + background: transparent; + color: var(--dsw-alias-label-tertiary); + cursor: pointer; + flex: none; +} + +.rowGear:hover { + color: var(--dsw-alias-label-primary); + background: var(--dsw-alias-interactive-bg-hover); +} + /* Settings-row recipe (the DSH General section rows): title/desc left, control right, hairline separators (stripped on the last row). */ .row { diff --git a/src/client/SideCardSection.tsx b/src/client/SideCardSection.tsx index 410cd1a..72df028 100644 --- a/src/client/SideCardSection.tsx +++ b/src/client/SideCardSection.tsx @@ -52,6 +52,8 @@ import type {} from '@deepseek-ai/dsh-client-ui-settings/client' import type { PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots' import { clampWidthPercent, + TITLE_BAR_STRIP_MAX, + TITLE_BAR_STRIP_MIN, WIDTH_PERCENT_MAX, WIDTH_PERCENT_MIN, type SidebarPrefs, @@ -380,6 +382,8 @@ export function SideCardSection({ store, service }: SideCardSectionProps) { const [error, setError] = useState(null) // Which feature's secondary settings popup is open (null = closed). const [settingsFor, setSettingsFor] = useState(null) + // Whether the position-compat strip popup (the gear on the 常规 row) is open. + const [stripSettingsOpen, setStripSettingsOpen] = useState(false) // Whether the "add plugin" modal (a dashed card at the end of the // 侧边栏内容 / 文件预览 grids) is open, and for which extension point // (null = closed). @@ -665,11 +669,30 @@ export function SideCardSection({ store, service }: SideCardSectionProps) { {t('settingsTitleBarTitle')} {t('settingsTitleBarDesc')} - { applyPref({ titleBarCompat: next }) }} - /> + + {/* + The position-compat row's gear (same popup pattern as the + feature cards): opens a Modal with the strip-height number row. + Hidden while the mode is off — its related setting is dormant + then (the feature-card convention). + */} + {prefs.titleBarCompat && ( + + )} + { applyPref({ titleBarCompat: next }) }} + /> + @@ -794,6 +817,41 @@ export function SideCardSection({ store, service }: SideCardSectionProps) { )} + {/* The position-compat strip popup (opened by the gear on the 常规 + row): one number row for the reserved strip height in px. Same + Modal chrome and row machinery as the feature popups — mounted + only while open (the Modal SSR rule above). */} + {stripSettingsOpen && ( + { setStripSettingsOpen(false) }} + title={t('settingsTitleBarTitle')} + description={t('settingsPopupDesc', { feature: t('settingsTitleBarTitle') })} + closeLabel={t('close')} + className={css.popupDialog} + footer={( + + )} + > + t('settingsTitleBarStripTitle'), + desc: () => t('settingsTitleBarStripDesc'), + min: TITLE_BAR_STRIP_MIN, + max: TITLE_BAR_STRIP_MAX, + unit: 'px', + }]} + prefs={prefs} + onToggle={onToggleSetting} + onCommit={onCommitSetting} + /> + + )} + {/* The "add plugin" modal (opened by the dashed cards above): declares the extension point of the clicked kind, opens the GitHub topic, and lists the matching recommended plugin catalog with per-entry diff --git a/src/client/Sidebar.tsx b/src/client/Sidebar.tsx index d4537e0..c450a36 100644 --- a/src/client/Sidebar.tsx +++ b/src/client/Sidebar.tsx @@ -172,15 +172,27 @@ export function Sidebar(props: { ctx: Context; store: SidebarStore }) { // window's top-right corner, OVER the web content. When the user enables // the pref, the body attribute lets sidebar.module.css drop the toggle // cluster below the strip and push the right panel's content below it. - // The attribute rides the snapshot's prefs, so flipping the setting - // re-renders and re-applies immediately; the cleanup removes it on - // unmount/boundary swap so a crashed sidebar never leaves it behind. + // The strip height is user-tunable (titleBarStripPx) and rides a CSS + // variable so the rules stay declarative. The attribute rides the + // snapshot's prefs, so flipping the setting re-renders and re-applies + // immediately; the cleanup removes both on unmount/boundary swap so a + // crashed sidebar never leaves them behind. const titleBarCompat = snapshot.prefs.titleBarCompat + const titleBarStrip = snapshot.prefs.titleBarStripPx useEffect(() => { - if (titleBarCompat) document.body.setAttribute('data-dsh-title-bar-compat', '') - else document.body.removeAttribute('data-dsh-title-bar-compat') - return () => { document.body.removeAttribute('data-dsh-title-bar-compat') } - }, [titleBarCompat]) + const root = document.documentElement + if (titleBarCompat) { + document.body.setAttribute('data-dsh-title-bar-compat', '') + root.style.setProperty('--dsh-title-bar-strip', `${titleBarStrip}px`) + } else { + document.body.removeAttribute('data-dsh-title-bar-compat') + root.style.removeProperty('--dsh-title-bar-strip') + } + return () => { + document.body.removeAttribute('data-dsh-title-bar-compat') + root.style.removeProperty('--dsh-title-bar-strip') + } + }, [titleBarCompat, titleBarStrip]) /** * Bottom-panel merge on narrow viewports: whenever a session is current diff --git a/src/client/locales.ts b/src/client/locales.ts index 5dc6c07..1ec4604 100644 --- a/src/client/locales.ts +++ b/src/client/locales.ts @@ -118,6 +118,8 @@ export const zh = { settingsOpenPathDesc: '在聊天里点击文件链接(工具行、产物列表、文件提及)时,在侧边栏编辑器中打开,不再调用系统默认应用', settingsTitleBarTitle: '位置兼容模式', settingsTitleBarDesc: '为 Windows 右上角的原生标题栏预留空间:侧边栏按钮与侧边栏内容整体下移,避免被标题栏遮挡', + settingsTitleBarStripTitle: '下移距离', + settingsTitleBarStripDesc: '标题栏条带高度:侧边栏按钮与内容下移的像素数(0–120,默认 40)', settingsSaveFailed: '保存失败', settingsConflict: '设置已被其他窗口修改,请重试', binaryNoPreview: '此文件类型不支持预览', @@ -337,6 +339,8 @@ export const en: Record = { settingsOpenPathDesc: 'Open file links in the chat (tool rows, produced files, mentions) in the sidebar editor instead of the system default app', settingsTitleBarTitle: 'Position compatibility mode', settingsTitleBarDesc: 'Reserve space for the native Windows title bar at the top-right so the sidebar buttons and content sit below it instead of underneath', + settingsTitleBarStripTitle: 'Shift distance', + settingsTitleBarStripDesc: 'Title-bar strip height: how far the sidebar buttons and content move down in px (0–120, default 40)', settingsSaveFailed: 'Failed to save', settingsConflict: 'The setting changed in another window — please retry', binaryNoPreview: 'This file type cannot be previewed', diff --git a/src/client/prefs.ts b/src/client/prefs.ts index ec92080..3bae810 100644 --- a/src/client/prefs.ts +++ b/src/client/prefs.ts @@ -11,12 +11,13 @@ import type { api } from './api.ts' import { clampTerminalFontSize, + clampTitleBarStrip, clampWidthPercent, SIDEBAR_PREFS_DEFAULTS, type SidebarPrefs, } from '../prefs-shared.ts' -export { SIDEBAR_PREFS_DEFAULTS, clampTerminalFontSize, clampWidthPercent } +export { SIDEBAR_PREFS_DEFAULTS, clampTerminalFontSize, clampTitleBarStrip, clampWidthPercent } export type { SidebarPrefs } /** The settings wire face the preferences need (a subset of the plugin api). */ @@ -62,6 +63,9 @@ export function parsePrefs(value: unknown): SidebarPrefs { titleBarCompat: typeof record.titleBarCompat === 'boolean' ? record.titleBarCompat : SIDEBAR_PREFS_DEFAULTS.titleBarCompat, + titleBarStripPx: typeof record.titleBarStripPx === 'number' && Number.isFinite(record.titleBarStripPx) + ? clampTitleBarStrip(record.titleBarStripPx) + : SIDEBAR_PREFS_DEFAULTS.titleBarStripPx, htmlViewerNoSandbox: typeof record.htmlViewerNoSandbox === 'boolean' ? record.htmlViewerNoSandbox : SIDEBAR_PREFS_DEFAULTS.htmlViewerNoSandbox, diff --git a/src/client/sidebar.module.css b/src/client/sidebar.module.css index 0d765b9..5560abd 100644 --- a/src/client/sidebar.module.css +++ b/src/client/sidebar.module.css @@ -217,18 +217,19 @@ pref, the sidebar yields the strip: the toggle cluster drops below it and the right panel's content starts below it — the panel's surface still spans the full height, so the caption buttons float over empty sidebar - fill (the VSCode hidden-title-bar look). The bottom panel is unaffected: - it sits at the window's bottom, never under the top strip. The Sidebar - shell toggles the body attribute from the prefs. */ + fill (the VSCode hidden-title-bar look). The strip height is user-tunable + (`--dsh-title-bar-strip`, written by the Sidebar shell from + `titleBarStripPx`; 40px fallback). The bottom panel is unaffected: it + sits at the window's bottom, never under the top strip. */ :global(body[data-dsh-title-bar-compat]) .toggleCluster { - top: 43px; /* 40px caption strip + the original 3px inset */ + top: calc(var(--dsh-title-bar-strip, 40px) + 3px); } :global(body[data-dsh-title-bar-compat]) .panel { /* The caption strip is drawn over the panel's own top edge: keep the surface full-height and push the content (the tab strip) below the strip instead. .panelResize is absolutely positioned and unaffected. */ - padding-top: 40px; + padding-top: var(--dsh-title-bar-strip, 40px); } /* The shared corner (both panels open): the intersection of the right diff --git a/src/config.ts b/src/config.ts index 18582fb..a5bc8c2 100644 --- a/src/config.ts +++ b/src/config.ts @@ -12,6 +12,9 @@ import { TERMINAL_FONT_SIZE_DEFAULT, TERMINAL_FONT_SIZE_MAX, TERMINAL_FONT_SIZE_MIN, + TITLE_BAR_STRIP_DEFAULT, + TITLE_BAR_STRIP_MAX, + TITLE_BAR_STRIP_MIN, WIDTH_PERCENT_DEFAULT, WIDTH_PERCENT_MAX, WIDTH_PERCENT_MIN, @@ -24,6 +27,9 @@ export { TERMINAL_FONT_SIZE_DEFAULT, TERMINAL_FONT_SIZE_MAX, TERMINAL_FONT_SIZE_MIN, + TITLE_BAR_STRIP_DEFAULT, + TITLE_BAR_STRIP_MAX, + TITLE_BAR_STRIP_MIN, WIDTH_PERCENT_DEFAULT, WIDTH_PERCENT_MAX, WIDTH_PERCENT_MIN, @@ -92,6 +98,7 @@ export const PrefsSchema: z = z.object({ terminalFontSize: z.number().step(1).min(TERMINAL_FONT_SIZE_MIN).max(TERMINAL_FONT_SIZE_MAX).default(TERMINAL_FONT_SIZE_DEFAULT), interceptOpenPath: z.boolean().default(true), titleBarCompat: z.boolean().default(false), + titleBarStripPx: z.number().step(1).min(TITLE_BAR_STRIP_MIN).max(TITLE_BAR_STRIP_MAX).default(TITLE_BAR_STRIP_DEFAULT), htmlViewerNoSandbox: z.boolean().default(false), htmlViewerDefaultUnsafe: z.boolean().default(false), browserNoSandbox: z.boolean().default(false), diff --git a/src/prefs-shared.ts b/src/prefs-shared.ts index c0f04ed..42986a4 100644 --- a/src/prefs-shared.ts +++ b/src/prefs-shared.ts @@ -68,6 +68,13 @@ export interface SidebarPrefs { * below it. Off by default — the sidebar layout is untouched. */ titleBarCompat: boolean + /** + * The reserved top strip height in px when `titleBarCompat` is on + * (0–120, default 40). Drives the `--dsh-title-bar-strip` CSS variable: + * the toggle cluster drops `strip + 3px` and the right panel's content + * starts `strip` px below its top edge. + */ + titleBarStripPx: number /** * Whether the HTML previewer drops its sandboxed iframe. Sandbox ON (the * default) renders previewed HTML in an opaque-origin iframe that cannot @@ -156,6 +163,11 @@ export const TERMINAL_FONT_SIZE_MIN = 9 export const TERMINAL_FONT_SIZE_MAX = 32 export const TERMINAL_FONT_SIZE_DEFAULT = 13 +/** Range contract of {@link SidebarPrefs.titleBarStripPx}. */ +export const TITLE_BAR_STRIP_MIN = 0 +export const TITLE_BAR_STRIP_MAX = 120 +export const TITLE_BAR_STRIP_DEFAULT = 40 + /** Fallback prefs used whenever the settings document is unreachable or malformed. */ export const SIDEBAR_PREFS_DEFAULTS: SidebarPrefs = { openByDefault: true, @@ -168,6 +180,7 @@ export const SIDEBAR_PREFS_DEFAULTS: SidebarPrefs = { terminalFontSize: TERMINAL_FONT_SIZE_DEFAULT, interceptOpenPath: true, titleBarCompat: false, + titleBarStripPx: TITLE_BAR_STRIP_DEFAULT, htmlViewerNoSandbox: false, htmlViewerDefaultUnsafe: false, browserNoSandbox: false, @@ -188,3 +201,8 @@ export function clampWidthPercent(value: number): number { export function clampTerminalFontSize(value: number): number { return Math.min(TERMINAL_FONT_SIZE_MAX, Math.max(TERMINAL_FONT_SIZE_MIN, Math.round(value))) } + +/** Clamp one title-bar strip height into the contract range (shared by schema and client reads). */ +export function clampTitleBarStrip(value: number): number { + return Math.min(TITLE_BAR_STRIP_MAX, Math.max(TITLE_BAR_STRIP_MIN, Math.round(value))) +} diff --git a/tests/plugin-shape.spec.ts b/tests/plugin-shape.spec.ts index 8d0a0a7..2110edb 100644 --- a/tests/plugin-shape.spec.ts +++ b/tests/plugin-shape.spec.ts @@ -54,8 +54,10 @@ describe('dsh-better-sidebar plugin export shape', () => { // and 13px. expect(resolved.terminalFontFamily).toBe('') expect(resolved.terminalFontSize).toBe(13) - // The position-compat mode defaults OFF (the normal layout is default). + // The position-compat mode defaults OFF (the normal layout is default), + // with the strip defaulting to 40px. expect(resolved.titleBarCompat).toBe(false) + expect(resolved.titleBarStripPx).toBe(40) // The enable-switch maps resolve to {} (everything on) for old documents. expect(resolved.tabsEnabled).toEqual({}) expect(resolved.viewersEnabled).toEqual({}) @@ -65,6 +67,6 @@ describe('dsh-better-sidebar plugin export shape', () => { const overridden = (PrefsSchema as unknown as { (input: Record | undefined): Record })({ openByDefault: false, defaultWidthPercent: 45 }) - expect(overridden).toEqual({ openByDefault: false, defaultWidthPercent: 45, autoOpenSubagent: true, autoOpenJobs: true, agentTerminalTools: false, bottomPanelAutoTerminal: true, terminalFontFamily: '', terminalFontSize: 13, interceptOpenPath: true, titleBarCompat: false, htmlViewerNoSandbox: false, htmlViewerDefaultUnsafe: false, browserNoSandbox: false, browserInterceptLinks: true, browserInterceptHttp: true, browserInterceptHttps: false, tabsEnabled: {}, viewersEnabled: {}, pluginSettings: {} }) + expect(overridden).toEqual({ openByDefault: false, defaultWidthPercent: 45, autoOpenSubagent: true, autoOpenJobs: true, agentTerminalTools: false, bottomPanelAutoTerminal: true, terminalFontFamily: '', terminalFontSize: 13, interceptOpenPath: true, titleBarCompat: false, titleBarStripPx: 40, htmlViewerNoSandbox: false, htmlViewerDefaultUnsafe: false, browserNoSandbox: false, browserInterceptLinks: true, browserInterceptHttp: true, browserInterceptHttps: false, tabsEnabled: {}, viewersEnabled: {}, pluginSettings: {} }) }) }) diff --git a/tests/side-card-section.spec.tsx b/tests/side-card-section.spec.tsx index c7f860c..78391ea 100644 --- a/tests/side-card-section.spec.tsx +++ b/tests/side-card-section.spec.tsx @@ -163,11 +163,15 @@ describe('SideCardSection declarative inventory', () => { // checkbox count stays at 2 while the total checkbox count is 3. expect(html.match(/type="checkbox"/g)?.length).toBe(3) expect(html.match(/checked=""/g)?.length).toBe(2) + // The row's gear (customize the shift distance) is dormant while the + // mode is off — the feature-card convention. + expect(html).not.toContain('Position compatibility mode Feature settings') - // When the pref is on, the new switch is checked too. + // When the pref is on, the new switch is checked and the gear appears. store.setPrefs({ ...store.getPrefs(), titleBarCompat: true }) html = renderSection(store, service) expect(html.match(/checked=""/g)?.length).toBe(3) + expect(html).toContain('aria-label="Position compatibility mode Feature settings"') }) }) @@ -247,6 +251,31 @@ describe('FeatureSettingsRows (the secondary settings popup body)', () => { expect(html).toContain('px') expect(html).not.toContain('type="checkbox"') }) + + it('renders the title-bar strip row: the pref value, the 0–120 bounds and the px suffix', () => { + const html = renderToString(createElement(FeatureSettingsRows, { + toggles: [{ + key: 'titleBarStripPx', + type: 'number', + title: () => 'Shift distance', + desc: () => 'Title-bar strip height in px', + min: 0, + max: 120, + unit: 'px', + }], + prefs: { ...prefs, titleBarStripPx: 64 }, + onToggle: () => {}, + onCommit: () => '64', + })) + expect(html).toContain('Shift distance') + expect(html).toContain('Title-bar strip height in px') + expect(html).toContain('type="number"') + expect(html).toContain('value="64"') + expect(html).toContain('min="0"') + expect(html).toContain('max="120"') + expect(html).toContain('px') + expect(html).not.toContain('type="checkbox"') + }) }) describe('mergePluginSetting (v0.12.0, codex review fix)', () => { diff --git a/tests/smoke.spec.ts b/tests/smoke.spec.ts index bacd3a2..6722662 100644 --- a/tests/smoke.spec.ts +++ b/tests/smoke.spec.ts @@ -521,6 +521,7 @@ describe('side card settings routes', () => { terminalFontSize: 13, interceptOpenPath: true, titleBarCompat: false, + titleBarStripPx: 40, htmlViewerNoSandbox: false, htmlViewerDefaultUnsafe: false, browserNoSandbox: false, diff --git a/tests/unit.spec.ts b/tests/unit.spec.ts index 4229979..d49c2f7 100644 --- a/tests/unit.spec.ts +++ b/tests/unit.spec.ts @@ -1141,6 +1141,7 @@ describe('side card preferences', () => { terminalFontSize: 13, interceptOpenPath: true, titleBarCompat: false, + titleBarStripPx: 40, htmlViewerNoSandbox: false, htmlViewerDefaultUnsafe: false, browserNoSandbox: false, @@ -1166,6 +1167,7 @@ describe('side card preferences', () => { terminalFontSize: 13, interceptOpenPath: true, titleBarCompat: false, + titleBarStripPx: 40, htmlViewerNoSandbox: false, htmlViewerDefaultUnsafe: false, browserNoSandbox: false, @@ -1191,6 +1193,7 @@ describe('side card preferences', () => { terminalFontSize: 13, interceptOpenPath: true, titleBarCompat: false, + titleBarStripPx: 40, htmlViewerNoSandbox: false, htmlViewerDefaultUnsafe: false, browserNoSandbox: false, @@ -1237,6 +1240,19 @@ describe('side card preferences', () => { expect((await loadPrefs(wire({ titleBarCompat: true }))).titleBarCompat).toBe(true) }) + it('defaults titleBarStripPx to 40 and clamps stored values into the contract range', async () => { + // Absent or malformed → 40 (the strip default). + expect((await loadPrefs(wire({}))).titleBarStripPx).toBe(40) + expect((await loadPrefs(wire({ titleBarStripPx: 'yes' }))).titleBarStripPx).toBe(40) + // Out-of-range numbers clamp into 0–120. + expect((await loadPrefs(wire({ titleBarStripPx: -5 }))).titleBarStripPx).toBe(0) + expect((await loadPrefs(wire({ titleBarStripPx: 200 }))).titleBarStripPx).toBe(120) + expect((await loadPrefs(wire({ titleBarStripPx: 47.6 }))).titleBarStripPx).toBe(48) + // In-range values survive verbatim. + expect((await loadPrefs(wire({ titleBarStripPx: 0 }))).titleBarStripPx).toBe(0) + expect((await loadPrefs(wire({ titleBarStripPx: 64 }))).titleBarStripPx).toBe(64) + }) + it('defaults the link-takeover protocol flags: http on, https off, master on', async () => { // Absent or malformed → the per-protocol defaults. expect((await loadPrefs(wire({}))).browserInterceptLinks).toBe(true) @@ -1286,9 +1302,9 @@ describe('side card preferences', () => { const store = createSidebarStore() // Node environment: no window → the width falls back to PANEL_DEFAULT, // while the open flag still follows the preference. - store.setPrefs({ openByDefault: false, defaultWidthPercent: 45, autoOpenSubagent: true, autoOpenJobs: true, agentTerminalTools: false, bottomPanelAutoTerminal: true, terminalFontFamily: '', terminalFontSize: 13, interceptOpenPath: true, titleBarCompat: false, htmlViewerNoSandbox: false, htmlViewerDefaultUnsafe: false, browserNoSandbox: false, browserInterceptLinks: true, browserInterceptHttp: true, browserInterceptHttps: false, tabsEnabled: {}, viewersEnabled: {}, pluginSettings: {} }) + store.setPrefs({ openByDefault: false, defaultWidthPercent: 45, autoOpenSubagent: true, autoOpenJobs: true, agentTerminalTools: false, bottomPanelAutoTerminal: true, terminalFontFamily: '', terminalFontSize: 13, interceptOpenPath: true, titleBarCompat: false, titleBarStripPx: 40, htmlViewerNoSandbox: false, htmlViewerDefaultUnsafe: false, browserNoSandbox: false, browserInterceptLinks: true, browserInterceptHttp: true, browserInterceptHttps: false, tabsEnabled: {}, viewersEnabled: {}, pluginSettings: {} }) store.setSession('fresh-session') - expect(store.getPrefs()).toEqual({ openByDefault: false, defaultWidthPercent: 45, autoOpenSubagent: true, autoOpenJobs: true, agentTerminalTools: false, bottomPanelAutoTerminal: true, terminalFontFamily: '', terminalFontSize: 13, interceptOpenPath: true, titleBarCompat: false, htmlViewerNoSandbox: false, htmlViewerDefaultUnsafe: false, browserNoSandbox: false, browserInterceptLinks: true, browserInterceptHttp: true, browserInterceptHttps: false, tabsEnabled: {}, viewersEnabled: {}, pluginSettings: {} }) + expect(store.getPrefs()).toEqual({ openByDefault: false, defaultWidthPercent: 45, autoOpenSubagent: true, autoOpenJobs: true, agentTerminalTools: false, bottomPanelAutoTerminal: true, terminalFontFamily: '', terminalFontSize: 13, interceptOpenPath: true, titleBarCompat: false, titleBarStripPx: 40, htmlViewerNoSandbox: false, htmlViewerDefaultUnsafe: false, browserNoSandbox: false, browserInterceptLinks: true, browserInterceptHttp: true, browserInterceptHttps: false, tabsEnabled: {}, viewersEnabled: {}, pluginSettings: {} }) const snapshot = store.getSnapshot() expect(snapshot.sessionId).toBe('fresh-session') expect(snapshot.state?.panelOpen).toBe(false) @@ -1324,7 +1340,7 @@ describe('side card preferences', () => { it('skips the default explorer tab when the explorer type is disabled', () => { const store = createSidebarStore() - store.setPrefs({ openByDefault: true, defaultWidthPercent: 30, autoOpenSubagent: true, autoOpenJobs: true, agentTerminalTools: false, bottomPanelAutoTerminal: true, terminalFontFamily: '', terminalFontSize: 13, interceptOpenPath: true, titleBarCompat: false, htmlViewerNoSandbox: false, htmlViewerDefaultUnsafe: false, browserNoSandbox: false, browserInterceptLinks: true, browserInterceptHttp: true, browserInterceptHttps: false, tabsEnabled: { explorer: false }, viewersEnabled: {}, pluginSettings: {} }) + store.setPrefs({ openByDefault: true, defaultWidthPercent: 30, autoOpenSubagent: true, autoOpenJobs: true, agentTerminalTools: false, bottomPanelAutoTerminal: true, terminalFontFamily: '', terminalFontSize: 13, interceptOpenPath: true, titleBarCompat: false, titleBarStripPx: 40, htmlViewerNoSandbox: false, htmlViewerDefaultUnsafe: false, browserNoSandbox: false, browserInterceptLinks: true, browserInterceptHttp: true, browserInterceptHttps: false, tabsEnabled: { explorer: false }, viewersEnabled: {}, pluginSettings: {} }) store.setSession('no-explorer') const state = store.getSnapshot().state! const tabs = allLeaves(state.splits).flatMap(leaf => leaf.tabs) @@ -1332,7 +1348,7 @@ describe('side card preferences', () => { expect(state.splits.kind).toBe('leaf') // Re-enabling seeds the explorer tab again. const openStore = createSidebarStore() - openStore.setPrefs({ openByDefault: true, defaultWidthPercent: 30, autoOpenSubagent: true, autoOpenJobs: true, agentTerminalTools: false, bottomPanelAutoTerminal: true, terminalFontFamily: '', terminalFontSize: 13, interceptOpenPath: true, titleBarCompat: false, htmlViewerNoSandbox: false, htmlViewerDefaultUnsafe: false, browserNoSandbox: false, browserInterceptLinks: true, browserInterceptHttp: true, browserInterceptHttps: false, tabsEnabled: {}, viewersEnabled: {}, pluginSettings: {} }) + openStore.setPrefs({ openByDefault: true, defaultWidthPercent: 30, autoOpenSubagent: true, autoOpenJobs: true, agentTerminalTools: false, bottomPanelAutoTerminal: true, terminalFontFamily: '', terminalFontSize: 13, interceptOpenPath: true, titleBarCompat: false, titleBarStripPx: 40, htmlViewerNoSandbox: false, htmlViewerDefaultUnsafe: false, browserNoSandbox: false, browserInterceptLinks: true, browserInterceptHttp: true, browserInterceptHttps: false, tabsEnabled: {}, viewersEnabled: {}, pluginSettings: {} }) openStore.setSession('with-explorer') const openTabs = allLeaves(openStore.getSnapshot().state!.splits).flatMap(leaf => leaf.tabs) expect(openTabs.map(tab => tab.type)).toEqual(['explorer'])