feat(sidebar): 桌面兼容三方案——自动检测(WCO 标准几何)/壳预设/自定义 CSS (#284)

* feat(sidebar): 桌面兼容三方案——自动检测(WCO 标准几何)/壳预设/自定义 CSS

核心只保留 Web 标准机制,壳专属适配全部下沉到用户空间(KISS):
- prefs 新增 titleBarScheme(默认 auto)/titleBarPresetId/customCss,旧 titleBarCompat
  布尔自动迁移(true→custom 保留 stripPx,false/缺失→auto)
- wco.ts:navigator.windowControlsOverlay 反应式几何(geometrychange 实时更新;
  visible=false 的幽灵 API 视为缺失),实现 #257
- titlebar-strip.ts 唯一 strip 取值链:WCO → dsh-desktop-titlebar-inset 契约参数
  → 预设 stripFor → 手动 px → 0;desktop-env.ts 移除 win32 32px 硬编码
- shell-presets.ts 数据驱动预设(准入:issue/PR 提及 + 100+ star;v1 仅
  anywhere-labs DSH Desktop:darwin 20px / win32 无 WCO 时 32px),opt-in 手动
- 预设/自定义 CSS 注入(data-dsh-preset-css / data-dsh-custom-css 标签),
  chrome 补稳定寻址面 data-dsh-toggle-cluster / data-dsh-panel
- 交互 chrome 统一 -webkit-app-region: no-drag(吸收 #111/#153)
- layout.css 底栏推挤复合锚点([data-pane] 与 :has(> [data-slot]) 双保险,
  rc.8 实测前者失效、后者命中,吸收 #226 思路)
- 设置 UI:位置兼容模式齿轮弹窗改三方案(自动检测/壳兼容方案/自定义方案)
- 测试:wco/titlebar-strip/shell-presets 单测 + prefs 迁移 + e2e 5 场景
  (保守 auto/WCO 反应式/预设/自定义 CSS/复合锚点),挂载门禁 12/12 全绿

核心 grep 无壳专属类名;为某壳做的兼容在另一壳会再坏——预设与自定义 CSS
是唯一出路(AGENTS.md §8 契约已更新)。

* refine(sidebar): 位置兼容模式改主行下拉四选项——自动检测/DSH官方Web/壳预设/自定义

按用户反馈定稿 UI:
- 主行下拉:自动检测(默认)/ DSH官方Web(显式零适配,连 WCO 也强制 0)/
  各壳预设(命中环境带「已检测」后缀,仅提示)/ 自定义方案(保留齿轮弹窗:
  下移距离 + 自定义 CSS)
- 旧文档迁移:titleBarCompat=true 或 titleBarStripPx 非默认值(此前已有配置)
  → 默认落到 custom 方案保留用户数值;干净文档 → auto
- prefs 新增 'web' 方案;strip 取值链最优先处理 web→0
- 测试:prefs 迁移(stripPx 48→custom / 40→auto)、web 强制 0、下拉渲染断言
- 单测 693 通过,挂载 e2e 12/12 全绿

* refine(sidebar): 方案下拉复用 SelectMenu 组件,不用原生 <select>

- 抽出可复用 SelectMenu(primitives Menu:锚点按钮 + 下拉选项卡,支持
  icon/desc/多选),SelectRow 改为其薄壳
- 位置兼容模式方案下拉改用 SelectMenu;选项带描述(自动检测/DSH官方Web/
  各壳预设(命中带「已检测」后缀)/自定义方案)
- 移除原生 select 样式;单测 693 通过,挂载 e2e 12/12 全绿
This commit is contained in:
Atlas Zeng
2026-08-21 01:28:17 +08:00
committed by GitHub
parent 4e52923970
commit 50a888845f
26 changed files with 1312 additions and 181 deletions
+26 -9
View File
@@ -1,7 +1,10 @@
/**
* Desktop-shell detection tests: URL stamps (dsh-desktop-mode/platform)
* from the official Electron shell, the preload marker
* (__DSH_DESKTOP_FILE_PATH__), and the win32 advanced overlay adaptation.
* (__DSH_DESKTOP_FILE_PATH__), and the `dsh-desktop-titlebar-inset`
* contract parameter. Geometry ADAPTATION is deliberately NOT here — the
* strip resolution chain lives in titlebar-strip.ts (standard WCO first,
* see wco.spec.ts), keeping this module a pure environment reporter.
*/
import { beforeEach, describe, expect, it } from 'vitest'
import './browser-globals.ts'
@@ -18,25 +21,25 @@ beforeEach(() => {
})
describe('parseDesktopEnv', () => {
it('reports a plain browser page as non-desktop', () => {
expect(parseDesktopEnv()).toEqual({ desktop: false, mode: null, platform: null, win32OverlayTop: 0 })
it('reports a plain browser page as non-desktop with no inset', () => {
expect(parseDesktopEnv()).toEqual({ desktop: false, mode: null, platform: null, titlebarInset: 0 })
})
it('parses win32 advanced stamps with the 32px window-control overlay', () => {
it('parses win32 advanced stamps (no overlay guess — geometry comes from WCO/preset)', () => {
setSearch('?dsh-desktop-mode=advanced&dsh-desktop-platform=win32')
const env = parseDesktopEnv()
expect(env.desktop).toBe(true)
expect(env.mode).toBe('advanced')
expect(env.platform).toBe('win32')
expect(env.win32OverlayTop).toBe(32)
expect(env.titlebarInset).toBe(0)
})
it('parses darwin advanced stamps without an overlay (traffic lights are top-left)', () => {
it('parses darwin advanced stamps', () => {
setSearch('?dsh-desktop-mode=advanced&dsh-desktop-platform=darwin')
const env = parseDesktopEnv()
expect(env.desktop).toBe(true)
expect(env.platform).toBe('darwin')
expect(env.win32OverlayTop).toBe(0)
expect(env.titlebarInset).toBe(0)
})
it('parses compatibility mode as desktop with the native frame (no adaptation)', () => {
@@ -44,7 +47,7 @@ describe('parseDesktopEnv', () => {
const env = parseDesktopEnv()
expect(env.desktop).toBe(true)
expect(env.mode).toBe('compatibility')
expect(env.win32OverlayTop).toBe(0)
expect(env.titlebarInset).toBe(0)
})
it('detects the desktop preload marker even without URL stamps', () => {
@@ -52,7 +55,7 @@ describe('parseDesktopEnv', () => {
const env = parseDesktopEnv()
expect(env.desktop).toBe(true)
expect(env.mode).toBeNull()
expect(env.win32OverlayTop).toBe(0)
expect(env.titlebarInset).toBe(0)
})
it('ignores unknown mode values (exotic shells keep plain-browser semantics)', () => {
@@ -61,6 +64,20 @@ describe('parseDesktopEnv', () => {
expect(parseDesktopEnv().desktop).toBe(false)
})
it('reads the documented titlebar-inset contract parameter (clamped 0–120)', () => {
setSearch('?dsh-desktop-mode=advanced&dsh-desktop-platform=win32&dsh-desktop-titlebar-inset=36')
expect(parseDesktopEnv().titlebarInset).toBe(36)
setSearch('?dsh-desktop-mode=advanced&dsh-desktop-titlebar-inset=200')
resetDesktopEnvForTests()
expect(parseDesktopEnv().titlebarInset).toBe(120)
setSearch('?dsh-desktop-mode=advanced&dsh-desktop-titlebar-inset=-5')
resetDesktopEnvForTests()
expect(parseDesktopEnv().titlebarInset).toBe(0)
setSearch('?dsh-desktop-mode=advanced&dsh-desktop-titlebar-inset=abc')
resetDesktopEnvForTests()
expect(parseDesktopEnv().titlebarInset).toBe(0)
})
it('memoizes across calls until the test hook resets', () => {
setSearch('?dsh-desktop-mode=advanced&dsh-desktop-platform=win32')
const first = parseDesktopEnv()
+31
View File
@@ -702,3 +702,34 @@ test('bottom panel never flashes full-width after a width drag release (issue #2
).toBeLessThanOrEqual(8)
}
})
test('the bottom-push anchor resolves through the composite selectors (at least one; same element when both)', async ({ page }) => {
// layout.css pushes the bottom panel via the center column. The selector
// is COMPOSITE on purpose: `[data-pane="conversation"]` (0.1.x naming)
// and `:has(> [data-slot="conversation"])` (rc.8-era naming) — HOST
// VERSIONS MAY RENAME THE ATTRIBUTE (issue #208 comment / PR #226), so
// the contract is "at least one resolves", and when both resolve they
// must hit the SAME element (otherwise the push would land twice).
await page.goto(BASE_URL, { waitUntil: 'domcontentloaded' })
await expect(page.locator('[data-dsh-better-sidebar]')).toBeAttached({ timeout: 90_000 })
const anchors = await page.evaluate(() => {
const a = document.querySelector('#root [data-dsh-frame] > [data-pane="conversation"]')
const b = document.querySelector('#root :has(> [data-slot="conversation"])')
const frame = document.querySelector('#root [data-dsh-frame]')
return {
a: a !== null,
b: b !== null,
same: a !== null && b !== null && a === b,
frameChildren: frame !== null
? [...frame.children].map(el => `${el.tagName}[${[...el.attributes].map(attr => attr.name).filter(name => name.startsWith('data-')).join(',')}]`)
: [],
}
})
expect(
anchors.a || anchors.b,
`at least one bottom-push anchor must resolve on this host (frame children: ${anchors.frameChildren.join(' / ') || 'no [data-dsh-frame]'})`,
).toBe(true)
if (anchors.a && anchors.b) {
expect(anchors.same, 'both selectors must hit the SAME center-column element').toBe(true)
}
})
+111 -9
View File
@@ -341,20 +341,122 @@ test('plugin mounts into the DSH shell and survives a built-in tab sweep', async
await page.screenshot({ path: 'test-results/mount-final.png' })
})
test('desktop shell stamps auto-enable the win32 title-bar compatibility mode', async ({ page }) => {
test('conservative auto: URL stamps alone never modify the layout; plugin chrome carries the stable data attributes', async ({ page }) => {
// The official DSH Desktop shell stamps every render URL with
// dsh-desktop-mode / dsh-desktop-platform (and exposes a preload marker).
// The win32 advanced shell reserves a 32px overlay for the window
// controls where the toggle cluster sits — the sidebar must auto-drop
// below it (body[data-dsh-title-bar-compat]) without any manual pref.
// dsh-desktop-mode / dsh-desktop-platform. Under the conservative AUTO
// scheme, shell stamps are REPORTS, not geometry: without the standard
// Window Controls Overlay API the layout must stay untouched (plain-web
// semantics) — the strip/body attribute appear only for real standard
// geometry (see the WCO scenario below) or an opt-in preset.
await page.goto(`${BASE_URL}?dsh-desktop-mode=advanced&dsh-desktop-platform=win32`, { waitUntil: 'domcontentloaded' })
await expect(page.locator('[data-dsh-better-sidebar]')).toBeAttached({ timeout: 90_000 })
await expect(
page.locator('body[data-dsh-title-bar-compat]'),
'win32 advanced shell must auto-enable title-bar compatibility',
).toBeAttached({ timeout: 90_000 })
// The strip variable must be sized to the shell's 32px overlay.
'stamps alone must NOT auto-enable title-bar compatibility under auto',
).toHaveCount(0)
await expect
.poll(() => page.evaluate(() => document.documentElement.style.getPropertyValue('--dsh-title-bar-strip')))
.toBe('32px')
.toBe('')
// The stable addressing surface for presets / custom CSS is mounted.
await expect(page.locator('[data-dsh-toggle-cluster]')).toBeAttached()
await expect(page.locator('[data-dsh-panel]').first()).toBeAttached()
// The plugin's interactive chrome opts out of Electron drag regions
// (issues #103/#111) — inert in plain browsers, present in the bundle
// (the bundler minifies the property's whitespace, so match loosely).
const hasNoDragRule = await page.evaluate(() => {
for (const tag of document.querySelectorAll('style')) {
if (tag.textContent !== null && /-webkit-app-region:\s*no-drag/.test(tag.textContent)) return true
}
return false
})
expect(hasNoDragRule, 'the bundle must ship the drag-region opt-out rule').toBe(true)
})
test('standard WCO geometry drives the strip reactively (issue #257)', async ({ page }) => {
// The Window Controls Overlay API is the STANDARD signal for shells that
// draw the native caption buttons over web content (Electron
// `titleBarOverlay`). Mock it with the real API shape: the strip must
// follow the reported rect and react to geometrychange (maximize/restore).
await page.addInitScript(() => {
const rect = { x: 0, y: 0, width: 138, height: 36 }
const listeners = new Set<() => void>()
Object.defineProperty(navigator, 'windowControlsOverlay', {
configurable: true,
value: {
visible: true,
getTitlebarAreaRect: () => ({ ...rect }),
addEventListener: (type: string, listener: () => void) => { if (type === 'geometrychange') listeners.add(listener) },
removeEventListener: (type: string, listener: () => void) => { if (type === 'geometrychange') listeners.delete(listener) },
},
})
;(globalThis as { __wcoMock?: { setHeight: (height: number) => void } }).__wcoMock = {
setHeight: (height: number) => {
rect.height = height
for (const listener of listeners) listener()
},
}
})
await page.goto(BASE_URL, { waitUntil: 'domcontentloaded' })
await expect(page.locator('[data-dsh-better-sidebar]')).toBeAttached({ timeout: 90_000 })
// Real reported height (36px, not a hardcoded 32) drives the strip.
await expect(page.locator('body[data-dsh-title-bar-compat]')).toBeAttached({ timeout: 90_000 })
await expect
.poll(() => page.evaluate(() => document.documentElement.style.getPropertyValue('--dsh-title-bar-strip')))
.toBe('36px')
// Maximize → the overlay reports a zero rect → the strip is removed.
await page.evaluate(() => (globalThis as { __wcoMock?: { setHeight: (height: number) => void } }).__wcoMock?.setHeight(0))
await expect(page.locator('body[data-dsh-title-bar-compat]')).toHaveCount(0)
await expect
.poll(() => page.evaluate(() => document.documentElement.style.getPropertyValue('--dsh-title-bar-strip')))
.toBe('')
// Restore → the strip comes back.
await page.evaluate(() => (globalThis as { __wcoMock?: { setHeight: (height: number) => void } }).__wcoMock?.setHeight(36))
await expect(page.locator('body[data-dsh-title-bar-compat]')).toBeAttached()
})
test('opt-in shell preset applies its strip when WCO is absent (data-driven, manual)', async ({ request, page }) => {
// The anywhere-labs DSH Desktop preset (shell-presets.ts) is OPT-IN: under
// the preset scheme the win32 advanced stamp resolves to its 32px fallback
// even without the WCO API; auto never does this.
const update = await request.post(`${BASE_URL}/sidebar/api/settings.update`, {
data: { patch: { titleBarScheme: 'preset', titleBarPresetId: 'dsh-desktop', titleBarCompat: true } },
})
expect(update.ok(), `settings.update: ${update.status()}`).toBe(true)
try {
await page.goto(`${BASE_URL}?dsh-desktop-mode=advanced&dsh-desktop-platform=win32`, { waitUntil: 'domcontentloaded' })
await expect(page.locator('[data-dsh-better-sidebar]')).toBeAttached({ timeout: 90_000 })
await expect(page.locator('body[data-dsh-title-bar-compat]')).toBeAttached({ timeout: 90_000 })
await expect
.poll(() => page.evaluate(() => document.documentElement.style.getPropertyValue('--dsh-title-bar-strip')))
.toBe('32px')
// The v1 anywhere-labs preset is PURE STRIP DATA (no extra CSS — the
// injection mechanism is exercised by the custom-scheme test). Assert
// the absence explicitly: a future preset that ADDS css would trip
// here instead of silently shipping unstyled.
await expect(page.locator('style[data-dsh-preset-css]')).toHaveCount(0)
} finally {
// Restore the shared server state for the lanes after this one.
await request.post(`${BASE_URL}/sidebar/api/settings.update`, {
data: { patch: { titleBarScheme: 'auto', titleBarPresetId: '', customCss: '', titleBarCompat: false } },
})
}
})
test('custom scheme injects the user stylesheet live', async ({ request, page }) => {
const update = await request.post(`${BASE_URL}/sidebar/api/settings.update`, {
data: { patch: { titleBarScheme: 'custom', customCss: 'html { --dsh-e2e-marker: 1; }', titleBarCompat: true } },
})
expect(update.ok(), `settings.update: ${update.status()}`).toBe(true)
try {
await page.goto(BASE_URL, { waitUntil: 'domcontentloaded' })
await expect(page.locator('[data-dsh-better-sidebar]')).toBeAttached({ timeout: 90_000 })
await expect(page.locator('style[data-dsh-custom-css="custom"]')).toBeAttached()
// The injected CSS is live (a custom property the page can read back).
const marker = await page.evaluate(() => getComputedStyle(document.documentElement).getPropertyValue('--dsh-e2e-marker').trim())
expect(marker).toBe('1')
} finally {
await request.post(`${BASE_URL}/sidebar/api/settings.update`, {
data: { patch: { titleBarScheme: 'auto', titleBarPresetId: '', customCss: '', titleBarCompat: false } },
})
}
})
+9 -2
View File
@@ -69,8 +69,15 @@ 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),
// with the strip defaulting to 40px.
// The position-compat scheme is declared WITHOUT a schema default so a
// stored document that predates it resolves without the field — the
// CLIENT parsePrefs then applies the conservative `auto` default (or
// migrates the legacy boolean), which is exactly what makes old
// documents migrate instead of silently flipping to a scheme. The
// legacy strip keeps its schema default of 40px.
expect(resolved.titleBarScheme).toBeUndefined()
expect(resolved.titleBarPresetId).toBeUndefined()
expect(resolved.customCss).toBeUndefined()
expect(resolved.titleBarCompat).toBe(false)
expect(resolved.titleBarStripPx).toBe(40)
// The enable-switch maps resolve to {} (everything on) for old documents.
+50 -13
View File
@@ -45,6 +45,9 @@ describe('side card preferences', () => {
editorExplorer: false,
terminalShell: '',
terminalShellArgs: '',
titleBarScheme: 'auto',
titleBarPresetId: '',
customCss: '',
titleBarCompat: false,
titleBarStripPx: 40,
htmlViewerNoSandbox: false,
@@ -74,6 +77,9 @@ describe('side card preferences', () => {
editorExplorer: false,
terminalShell: '',
terminalShellArgs: '',
titleBarScheme: 'auto',
titleBarPresetId: '',
customCss: '',
titleBarCompat: false,
titleBarStripPx: 40,
htmlViewerNoSandbox: false,
@@ -103,6 +109,9 @@ describe('side card preferences', () => {
editorExplorer: false,
terminalShell: '',
terminalShellArgs: '',
titleBarScheme: 'auto',
titleBarPresetId: '',
customCss: '',
titleBarCompat: false,
titleBarStripPx: 40,
htmlViewerNoSandbox: false,
@@ -151,14 +160,42 @@ describe('side card preferences', () => {
expect((await loadPrefs(wire({ editorExplorer: true }))).editorExplorer).toBe(true)
})
it('defaults titleBarCompat to false; only an explicit true turns the position-compat mode on', async () => {
// Absent or malformed → off (the normal layout is the default).
expect((await loadPrefs(wire({}))).titleBarCompat).toBe(false)
expect((await loadPrefs(wire({ titleBarCompat: 'yes' }))).titleBarCompat).toBe(false)
expect((await loadPrefs(wire({ titleBarCompat: 1 }))).titleBarCompat).toBe(false)
// Explicit booleans survive verbatim.
expect((await loadPrefs(wire({ titleBarCompat: false }))).titleBarCompat).toBe(false)
expect((await loadPrefs(wire({ titleBarCompat: true }))).titleBarCompat).toBe(true)
it('defaults the title-bar scheme to the conservative auto with no preset or custom CSS', async () => {
// Absent or malformed → auto (plain web keeps the untouched layout).
expect((await loadPrefs(wire({}))).titleBarScheme).toBe('auto')
expect((await loadPrefs(wire({ titleBarScheme: 'weird' }))).titleBarScheme).toBe('auto')
expect((await loadPrefs(wire({ titleBarScheme: 1 }))).titleBarScheme).toBe('auto')
expect((await loadPrefs(wire({}))).titleBarPresetId).toBe('')
expect((await loadPrefs(wire({ titleBarPresetId: 5 }))).titleBarPresetId).toBe('')
expect((await loadPrefs(wire({}))).customCss).toBe('')
expect((await loadPrefs(wire({ customCss: 7 }))).customCss).toBe('')
// Valid values survive verbatim (including the explicit web scheme).
const picked = await loadPrefs(wire({ titleBarScheme: 'preset', titleBarPresetId: 'dsh-desktop', customCss: 'html { }' }))
expect(picked.titleBarScheme).toBe('preset')
expect(picked.titleBarPresetId).toBe('dsh-desktop')
expect(picked.customCss).toBe('html { }')
expect((await loadPrefs(wire({ titleBarScheme: 'web' }))).titleBarScheme).toBe('web')
})
it('migrates LEGACY documents that ALREADY HAVE VALUES into the custom scheme', async () => {
// A pre-scheme document with the manual compat flag on maps to the
// custom scheme, keeping the strip px the user chose.
const migrated = await loadPrefs(wire({ titleBarCompat: true, titleBarStripPx: 56 }))
expect(migrated.titleBarScheme).toBe('custom')
expect(migrated.titleBarStripPx).toBe(56)
// A non-default strip px alone (only reachable through the old gear
// popup) also counts as "already has values" → custom.
const stripOnly = await loadPrefs(wire({ titleBarStripPx: 48 }))
expect(stripOnly.titleBarScheme).toBe('custom')
expect(stripOnly.titleBarStripPx).toBe(48)
// A stored scheme always wins over the legacy fields (round-trip of the
// mirrored write: preset stays preset even though the mirror is true).
const roundTrip = await loadPrefs(wire({ titleBarScheme: 'preset', titleBarCompat: true }))
expect(roundTrip.titleBarScheme).toBe('preset')
// Legacy off / absent / default strip → the conservative auto scheme.
expect((await loadPrefs(wire({ titleBarCompat: false }))).titleBarScheme).toBe('auto')
expect((await loadPrefs(wire({ titleBarStripPx: 40 }))).titleBarScheme).toBe('auto')
expect((await loadPrefs(wire({}))).titleBarScheme).toBe('auto')
})
it('defaults titleBarStripPx to 40 and clamps stored values into the contract range', async () => {
@@ -223,9 +260,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, editorExplorer: true, terminalShell: '', terminalShellArgs: '', titleBarCompat: false, titleBarStripPx: 40, 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, editorExplorer: true, terminalShell: '', terminalShellArgs: '', titleBarScheme: 'auto', titleBarPresetId: '', customCss: '', 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, editorExplorer: true, terminalShell: '', terminalShellArgs: '', titleBarCompat: false, titleBarStripPx: 40, 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, editorExplorer: true, terminalShell: '', terminalShellArgs: '', titleBarScheme: 'auto', titleBarPresetId: '', customCss: '', 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)
@@ -261,7 +298,7 @@ describe('side card preferences', () => {
it('skips the default seed tab when the editor (files window) 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, editorExplorer: true, terminalShell: '', terminalShellArgs: '', titleBarCompat: false, titleBarStripPx: 40, htmlViewerNoSandbox: false, htmlViewerDefaultUnsafe: false, browserNoSandbox: false, browserInterceptLinks: true, browserInterceptHttp: true, browserInterceptHttps: false, tabsEnabled: { editor: false }, viewersEnabled: {}, pluginSettings: {} })
store.setPrefs({ openByDefault: true, defaultWidthPercent: 30, autoOpenSubagent: true, autoOpenJobs: true, agentTerminalTools: false, bottomPanelAutoTerminal: true, terminalFontFamily: '', terminalFontSize: 13, interceptOpenPath: true, editorExplorer: true, terminalShell: '', terminalShellArgs: '', titleBarScheme: 'auto', titleBarPresetId: '', customCss: '', titleBarCompat: false, titleBarStripPx: 40, htmlViewerNoSandbox: false, htmlViewerDefaultUnsafe: false, browserNoSandbox: false, browserInterceptLinks: true, browserInterceptHttp: true, browserInterceptHttps: false, tabsEnabled: { editor: false }, viewersEnabled: {}, pluginSettings: {} })
store.setSession('no-editor')
const state = store.getSnapshot().state!
const tabs = allLeaves(state.splits).flatMap(leaf => leaf.tabs)
@@ -271,7 +308,7 @@ describe('side card preferences', () => {
// editorExplorer modes.
for (const editorExplorer of [true, false]) {
const openStore = createSidebarStore()
openStore.setPrefs({ openByDefault: true, defaultWidthPercent: 30, autoOpenSubagent: true, autoOpenJobs: true, agentTerminalTools: false, bottomPanelAutoTerminal: true, terminalFontFamily: '', terminalFontSize: 13, interceptOpenPath: true, editorExplorer, terminalShell: '', terminalShellArgs: '', titleBarCompat: false, titleBarStripPx: 40, 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, editorExplorer, terminalShell: '', terminalShellArgs: '', titleBarScheme: 'auto', titleBarPresetId: '', customCss: '', titleBarCompat: false, titleBarStripPx: 40, htmlViewerNoSandbox: false, htmlViewerDefaultUnsafe: false, browserNoSandbox: false, browserInterceptLinks: true, browserInterceptHttp: true, browserInterceptHttps: false, tabsEnabled: {}, viewersEnabled: {}, pluginSettings: {} })
openStore.setSession(`with-editor-${editorExplorer}`)
const openTabs = allLeaves(openStore.getSnapshot().state!.splits).flatMap(leaf => leaf.tabs)
expect(openTabs.map(tab => tab.type)).toEqual(['editor'])
@@ -281,7 +318,7 @@ describe('side card preferences', () => {
it('seeds the empty editor home tab (files window) in both editorExplorer modes', () => {
for (const editorExplorer of [true, false]) {
const store = createSidebarStore()
store.setPrefs({ openByDefault: true, defaultWidthPercent: 30, autoOpenSubagent: true, autoOpenJobs: true, agentTerminalTools: false, bottomPanelAutoTerminal: true, terminalFontFamily: '', terminalFontSize: 13, interceptOpenPath: true, editorExplorer, terminalShell: '', terminalShellArgs: '', titleBarCompat: false, titleBarStripPx: 40, htmlViewerNoSandbox: false, htmlViewerDefaultUnsafe: false, browserNoSandbox: false, browserInterceptLinks: true, browserInterceptHttp: true, browserInterceptHttps: false, tabsEnabled: {}, viewersEnabled: {}, pluginSettings: {} })
store.setPrefs({ openByDefault: true, defaultWidthPercent: 30, autoOpenSubagent: true, autoOpenJobs: true, agentTerminalTools: false, bottomPanelAutoTerminal: true, terminalFontFamily: '', terminalFontSize: 13, interceptOpenPath: true, editorExplorer, terminalShell: '', terminalShellArgs: '', titleBarScheme: 'auto', titleBarPresetId: '', customCss: '', titleBarCompat: false, titleBarStripPx: 40, htmlViewerNoSandbox: false, htmlViewerDefaultUnsafe: false, browserNoSandbox: false, browserInterceptLinks: true, browserInterceptHttp: true, browserInterceptHttps: false, tabsEnabled: {}, viewersEnabled: {}, pluginSettings: {} })
store.setSession(`fresh-${editorExplorer}`)
const tabs = allLeaves(store.getSnapshot().state!.splits).flatMap(leaf => leaf.tabs)
expect(tabs).toHaveLength(1)
+66
View File
@@ -0,0 +1,66 @@
/**
* Built-in shell-preset data tests: registry integrity (unique ids, non-empty
* titles, pure strip/detect functions) and the anywhere-labs DSH Desktop
* entry's strip values (darwin advanced 20px, win32 advanced 32px as the
* no-WCO fallback, compatibility/plain browser nothing).
*/
import { describe, expect, it } from 'vitest'
import { getShellPreset, getShellPresets, presetStripFor } from '../src/client/shell-presets.ts'
import type { DesktopEnv } from '../src/client/desktop-env.ts'
const env = (partial: Partial<DesktopEnv>): DesktopEnv => ({
desktop: false,
mode: null,
platform: null,
titlebarInset: 0,
...partial,
})
describe('shell presets', () => {
it('keeps the registry well-formed (unique ids, titles, pure strip functions)', () => {
const presets = getShellPresets()
expect(presets.length).toBeGreaterThan(0)
const ids = new Set<string>()
for (const preset of presets) {
expect(ids.has(preset.id)).toBe(false)
ids.add(preset.id)
expect(preset.title.length).toBeGreaterThan(0)
expect(preset.desc.length).toBeGreaterThan(0)
// The strip function must be total — an unknown environment yields
// undefined, never a throw.
expect(() => presetStripFor(preset, env({ desktop: false, mode: null, platform: null }))).not.toThrow()
expect(() => preset.detect?.(env({ desktop: false, mode: null, platform: null }))).not.toThrow()
}
})
it('looks up presets by id; unknown/empty ids resolve to undefined', () => {
expect(getShellPreset('dsh-desktop')).toBeDefined()
expect(getShellPreset('')).toBeUndefined()
expect(getShellPreset('no-such-shell')).toBeUndefined()
})
describe('dsh-desktop (DeepSeek Harness Desktop, anywhere-labs)', () => {
const preset = getShellPreset('dsh-desktop')!
it('reserves 20px on darwin advanced (the caption row above the surfaces)', () => {
expect(presetStripFor(preset, env({ desktop: true, mode: 'advanced', platform: 'darwin' }))).toBe(20)
})
it('reserves 32px on win32 advanced as the no-WCO fallback', () => {
expect(presetStripFor(preset, env({ desktop: true, mode: 'advanced', platform: 'win32' }))).toBe(32)
})
it('reserves nothing in compatibility mode or on unknown platforms', () => {
expect(presetStripFor(preset, env({ desktop: true, mode: 'compatibility', platform: 'win32' }))).toBeUndefined()
expect(presetStripFor(preset, env({ desktop: true, mode: 'advanced', platform: 'linux' }))).toBeUndefined()
expect(presetStripFor(preset, env({ desktop: false, mode: null, platform: null }))).toBeUndefined()
})
it('detects advanced shells only as a SUGGESTION signal (never auto-applies)', () => {
expect(preset.detect?.(env({ desktop: true, mode: 'advanced', platform: 'darwin' }))).toBe(true)
expect(preset.detect?.(env({ desktop: true, mode: 'advanced', platform: 'win32' }))).toBe(true)
expect(preset.detect?.(env({ desktop: true, mode: 'compatibility', platform: 'win32' }))).toBe(false)
expect(preset.detect?.(env({ desktop: false, mode: null, platform: null }))).toBe(false)
})
})
})
+22 -13
View File
@@ -125,8 +125,10 @@ describe('SideCardSection declarative inventory', () => {
const html = renderSection(store, service)
// Subagents declares a toggle → its card carries the settings gear
// (aria-label = "<title> Feature settings"); Explorer and Image declare
// none → no gear.
// none → no gear. The position-compat row is a DROPDOWN (no gear unless
// the custom scheme is active), so the total is 1.
expect(html.match(/aria-label="[^"]*Feature settings"/g)?.length).toBe(1)
expect(html).toContain('Subagents Feature settings')
})
it('renders the two dashed "add plugin" cards (tab grid + viewer grid)', () => {
@@ -159,28 +161,35 @@ describe('SideCardSection declarative inventory', () => {
const { store, service } = mount()
store.setPrefs({ ...store.getPrefs(), tabsEnabled: { subagent: false } })
const html = renderSection(store, service)
// The disabled Subagents card loses its gear; the position-compat row
// is a dropdown (default auto → no gear either).
expect(html).not.toContain('Feature settings')
})
it('renders the position-compat mode general row: off by default, checked when the pref is on', () => {
it('renders the position-compat mode row as a scheme dropdown: auto default, custom keeps the gear', () => {
const { store, service } = mount()
let html = renderSection(store, service)
// The general row renders its title and description.
// The general row renders its title and description; the scheme is the
// conservative auto by default and the row is the shared SelectMenu
// dropdown (the closed anchor shows the picked option — NOT a native
// <select>).
expect(html).toContain('Position compatibility mode')
expect(html).toContain('Reserve space for the native Windows title bar')
// Three general rows now: only interceptOpenPath is checked by default
// (openByDefault defaults off, the titleBarCompat row starts UNCHECKED) —
// the checked checkbox count is 1 while the total checkbox count is 3.
expect(html.match(/type="checkbox"/g)?.length).toBe(3)
expect(html).toContain('Pick the title-bar compatibility scheme: auto-detect (default, conservative) / DSH official web / known desktop shells / custom (shift distance + custom CSS)')
expect(html).not.toContain('<select')
expect(html).toContain('>Auto-detect<')
// Two general-row switches remain (openByDefault + interceptOpenPath),
// only interceptOpenPath checked by default — the scheme row is a
// dropdown, not a switch.
expect(html.match(/type="checkbox"/g)?.length).toBe(2)
expect(html.match(/checked=""/g)?.length).toBe(1)
// The row's gear (customize the shift distance) is dormant while the
// mode is off — the feature-card convention.
// Auto (default) needs no further settings → no gear.
expect(html).not.toContain('Position compatibility mode Feature settings')
// When the pref is on, the new switch is checked and the gear appears.
store.setPrefs({ ...store.getPrefs(), titleBarCompat: true })
// The custom scheme keeps its settings button (the px + CSS popup);
// the anchor now shows the picked custom option.
store.setPrefs({ ...store.getPrefs(), titleBarScheme: 'custom', titleBarCompat: true })
html = renderSection(store, service)
expect(html.match(/checked=""/g)?.length).toBe(2)
expect(html).toContain('>Custom<')
expect(html).toContain('aria-label="Position compatibility mode Feature settings"')
})
})
+75
View File
@@ -0,0 +1,75 @@
/**
* The title-bar strip resolution chain: standard WCO geometry first
* (authoritative even when 0), then the documented URL inset contract
* parameter, then the opt-in preset / custom scheme — and 0 (plain web,
* nothing modified) otherwise. No per-shell branch lives in the core.
*/
import { describe, expect, it } from 'vitest'
import { computeTitleBarStrip } from '../src/client/titlebar-strip.ts'
import type { DesktopEnv } from '../src/client/desktop-env.ts'
import type { WcoSnapshot } from '../src/client/wco.ts'
import type { ShellPreset } from '../src/client/shell-presets.ts'
const env = (partial: Partial<DesktopEnv>): DesktopEnv => ({
desktop: false,
mode: null,
platform: null,
titlebarInset: 0,
...partial,
})
const wco = (present: boolean, height = 0): WcoSnapshot => ({ present, height })
const noPreset: ShellPreset | undefined = undefined
describe('computeTitleBarStrip', () => {
it('keeps plain web untouched: no WCO, no inset, no scheme → 0', () => {
expect(computeTitleBarStrip(env({}), wco(false), 'auto', noPreset, 40)).toBe(0)
expect(computeTitleBarStrip(env({}), wco(false), 'custom', noPreset, 40)).toBe(40)
})
it('the explicit WEB scheme forces 0 — not even standard WCO geometry applies', () => {
const desktop = env({ desktop: true, mode: 'advanced', platform: 'win32' })
// The user declared "DSH official web": no adaptation at all, even when
// a real overlay exists or a preset is active.
expect(computeTitleBarStrip(desktop, wco(true, 36), 'web', noPreset, 40)).toBe(0)
expect(computeTitleBarStrip(desktop, wco(false), 'web', noPreset, 40)).toBe(0)
expect(computeTitleBarStrip(desktop, wco(false), 'web', {
id: 't', title: 't', desc: '', stripFor: () => 20,
}, 40)).toBe(0)
})
it('trusts the standard WCO geometry first in every scheme (even 0 — e.g. maximized)', () => {
const desktop = env({ desktop: true, mode: 'advanced', platform: 'win32' })
expect(computeTitleBarStrip(desktop, wco(true, 36), 'auto', noPreset, 0)).toBe(36)
expect(computeTitleBarStrip(desktop, wco(true, 36), 'preset', noPreset, 0)).toBe(36)
expect(computeTitleBarStrip(desktop, wco(true, 36), 'custom', noPreset, 40)).toBe(36)
// Authoritative zero: the overlay is hidden while maximized — no strip.
expect(computeTitleBarStrip(desktop, wco(true, 0), 'auto', noPreset, 40)).toBe(0)
})
it('applies the documented URL inset contract parameter before any scheme', () => {
const stamped = env({ desktop: true, mode: 'advanced', platform: 'darwin', titlebarInset: 20 })
expect(computeTitleBarStrip(stamped, wco(false), 'auto', noPreset, 0)).toBe(20)
expect(computeTitleBarStrip(stamped, wco(false), 'preset', noPreset, 0)).toBe(20)
})
it('uses the opt-in preset strip under the preset scheme only', () => {
const preset: ShellPreset = {
id: 't', title: 't', desc: '',
stripFor: (e) => e.platform === 'darwin' ? 20 : undefined,
}
const darwin = env({ desktop: true, mode: 'advanced', platform: 'darwin' })
expect(computeTitleBarStrip(darwin, wco(false), 'preset', preset, 0)).toBe(20)
// Auto never applies the preset — only standard signals.
expect(computeTitleBarStrip(darwin, wco(false), 'auto', preset, 0)).toBe(0)
// Custom never applies the preset either.
expect(computeTitleBarStrip(darwin, wco(false), 'custom', preset, 0)).toBe(0)
})
it('uses the manual strip px under the custom scheme only', () => {
expect(computeTitleBarStrip(env({}), wco(false), 'custom', noPreset, 56)).toBe(56)
expect(computeTitleBarStrip(env({}), wco(false), 'auto', noPreset, 56)).toBe(0)
expect(computeTitleBarStrip(env({}), wco(false), 'preset', noPreset, 56)).toBe(0)
})
})
+125
View File
@@ -0,0 +1,125 @@
/**
* Window Controls Overlay (WCO) geometry store tests: feature detection,
* subscribe/getSnapshot, geometrychange re-reads, and the absent-API
* fallback (the conservative "auto" signal — plain browsers and non-overlay
* shells report NONE and the sidebar adapts nothing).
*/
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
import './browser-globals.ts'
import {
getWcoSnapshot, setWcoSourceForTests, subscribeWco, WCO_NONE,
type WcoSource,
} from '../src/client/wco.ts'
/** A controllable fake of the standard API surface. */
function makeSource(initial: { width: number; height: number; visible?: boolean }): {
source: WcoSource
setRect: (width: number, height: number) => void
fireGeometryChange: () => void
listeners: Set<() => void>
} {
const listeners = new Set<() => void>()
let rect = { x: 0, y: 0, width: initial.width, height: initial.height }
const source: WcoSource = {
visible: initial.visible ?? true,
getTitlebarAreaRect: () => rect,
addEventListener: (type, listener) => { if (type === 'geometrychange') listeners.add(listener) },
removeEventListener: (type, listener) => { if (type === 'geometrychange') listeners.delete(listener) },
}
return {
source,
setRect: (width, height) => { rect = { x: 0, y: 0, width, height } },
fireGeometryChange: () => { for (const listener of listeners) listener() },
listeners,
}
}
beforeEach(() => {
setWcoSourceForTests(undefined)
})
afterEach(() => {
setWcoSourceForTests(undefined)
})
describe('wco geometry store', () => {
it('reports NONE (absent API) in plain browsers / non-overlay shells', () => {
expect(getWcoSnapshot()).toBe(WCO_NONE)
expect(getWcoSnapshot().present).toBe(false)
expect(getWcoSnapshot().height).toBe(0)
})
it('reads the real overlay rect once a source is present', () => {
const { source } = makeSource({ width: 138, height: 36 })
setWcoSourceForTests(source)
expect(getWcoSnapshot()).toEqual({ present: true, height: 36 })
})
it('rounds fractional overlay heights to whole pixels', () => {
const { source } = makeSource({ width: 100, height: 35.6 })
setWcoSourceForTests(source)
expect(getWcoSnapshot().height).toBe(36)
})
it('re-reads geometry on geometrychange (maximize/restore) and notifies subscribers', () => {
const { source, setRect, fireGeometryChange, listeners } = makeSource({ width: 138, height: 32 })
setWcoSourceForTests(source)
const calls: number[] = []
const off = subscribeWco(() => { calls.push(getWcoSnapshot().height) })
// Maximized: the overlay reports a zero/empty rect.
setRect(0, 0)
fireGeometryChange()
expect(getWcoSnapshot()).toEqual({ present: true, height: 0 })
expect(calls).toEqual([0])
// Restored.
setRect(138, 36)
fireGeometryChange()
expect(getWcoSnapshot()).toEqual({ present: true, height: 36 })
expect(calls).toEqual([0, 36])
// The disposer stops notifications.
off()
setRect(138, 40)
fireGeometryChange()
expect(calls).toEqual([0, 36])
expect(listeners.size).toBe(0)
})
it('treats a PHANTOM API (present but not visible) as absent — headless/macOS expose the interface with no overlay', () => {
const { source } = makeSource({ width: 0, height: 0, visible: false })
setWcoSourceForTests(source)
// present=false lets the strip chain fall through to the inset / preset
// / custom scheme instead of trusting an empty rect.
expect(getWcoSnapshot()).toEqual({ present: false, height: 0 })
// A real overlay flips to authoritative immediately.
const real = makeSource({ width: 138, height: 36, visible: true })
setWcoSourceForTests(real.source)
expect(getWcoSnapshot()).toEqual({ present: true, height: 36 })
})
it('treats a throwing API as absent (never breaks the layout)', () => {
const source: WcoSource = {
visible: true,
getTitlebarAreaRect: () => { throw new Error('racy API') },
addEventListener: () => {},
removeEventListener: () => {},
}
setWcoSourceForTests(source)
expect(getWcoSnapshot()).toEqual({ present: false, height: 0 })
})
it('re-attaches cleanly when the source is swapped (test hook)', () => {
const first = makeSource({ width: 138, height: 32 })
const second = makeSource({ width: 138, height: 36 })
setWcoSourceForTests(first.source)
expect(first.listeners.size).toBe(0) // no subscriber yet
const off = subscribeWco(() => {})
expect(first.listeners.size).toBe(1)
setWcoSourceForTests(second.source)
expect(first.listeners.size).toBe(0)
expect(second.listeners.size).toBe(1)
expect(getWcoSnapshot().height).toBe(36)
off()
setWcoSourceForTests(undefined)
expect(getWcoSnapshot()).toBe(WCO_NONE)
})
})