feat: 位置兼容模式支持自定义下移距离(齿轮弹窗数字输入)

titleBarStripPx 新偏好(0–120px,默认 40):侧边栏按钮与内容的下移量改为
CSS 变量 --dsh-title-bar-strip 驱动,Sidebar shell 随 prefs 写入。设置页
「常规」的位置兼容模式行在开启时显示齿轮小图标,弹窗内用数字行设置下移
距离(复用 FeatureSettingsRows 行控件与提交钳制)。
This commit is contained in:
Menghuan1918
2026-08-15 15:46:56 +08:00
parent 648ff27b5f
commit 8ad1766390
14 changed files with 204 additions and 28 deletions
+1 -1
View File
@@ -38,7 +38,7 @@
| 功能 | 说明 | 截图 |
|---|---|---|
| 📐 位置兼容模式 | 设置页新增「位置兼容模式」开关:为 Windows 右上角原生标题栏预留顶部空间,侧边栏按钮与内容整体下移(默认关闭) | |
| 📐 位置兼容模式 | 设置页新增「位置兼容模式」开关:为 Windows 右上角原生标题栏预留顶部空间,侧边栏按钮与内容整体下移(默认关闭);下移距离可在齿轮弹窗中自定义(0–120px) | |
| 🔌 服务化基座 | 完整类型导出 + `version`/`features` 能力探测、状态订阅(`getSnapshot`/`subscribeState`)、tab 角标、`onOpen`/`onActivate`/`onClose` 生命周期回调、`updateTab`/`activateTab`/`openFile`、定向打开、`meta` 跨刷新持久化、插件自有设置(`pluginToggles`/`render`)、外链点击目标认领(`urlTarget`) | <a href="https://github.com/user-attachments/assets/946f7028-4967-461e-a750-d1b5056b62d0"><img width="800" alt="服务化基座截图" src="https://github.com/user-attachments/assets/946f7028-4967-461e-a750-d1b5056b62d0" /></a> |
| ➕ 添加插件 | 设置页「推荐插件目录」+ 一键复制安装命令;内置 Office 预览迁至推荐插件 | <a href="https://github.com/user-attachments/assets/d4385b7e-aab4-425d-a5c4-2da5da81a34e"><img width="800" alt="添加插件截图" src="https://github.com/user-attachments/assets/d4385b7e-aab4-425d-a5c4-2da5da81a34e" /></a> |
| 🖱️ 标签页滚轮 | 标签页栏支持鼠标滚轮横向滚动 | |
+1 -1
View File
@@ -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`) | <a href="https://github.com/user-attachments/assets/946f7028-4967-461e-a750-d1b5056b62d0"><img width="480" alt="Service API base screenshot" src="https://github.com/user-attachments/assets/946f7028-4967-461e-a750-d1b5056b62d0" /></a> |
| ➕ Add Plugins | Recommended plugin catalog in settings + one-click copy install command; built-in Office preview moved to the recommended plugin | <a href="https://github.com/user-attachments/assets/d4385b7e-aab4-425d-a5c4-2da5da81a34e"><img width="480" alt="Add Plugins screenshot" src="https://github.com/user-attachments/assets/d4385b7e-aab4-425d-a5c4-2da5da81a34e" /></a> |
| 🖱️ Tab-bar scroll | Mouse-wheel horizontal scrolling on the tab bar | |
+25 -1
View File
@@ -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 {
+63 -5
View File
@@ -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<string | null>(null)
// Which feature's secondary settings popup is open (null = closed).
const [settingsFor, setSettingsFor] = useState<TabDescriptor | FileViewerDescriptor | null>(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) {
<span className={css.title}>{t('settingsTitleBarTitle')}</span>
<span className={css.desc}>{t('settingsTitleBarDesc')}</span>
</span>
<Switch
label={t('settingsTitleBarTitle')}
checked={prefs.titleBarCompat}
onChange={(next) => { applyPref({ titleBarCompat: next }) }}
/>
<span className={css.control}>
{/*
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 && (
<button
type="button"
className={css.rowGear}
aria-label={`${t('settingsTitleBarTitle')} ${t('settingsPopup')}`}
title={t('settingsPopup')}
onClick={() => { setStripSettingsOpen(true) }}
>
<IconSettingsOutline16 size={14} />
</button>
)}
<Switch
label={t('settingsTitleBarTitle')}
checked={prefs.titleBarCompat}
onChange={(next) => { applyPref({ titleBarCompat: next }) }}
/>
</span>
</div>
</div>
@@ -794,6 +817,41 @@ export function SideCardSection({ store, service }: SideCardSectionProps) {
</Modal>
)}
{/* 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 && (
<Modal
open
onClose={() => { setStripSettingsOpen(false) }}
title={t('settingsTitleBarTitle')}
description={t('settingsPopupDesc', { feature: t('settingsTitleBarTitle') })}
closeLabel={t('close')}
className={css.popupDialog}
footer={(
<button type="button" className={css.done} onClick={() => { setStripSettingsOpen(false) }}>
{t('settingsDone')}
</button>
)}
>
<FeatureSettingsRows
toggles={[{
key: 'titleBarStripPx',
type: 'number',
title: () => t('settingsTitleBarStripTitle'),
desc: () => t('settingsTitleBarStripDesc'),
min: TITLE_BAR_STRIP_MIN,
max: TITLE_BAR_STRIP_MAX,
unit: 'px',
}]}
prefs={prefs}
onToggle={onToggleSetting}
onCommit={onCommitSetting}
/>
</Modal>
)}
{/* 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
+19 -7
View File
@@ -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
+4
View File
@@ -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<keyof typeof zh, string> = {
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',
+5 -1
View File
@@ -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,
+6 -5
View File
@@ -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
+7
View File
@@ -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<SidebarPrefs> = 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),
+18
View File
@@ -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)))
}
+4 -2
View File
@@ -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<string, unknown> | undefined): Record<string, unknown>
})({ 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: {} })
})
})
+30 -1
View File
@@ -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)', () => {
+1
View File
@@ -521,6 +521,7 @@ describe('side card settings routes', () => {
terminalFontSize: 13,
interceptOpenPath: true,
titleBarCompat: false,
titleBarStripPx: 40,
htmlViewerNoSandbox: false,
htmlViewerDefaultUnsafe: false,
browserNoSandbox: false,
+20 -4
View File
@@ -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'])